Puppeteer 常見的一種失敗模式是:一開始請求都正常,接著後面的請求開始回傳 403 或 429、逾時,或直接進到驗證挑戰頁。但很多教學仍把輪換代理講得像是改兩行設定就能搞定。
事實並不是這樣。從「加上 --proxy-server 參數」的範例,到真正可維護的系統,中間還差非常大一段。這篇指南會涵蓋瀏覽器層級輪換、帶認證的閘道、瀏覽器分片或外部中繼、一致的瀏覽器設定檔、生產等級的錯誤處理,以及一個誠實的答案:什麼時候你根本不該自己管代理。
什麼是輪換代理?為什麼 Puppeteer 需要它?
代理伺服器位在你的 Puppeteer 實例和目標網站之間。網站看到的是代理伺服器的出口 IP,而不是你本機的 IP。輪換代理會在一組出口 IP 之間切換——有時候是每次請求切一次,有時候是每個 session 切一次——這樣你的流量就不會看起來像同一個客戶端連續狂打同一台伺服器一千次。
Puppeteer 特別需要這個機制,因為 headless Chrome 從單一 IP 發出數百個連續請求,正是反機器人系統最擅長抓的模式。Cloudflare 自己的文件就提到,它們會同時運行多層偵測——規則判斷、JavaScript 指紋檢查、機器學習模型,以及行為異常偵測。更換 IP 只能解決其中一層,而且只是其中一層。
有三種代理類型值得認識,而且彼此不能互換:
- 資料中心代理:便宜、快,來源是主機代管商。因為 ASN(網路區塊)明顯是資料中心,不像住宅網路,所以很容易被目標網站標記。
- 住宅代理:經由真實的家用 ISP 路由,看起來像一般住家網路。雖然較慢也較貴,但可信度高得多。
- 行動代理:電信商網路 IP,通常是最貴的選項,適合真的需要行動網路身分的情境。
單看 ASN 分類,住宅出口通常比資料中心出口更不顯眼,但兩者都不是免封鎖的。也不存在什麼通用偵測率:結果取決於目標網站、出口聲譽、地理位置、session 歷史、瀏覽器設定檔與請求行為。
還有一個很容易搞混的差別:自己維護的靜態清單輪換(你管理代理池、挑下一個 IP、處理失敗)和 backconnect/gateway 代理(你只連一個端點,供應商在背後自動輪換出口)是不同的。兩者都可行,只是把複雜度放在不同地方。
為什麼要在 Puppeteer 中設定輪換代理?常見使用場景
老實說:你大概不需要輪換,直到你真的需要;一旦需要,就會非常需要。
| 使用場景 | 為什麼需要輪換 |
|---|---|
| 商品目錄價格監控 | 同一個 IP 反覆抓目錄頁,容易累積速率限制與聲譽訊號 |
| 潛在客戶補全/聯絡資料擷取 | 同一個 IP 一再查看個人頁面,看起來不像瀏覽而像爬取,很容易被行為引擎標記 |
| SERP 抓取 | 搜尋引擎在 IP 層級限流與 CAPTCHA 封鎖方面通常最嚴格 |
| 競爭情資蒐集 | 長時間反覆抓同一網域,會把你的 IP 與 cookie 歷史綁成固定指紋 |
| 內容彙整 | 高頁面量、每頁價值低——這正是反機器人系統最擅長辨識的流量形狀 |
沒有什麼像「Amazon 在第 51 次請求就封鎖」這種可引用的固定數字。網站不會公開通用門檻,而且控制條件會隨端點、帳號狀態、ASN 聲譽與流量型態改變。最好的做法是從最低且被授權的請求速率開始,先驗證內容而不只是狀態碼,只有在實際行為與目標政策需要時再加入輪換。
Puppeteer 的三種代理輪換策略:你該用哪一種?

這一段是大多數教學會完全略過,或者只講最粗糙版本的地方。其實有三種粒度可選,選錯不是浪費時間,就是把簡單工作搞得過度複雜。
| 輪換策略 | 粒度 | 需要重啟瀏覽器嗎? | 複雜度 | 最適合 |
|---|---|---|---|---|
每個瀏覽器一個代理(--proxy-server) | 每個瀏覽器實例 1 個代理 | 是 | 低 | 簡單、低流量的抓取 |
閘道管理(proxy-chain + backconnect 端點) | 由供應商/session 政策決定 | 否 | 中 | 帶認證的輪換閘道 |
| 瀏覽器分片或外部中繼 | 每個瀏覽器分片或中繼規則 1 個代理 | 無法在單一程序內直接切換 | 高 | 受控並發與細粒度路由 |
在你選擇之前,先提醒一點:Puppeteer 官方的網路攔截文件明確說明,setRequestInterception 並不是一個能「每個請求直接換代理」的乾淨開關——每個被攔截的請求都會暫停,直到你明確呼叫 continue、respond 或 abort。真正做到每請求路由,通常得把請求丟給本機可程式化的中繼(像 proxy-chain),而不是在攔截處理器裡硬切代理。下面的 Method 3 先記住這點。
如何在 Puppeteer 中設定輪換代理:逐步教學
難度: 中階
所需時間: 約 30–45 分鐘(涵蓋三種方法)
你需要準備: Node.js 18+、npm、代理清單或供應商帳號(格式:protocol://user:pass@host:port),以及 puppeteer、proxy-chain、puppeteer-extra 套件
前置準備:開始前你需要什麼
先安裝核心套件:
npm install puppeteer proxy-chain puppeteer-extra puppeteer-extra-plugin-stealth
從代理供應商取得代理清單(如果不是單純測試,優先考慮住宅代理),或者至少先準備幾個測試代理,確認程式沒問題後再拿真正的請求量上去。憑證請放在環境變數裡——不要寫死在程式碼,也不要把它放在最後會進 log 的 URL 裡。
方法 1:用 --proxy-server 做每個瀏覽器的代理輪換
這是每個人最先上手的基礎方法,而且很合理——它很直觀。Puppeteer 的 LaunchOptions文件說明 args 就是用來傳 Chrome 命令列參數的,而 --proxy-server 本來就是 Chromium 的原生參數。
import puppeteer from 'puppeteer';
const proxyPool = [
'http://proxy1.example:8080',
'http://proxy2.example:8080',
'http://proxy3.example:8080',
];
let proxyIndex = 0;
async function scrapeWithRotation(url) {
const proxy = proxyPool[proxyIndex % proxyPool.length];
proxyIndex++;
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=${proxy}`],
});
const page = await browser.newPage();
// 如果代理需要驗證,這一步必須在任何導覽之前執行
await page.authenticate({
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS,
});
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
const content = await page.content();
await browser.close(); // 切換代理前先關閉
return content;
}
請注意,page.authenticate() 其實會在背後悄悄啟用 request interception——這是 Puppeteer 官方文件提到的。這會帶來一點效能成本,值得知道,免得你之後在 debug 為什麼速度比預期慢。
預期結果: 每次呼叫都會啟動一個綁定不同代理的新瀏覽器。要輪換,就得關閉再重開,啟動成本無法避免。若你要抓 50 個頁面,單純因為瀏覽器啟動時間,速度會明顯比另外兩種方法慢。
適用時機: 低並發腳本、一次性抓取、比起速度更重視除錯簡單性的情境。
方法 2:用 proxy-chain 搭配帶認證的輪換閘道
Chrome 不接受把 user:pass@host 這種憑證直接嵌進代理 URL。proxy-chain(由 Apify 維護)透過啟動一個本機匿名代理,將流量轉送到你的上游認證代理,解決了這個驗證問題。如果上游本身就是供應商的輪換或 backconnect 閘道,供應商會根據 session 政策,在那個端點背後切換出口 IP。proxy-chain 本身不會替每個現有的 Puppeteer page 指派不同代理。
import puppeteer from 'puppeteer';
import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain';
async function scrapeThroughGateway(upstreamProxyUrl, targetUrl) {
const localProxy = await anonymizeProxy(upstreamProxyUrl);
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=${localProxy}`],
});
const page = await browser.newPage();
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
return await page.content();
} finally {
if (browser) await browser.close();
await closeAnonymizedProxy(localProxy, true); // 一定要清理
}
}
這段 finally 不是裝飾品——沒關掉的本機代理會把 port 卡住,我曾經看過爬蟲因為沒關匿名代理,整晚默默吃掉可用的檔案描述符。proxy-chain 也會提供特定錯誤碼(DNS 問題是 593、連線被拒絕是 594、驗證失敗是 597),對分類失敗原因非常有幫助——後面還會提到。
適用時機: 供應商管理的住宅/資料中心閘道,輪換由供應商端點或 session 參數控制。如果你需要多個固定代理身分同時並行,應該使用多個瀏覽器程序(瀏覽器分片)或專門設計的外部中繼;原生 Puppeteer 並沒有支援每個 page 單獨設定代理的介面。
方法 3:每請求路由需要外部中繼
這是粒度最高的選項——理論上,頁面上的每張圖片、每個 script、每次 API 呼叫都可以走不同出口。實務上,它也是最脆弱、文件最少的一種做法,因為 Puppeteer 的 request interception 是為了篩選與修改請求設計,不是為了每個請求切換網路傳輸路徑。
import puppeteer from 'puppeteer';
async function inspectRequests(url) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', async (request) => {
// 實務上,真正的每請求代理切換需要透過本機中繼(proxy-chain)轉送,
// 而不是在傳輸途中直接切換瀏覽器的路由——Chrome 不支援這樣做。
// 大多數生產環境會把這個處理器用來篩選/中止資源類型,
// 再搭配瀏覽器分片或閘道使用。
if (['image', 'font', 'stylesheet'].includes(request.resourceType())) {
request.abort();
} else {
request.continue();
}
});
await page.goto(url, { waitUntil: 'networkidle2' });
await browser.close();
}
坦白說: setRequestInterception() 並不能在 Puppeteer 內部真正做到每請求換 IP。如果你真的需要那種粒度,請把 Chrome 經由可程式化的外部中繼來路由,或使用本來就是為 proxy session 設計的爬取框架。對多數專案來說,每個瀏覽器分片一個代理,或使用供應商管理的輪換閘道,會更容易營運和稽核。
完整反偵測堆疊:只靠輪換代理,還是可能被封
常見抱怨是:「我明明用了代理還是被擋。」IP 位址只是現代機器人系統可以評估的多個訊號之一;如果只輪換 IP、其他訊號都不一致,反而可能製造更明顯的異常。像是瀏覽器自稱是 Windows Chrome,但 client hints、時區或地區語言卻對不上,這就是很明顯的例子。
第 1 層:輪換住宅代理
前面已經說過——住宅出口通常比資料中心出口更合理,但不存在什麼通用的最小代理池大小。代理池大小應根據實際請求量、session 長度、冷卻時間與供應商重用行為來估算,而不是隨便寫一個數字。
第 2 層:用 Stealth 外掛遮蔽 headless Chrome 訊號
puppeteer-extra-plugin-stealth 會修補一批已知的 headless 特徵:navigator.webdriver、WebGL vendor 字串、缺少的 Chrome runtime 物件,以及一些其他 CDP 洩漏。這確實是有用的相容層,但專案自己的 README也很坦白:這本質上是一場貓捉老鼠遊戲,想完全防住大概不可能。把它當基礎,不要把它當保證。
第 3 層:一致的瀏覽器設定檔與合理的請求節奏
User-Agent 字串必須和瀏覽器回報的其他資訊一致。Chrome 的 User-Agent Client Hints 會暴露結構化的平台資料,所以手寫的 user-agent 可能會和真實平台互相矛盾。建議直接使用隨附 Chrome 版本提供的 user agent,並在同一個 session 中保持 viewport、locale、timezone 穩定,不要每個頁面都硬造一個全新指紋。
下面把這三層組合在一起:
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain';
puppeteer.use(StealthPlugin());
function boundedDelay(minMs = 800, maxMs = 1800) {
return new Promise((r) => setTimeout(r, minMs + Math.random() * (maxMs - minMs)));
}
async function stableProfileScrape(targetUrl, upstreamProxy) {
const localProxy = await anonymizeProxy(upstreamProxy);
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=${localProxy}`, '--lang=en-US'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1366, height: 768 });
await boundedDelay();
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
return await page.content();
} finally {
if (browser) await browser.close();
await closeAnonymizedProxy(localProxy, true);
}
}
這就是大多數競品指南不會展示的部分——代理、stealth 與指紋隨機化放在同一段,能直接複製再調整。
生產環境等級的錯誤處理與代理健康檢查

大多數教學都在成功路徑跑通的那一刻就停了。真正的爬取會一直失敗——代理會掛、憑證會過期、目標會在執行中限流——這些都不是靠祈禱就能解決的。
使用指數退避與抖動的重試邏輯
function backoffMs(attempt, base = 1000, cap = 30_000) {
const exponential = Math.min(cap, base * 2 ** attempt);
return Math.floor(exponential * (0.5 + Math.random() * 0.5)); // jitter 可避免驚群效應
}
async function withRetry(fn, maxRetries = 4) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries) throw err;
const delay = backoffMs(attempt);
console.warn(`第 ${attempt + 1} 次嘗試失敗:${err.message}。${delay}ms 後重試`);
await new Promise((r) => setTimeout(r, delay));
}
}
}
自動封鎖失敗代理
const proxyStats = new Map(); // proxyUrl -> { success, failure }
function recordResult(proxyUrl, success) {
const stats = proxyStats.get(proxyUrl) || { success: 0, failure: 0 };
success ? stats.success++ : stats.failure++;
proxyStats.set(proxyUrl, stats);
}
function isHealthy(proxyUrl) {
const stats = proxyStats.get(proxyUrl);
if (!stats) return true;
const total = stats.success + stats.failure;
if (total < 5) return true; // 資料還不夠
return stats.failure / total < 0.5; // 失敗率超過 50% 就列入黑名單
}
function getHealthyProxy(pool) {
const healthy = pool.filter(isHealthy);
if (healthy.length === 0) throw new Error('代理池中已沒有可用的健康代理');
return healthy[Math.floor(Math.random() * healthy.length)];
}
要追蹤的是錯誤類型,而不只是通過或失敗——407(憑證錯誤)和 429(速率限制)需要完全不同的處理方式。對一個認證失敗的代理猛打快速重試,只會浪費時間;真正要修的是檢查憑證,不是更快地換代理。
Puppeteer 輪換代理常見錯誤排查
| 錯誤 | 可能原因 | 修正方式 |
|---|---|---|
ERR_PROXY_CONNECTION_FAILED | 代理掛掉或無法連線 | 從池中移除,改用下一個代理重試 |
407 Proxy Authentication Required | 憑證錯誤或不支援的驗證方式 | 檢查 page.authenticate() 的帳密;若憑證嵌在 URL 中,改用 proxy-chain |
TimeoutError | 代理太慢或目標網站阻擋 | 拉高 timeout;改用住宅代理 |
403 Forbidden | IP 或指紋被標記 | 輪換代理 + 啟用 stealth + 隨機化 UA |
ERR_TUNNEL_CONNECTION_FAILED | HTTPS 隧道問題 | 檢查是否支援 CONNECT 方法;試試 proxy-chain 的本機隧道 |
還有幾個值得知道、但不太好放進表格裡的點:200 狀態碼不代表成功。軟封鎖常常回傳一個完整的 HTML 頁面——像登入牆或挑戰頁——但狀態碼看起來仍然正常,所以你要驗證的是實際內容,而不只是 response status。當你卡住時,Puppeteer 的除錯指南建議用 headless: false 執行、加入 slowMo,並設定 NODE_DEBUG="puppeteer:*" 來取得更詳細的 protocol log——但要注意,這些 log 可能包含敏感請求資料,所以不要拿它們長時間跑正式環境憑證。
自管代理輪換 vs 代理閘道 vs AI 擷取 API
| 比較項目 | 自己維護清單輪換 | Backconnect 閘道(Bright Data、Oxylabs、Decodo) | AI 擷取 API(Thunderbit) |
|---|---|---|---|
| 成本(低流量) | 低到中 | 每 GB 中到高 | 低(有免費額度,之後按量計費) |
| 穩定性 | 取決於你的健康檢查 | 高(供應商代管) | 高(代管基礎設施) |
| 反偵測 | 自己做 | 部分(只有 IP 輪換) | 內建 |
| 結構化輸出 | 沒有(原始 HTML) | 沒有(原始 HTML) | 有(透過 schema 產出 JSON) |
| 設定時間 | 數小時 | 數分鐘 | 數分鐘 |
| 控制程度 | 完整控制 | 受限於供應商 API | 受限於 schema 模型 |
截至 2026-08-07 的實際供應商價格,可以看出閘道方案的成本曲線:Bright Data 的住宅代理定價提供隨用隨付與流量方案,促銷會變動;Oxylabs 顯示 5 GB 為每 GB 6 美元、1 TB 為每 GB 2.50 美元;Decodo(前身為 Smartproxy)顯示 3 GB 時每 GB 3.75 美元、100 GB 時每 GB 2.75 美元,以及每 GB 4 美元的隨用隨付方案。Decodo 也宣稱有 1.15 億以上 IP 池與 99.92% 成功率——這些都是供應商宣稱,並非獨立重現的基準。
我實際會用的判斷方式是:你是不是需要跟頁面互動——點擊、捲動、填表、維持登入 session?那就用 Puppeteer 加代理。你只是需要頁面上已經存在的資料?那先看看擷取 API,再決定要不要自己建一套永遠得維護的代理基礎設施。
什麼時候 Puppeteer + 代理太大材小用:改用 API 擷取結構化資料
我大概是在第三次重建代理健康檢查系統時才想通:某些專案其實只是想把產品價格放進試算表,卻搭了一整套龐大的基礎設施。很多這些東西只是為了處理一件事——把原始 HTML 從頁面上抓下來——但這其實不是開發者真正的目標。真正的目標是結構化資料。HTML 只是麻煩的中繼格式。
Thunderbit 的 Open API把「擷取」當成主要操作,而不是瀏覽器自動化的副作用。POST /extract 只要傳入 URL 和 JSON Schema,就會回傳匹配後的結構化資料,而且把 JS 渲染、反機器人機制與 CAPTCHA 這些事情放在後端處理,不必你自己去串 stealth 外掛與代理池:
curl -X POST https://openapi.thunderbit.com/openapi/v1/extract \
-H "Authorization: Bearer $THUNDERBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/product",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"price": {"type": "number"}
},
"required": ["name", "price"]
}
}'
另外還有 POST /distill,適合你只想要乾淨的 Markdown,而不是嚴格 schema 的情境;也支援批次擷取,可以一次把同一個 schema 套用到多個 URL。根據 Thunderbit 目前的 API 定價,Distill 每頁 1 點數,Extract 每頁 20 點數——免費方案包含 600 點一次性額度,足夠你先測試流程,再決定是否正式導入。
如果你在 Claude、Cursor 或其他相容 MCP 的客戶端裡工作,Thunderbit 也提供 thunderbit_extract 和 thunderbit_distill 作為 MCP 工具,讓 agent 在任務進行中自行判斷何時需要從頁面擷取資料,而不是另外再跑一個獨立的爬取步驟。我會建議在串 MCP 設定前,先看最新的 API 參考文件,因為工具名稱與參數可能會隨文件版本變動。
| 面向 | Puppeteer + 輪換代理 | Thunderbit API |
|---|---|---|
| 設定複雜度 | 高——代理池、輪換邏輯、stealth、重試 | 低——單一 API 呼叫搭配 JSON Schema |
| 反機器人處理 | 手動 | 內建 |
| 輸出 | 原始 HTML(需要解析) | 與你的 schema 相符的結構化 JSON |
| 維護成本 | 高——selector 會壞、代理會老化 | 低 |
| 最適合 | 客製自動化、登入流程、特殊互動 | 大規模資料擷取 |
公平地說,自建方案也有它的適用情境:如果你的需求是登入帳號、點擊多步驟流程,或需要在同一個 session 中維持狀態,那擷取 API 通常無法取代——Thunderbit 自己的 FAQ 也直接說明,目前 API 不支援互動式登入流程。這種情況下,Puppeteer 加代理還是更合適。但如果你的工作只是「把一堆公開頁面的資料抓下來,放進我定義的 schema」,那你自己搭代理輪換堆疊,其實是在解一個比你真正需要的更難的問題。對於想完全跳過程式碼的團隊,Thunderbit Chrome 擴充功能也提供同樣的 AI 擷取能力,而且是點選式介面——如果你正在比較 無程式碼網頁爬蟲 和完整開發者方案,這值得一看。
結論與重點整理
在 Puppeteer 裡設定輪換代理,不是一種單一技術。每個瀏覽器各自輪換最簡單,也最隔離。帶認證的 backconnect 閘道可以在單一瀏覽器層級端點後方切換出口。更細粒度的每請求或並行身分切換,則需要瀏覽器分片或外部中繼;光靠 request interception 並不能改變 Chrome 的網路路由。
但少了其他層,這些其實都不夠。代理解決的是 IP 聲譽問題;stealth 外掛與指紋一致性解決的是瀏覽器訊號問題;jitter 與請求節奏解決的是行為問題。少掉任何一層,你還是會被封,只是理由不同。
如果你打算自己建,先從 proxy-chain 倉庫和上面的程式碼開始——它們會比大多數付費課程更實用。如果你想完全跳過代理管理、直接拿結構化資料,Thunderbit 的 API 文件花 10 分鐘看一下,通常比你花整個週末搭一套永遠要維護的健康檢查基礎設施更划算。兩條路都合理——重點是你要解的是自己真正的問題,而不是每個教學預設你有的問題。若想更全面了解 AI 正在如何改變這個領域,也可以看看我們對 AI 網頁爬蟲以及它和傳統方法比較的深入分析。
常見問題
在 Puppeteer 中應該多久輪換一次代理?
這取決於目標網站的限流有多嚴格。對反機器人偵測嚴格的網站,最好每個頁面或每個 session 輪換一次。對比較寬鬆的網站,每個 session 甚至整個抓取流程共用單一 sticky IP 都可能夠用。沒有通用數字——把 403、429 和逾時視為你該更積極輪換的信號,而不是固定的請求次數。
Puppeteer 抓取可以用免費代理嗎?
技術上可以,但除了快速測試之外我不建議。免費代理清單通常很慢、不穩,而且很多早就被你要抓的網站列入黑名單。只要是正式環境,付費供應商的住宅代理或代管閘道都很值得。
puppeteer-extra-plugin-stealth 能對抗所有反機器人系統嗎?
不能,而且它自己的文件也明講了。它能減少一些常見的 headless Chrome 訊號,但目標網站仍然可以評估網路聲譽、TLS 特徵、cookie、client hints 和行為。把它當成一層相容性處理,不是保證。
proxy-chain 和 Puppeteer 的 --proxy-server 有什麼差別?
--proxy-server 是 Chromium 原生的啟動參數,會把一個代理端點指派給整個瀏覽器實例,而且 Chrome 不接受在那裡直接嵌入代理憑證。proxy-chain 則會建立一條連到上游認證代理的本機匿名隧道。之後的輪換來自重新啟動並換另一個上游、供應商管理的 backconnect 閘道,或你自己設計的中繼,而不是 proxy-chain 替單獨的 Puppeteer page 分派代理。
只靠輪換代理就能完全避免被封嗎?
不能——而且這是最常見的誤解。像 Cloudflare 的 bot management 這類現代反機器人系統,會把 IP 聲譽與瀏覽器指紋、行為模式、session 歷史一起關聯分析。代理只解決 IP 聲譽這一部分;你還需要 stealth 設定、一致的指紋與合理的時間間隔,才能避免在其他訊號上被標記。
延伸閱讀


