Puppeteer 里常见的一种失败模式是:前几次请求都很顺,后面却开始返回 403 或 429,或者直接超时,甚至跳到验证页面。可很多教程还是把轮换代理讲得像是改两行配置就搞定了。
事实并不是这样。把“加上 --proxy-server 参数”和搭出一个能长期维护的系统,这两者之间差得非常远。这篇指南会讲到浏览器级轮换、带认证的网关、浏览器分片或外部中继、稳定的浏览器配置文件、生产级错误处理,以及什么时候你根本不该自己管代理。
什么是轮换代理?为什么 Puppeteer 需要它?
代理位于你的 Puppeteer 实例和目标网站之间。目标网站看到的是代理出口 IP,而不是你机器的 IP。轮换代理会在一组出口 IP 之间切换——有时按请求切换,有时按会话切换——这样你的流量就不会看起来像同一个客户端一直猛刷同一台服务器。
Puppeteer 尤其需要这一点,因为无头 Chrome 用单个 IP 连续发出几百个请求,正是反爬系统最容易识别的模式。Cloudflare 官方文档 也提到,它会同时运行多层检测——规则判断、JavaScript 指纹检查、机器学习模型以及行为异常检测。轮换 IP 只能解决其中一层问题。只是其中一层。
常见代理类型有三种,不能混在一起看:
- 数据中心代理——便宜、速度快,来自云服务或机房。目标站点很容易识别,因为 ASN(网络段)一看就是数据中心,不像住宅网络。
- 住宅代理——通过真实家庭宽带 ISP 转发,看起来更像普通家庭上网。速度更慢、价格更高,但更不容易被怀疑。
- 移动代理——来自运营商网络,通常最贵,但当你确实需要“移动网络身份”时很有用。
只看 ASN 分类的话,住宅出口通常比数据中心出口更不显眼,但这两类都不是免封的。没有什么通用的“检测率”:最终效果取决于目标站点、出口信誉、地理位置、会话历史、浏览器配置文件和请求行为。
还有一个很容易混淆的区别:自己维护的静态代理列表(你负责管理代理池、选择下一个 IP、处理失败)和 backconnect/网关代理(你只访问一个入口,代理商在后台自动切换出口)并不是一回事。两者都能用,只是复杂度落在不同地方。
为什么要在 Puppeteer 里设置轮换代理?常见应用场景
老实说,你可能一直到真的需要轮换时才会开始用它,而一旦需要,就会非常需要。
| 使用场景 | 为什么需要轮换 |
|---|---|
| 商品目录价格监控 | 同一 IP 反复请求目录页,容易积累限流和信誉信号 |
| 线索补全 / 联系人数据采集 | 同一 IP 反复访问个人资料,行为更像爬虫而不是浏览,容易被行为引擎标记 |
| SERP 抓取 | 搜索引擎对基于 IP 的限流和验证码拦截最严格 |
| 竞品情报分析 | 连续多天反复抓同一域名,会形成与你的 IP 和 Cookie 历史绑定的指纹 |
| 内容聚合 | 单页价值低、页面量大——这正是反爬最敏感的流量形态 |
不存在那种“Amazon 在第 51 次请求时就封你”的固定数字。网站不会公开统一阈值,而且控制策略会随着端点、账号状态、ASN 信誉和流量形态而变化。最稳妥的做法是先从最低、且经过授权的请求频率开始,同时校验内容和状态码;只有在实际行为与目标站点政策都支持时,再逐步增加轮换。
Puppeteer 的三种代理轮换策略:你到底该用哪一种?

这部分通常是很多教程直接跳过,或者只给你看最粗糙的版本。实际上,这里有三种粒度,选错了要么白白浪费时间,要么把简单任务搞得过于复杂。
| 轮换策略 | 粒度 | 需要重启浏览器吗? | 复杂度 | 最适合 |
|---|---|---|---|---|
按浏览器轮换(--proxy-server) | 每个浏览器实例 1 个代理 | 需要 | 低 | 简单、低频率抓取 |
网关托管轮换(proxy-chain + backconnect 入口) | 由服务商的会话策略决定 | 不需要 | 中 | 带认证的轮换网关 |
| 浏览器分片或外部中继 | 每个浏览器分片或中继规则 1 个代理 | 不存在单进程内切换 | 高 | 需要可控并发和细粒度路由 |
在你做选择之前,先提醒一句:Puppeteer 官方网络拦截文档 明确说明,setRequestInterception 并不是一个可以“按请求无缝切换代理”的开关——每个被拦截的请求都会暂停,直到你显式继续、响应或中止它。真正的按请求代理路由,通常意味着通过本地可编程网关(例如 proxy-chain)转发请求,而不是在拦截处理器里直接改代理。方法 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
从代理服务商拿到代理列表(如果不是随便测试,优先选住宅代理),或者至少准备几条测试代理,先验证代码能不能跑通,再把真实请求量放上去。凭据要放到环境变量里——不要硬编码,也不要把它们直接写进最终会出现在日志里的 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() 会在后台悄悄启用请求拦截,这是 Puppeteer 文档里明确写到的——这会带来一点性能开销,提前知道总比你之后排查“为什么感觉变慢了”要好。
预期结果: 每次调用都会启动一个绑定到不同代理的新浏览器。想轮换就只能关闭再重启——这部分启动开销绕不过去。对于抓 50 个页面来说,光浏览器启动时间就会明显比另外两种方法慢。
适用场景: 低并发脚本、一次性抓取,或者你更看重调试简单性而不是速度的情况。
方法 2:用 proxy-chain 连接带认证的轮换网关
Chrome 不能直接接受 user:pass@host 这种把凭据嵌在代理 URL 里的写法。proxy-chain(由 Apify 维护)通过启动一个本地匿名代理来解决认证问题,这个本地代理再转发到你的上游认证代理。如果上游是服务商提供的轮换或 backconnect 网关,那么服务商会根据会话策略在这个入口后面自动切换出口 IP。proxy-chain 本身不会给现有的 Puppeteer 页面逐个分配不同代理。
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 不是装饰用的——本地代理服务如果没关,会漏端口。我还遇到过爬虫跑了一整晚,结果把可用文件描述符耗光的情况,原因就是匿名代理没有关闭。proxy-chain 还能返回具体的错误码(例如 593 表示 DNS 问题、594 表示连接被拒绝、597 表示认证失败),这对故障分类非常有用——后面还会提到。
适用场景: 带认证的住宅/数据中心网关,且轮换由服务商入口或会话参数控制。如果你需要多个固定代理身份并发使用,就应该用多个浏览器进程(浏览器分片)或者专门的外部中继;原生 Puppeteer 并没有支持按页面设置代理的接口。
方法 3:按请求路由必须依赖外部中继
这是粒度最高的一种方式——理论上,页面上的每个图片、脚本和 API 请求都可以走不同出口。实际操作里,这也是最脆弱、文档最少的一种,因为 Puppeteer 的请求拦截本意是筛选和修改请求,而不是按请求更换网络传输路径。
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();
}
说实话: Puppeteer 里并没有通过 setRequestInterception() 来真正按请求切换 IP 的能力。如果你真的需要这个粒度,就应该把 Chrome 通过一个可编程的外部中继转发,或者直接用基于代理会话构建的爬取框架。对大多数项目来说,每个浏览器分片用一个代理,或者用服务商托管的轮换网关,更容易运维,也更容易审计。
完整的反检测栈:只靠轮换代理并不能保证不被封
一个很常见的抱怨是:“我都用了代理,还是被封。”IP 只是现代反爬系统会评估的多个信号之一;如果只轮换 IP,其他特征却始终不一致,反而可能制造出更明显的异常。比如浏览器声称自己是 Windows Chrome,但客户端提示、时区或语言环境却对不上,这就很可疑。
第一层:轮换住宅代理
前面已经讲过——住宅出口通常比数据中心出口更合理,但没有统一的最小代理池规模。代理池大小应该根据实际请求量、会话时长、冷却时间和服务商复用策略来定,而不是随便拍一个数字。
第二层:用 Stealth 插件隐藏无头 Chrome 特征
puppeteer-extra-plugin-stealth 会修补一批常见的无头痕迹:navigator.webdriver、WebGL vendor 字符串、缺失的 Chrome runtime 对象,以及一些其他 CDP 泄漏。它确实很实用,但 项目自己的 README 也说得很直接:这是一场猫鼠游戏,想做到完全不被识破,大概率不现实。把它当作基础层,而不是保险承诺。
第三层:稳定的浏览器配置文件和合理的请求节奏
User-Agent 字符串必须和浏览器报告的其他信息彼此一致。Chrome 的 User-Agent Client Hints 会暴露结构化的平台数据,所以你手写的 user-agent 可能和真实平台互相矛盾。最好直接使用随附 Chrome 版本提供的 user-agent,在同一会话里保持 viewport、语言和时区稳定,并且控制请求节奏,不要每个页面都伪造一套新指纹。
下面是把这三层放在一起的一套启动配置:
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);
}
}
这就是很多竞品教程不会展示的部分——代理、隐身和指纹随机化放在同一处,直接可复制后再按需改造。
适合生产环境的错误处理与代理健康检查

很多教程一旦“能跑通”就结束了。真实抓取远没有这么顺利——代理会挂、凭据会过期、目标站点会在跑到一半时限流,而这些都不是靠“但愿如此”就能解决的。
带指数退避和随机抖动的重试逻辑
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)); // 抖动可避免惊群
}
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 | 代理太慢或目标站点在阻断 | 增加超时时间;换住宅代理 |
403 Forbidden | IP 或指纹被标记 | 更换代理 + 开启 stealth + 随机化 UA |
ERR_TUNNEL_CONNECTION_FAILED | HTTPS 隧道异常 | 检查是否支持 CONNECT 方法;尝试使用 proxy-chain 本地隧道 |
还有几点值得单独说明,没法整整齐齐塞进表格里:200 状态码并不等于成功。很多软封锁会返回一个完整的 HTML 页面——比如登录墙或验证页——但状态码还是正常,所以你要验证的是实际内容,而不只是响应状态。卡住的时候,Puppeteer 的调试指南 建议把 headless 设为 false,加上 slowMo,并设置 NODE_DEBUG="puppeteer:*" 来获取更详细的协议日志——只是要注意,这些日志可能包含敏感请求数据,所以不要在生产凭据环境里长期开启。
自己管理代理轮换 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 时为 $6/GB、1 TB 时为 $2.50/GB;Decodo(原 Smartproxy)则显示 3 GB 时为 $3.75/GB、100 GB 时为 $2.75/GB,并提供 $4/GB 的按量付费方案。Decodo 还宣传有 1.15 亿+ IP 池和 99.92% 成功率——这是厂商声明,不是独立复现的测试结果。
我实际用的决策逻辑是:你需要和页面交互吗?比如点击、滚动、填写表单、保持登录会话?那就用 Puppeteer 加代理。你只是要页面上已经存在的数据?那在自己搭一整套要长期维护的代理基础设施之前,先看看提取 API。
当 Puppeteer + 代理过于复杂时:直接用 API 提取结构化数据
有那么一次,大概是我第三次为一个“只需要把商品价格放进表格”的项目重建代理健康检查系统时,我突然意识到:这些基础设施解决的是“从页面拿到原始 HTML”这个问题——而这其实并不是开发者真正的目标。真正的目标是结构化数据。HTML 只是中间那个麻烦格式。
Thunderbit 的 Open API 把提取当成核心操作,而不是浏览器自动化的副作用。POST /extract 接收一个 URL 和 JSON Schema,并返回匹配的结构化数据——在后端处理 JS 渲染、反爬措施和验证码,而不是让你自己去接 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"]
}
}'
如果你只想要干净的 Markdown,而不是严格 schema,也可以用 POST /distill。另外还支持批量提取:一次调用就能把同一套 schema 跑到多个 URL 上。根据 Thunderbit 当前 API 定价,Distill 每页 1 个单位,Extract 每页 20 个单位;免费层包含 600 次一次性单位,足够你在正式投入前先把流程验证一遍。
对于在 Claude、Cursor 或其他兼容 MCP 的客户端里工作的开发者,Thunderbit 还提供 thunderbit_extract 和 thunderbit_distill 这两个 MCP 工具,让代理在任务进行中自己判断什么时候需要从页面拉数据,而不用单独写一套抓取步骤。我会先看最新的 API 参考文档,再去配置 MCP,因为工具名称和参数会随着文档版本变化。
| 维度 | Puppeteer + 轮换代理 | Thunderbit API |
|---|---|---|
| 配置复杂度 | 高——代理池、轮换逻辑、隐身、重试都要自己做 | 低——一个带 JSON Schema 的 API 调用即可 |
| 反爬处理 | 手动实现 | 内置 |
| 输出 | 原始 HTML(需要解析) | 与 schema 匹配的结构化 JSON |
| 维护成本 | 高——选择器会失效,代理会老化 | 低 |
| 最适合 | 自定义自动化、登录流程、小众交互 | 大规模数据提取 |
也得公平地说一句 DIY 路线:如果你的场景涉及登录账号、点击多步流程,或者需要在一个会话里持续保持状态,那提取 API 通常替代不了它——Thunderbit 自己的 FAQ 也明确说明,目前 API 还不支持交互式登录流程。这种情况下,Puppeteer 加代理依然更合适。但如果你的任务只是“把一堆公开网页里的数据抓出来,放进我定义好的 schema”,那自己搭代理轮换栈,实际上是在解决一个比你真正需求更难的问题。对于想完全跳过代码的团队,Thunderbit Chrome Extension 提供了同样的 AI 驱动提取能力,而且是点选式界面——如果你在比较 无代码网页抓取 和完整开发方案,这个值得看看。
结论与要点总结
在 Puppeteer 里设置轮换代理,并不是一种单一技巧。按浏览器轮换最简单,也最隔离。带认证的 backconnect 网关可以在一个浏览器级入口后自动切换出口。更细粒度的按请求或并发身份,则需要浏览器分片或外部中继;单靠请求拦截并不能改变 Chrome 的网络路由。
不过,如果没有其他配套,这些都意义不大。代理解决的是 IP 信誉问题;stealth 插件和指纹一致性解决的是浏览器信号问题;抖动和节奏控制解决的是行为问题。少了任何一层,你都还是会被封,只是原因不同。
如果你准备自己动手,先从 proxy-chain 仓库 和上面的代码块开始——它们比大多数付费课程更能帮到你。如果你更想跳过代理管理,直接拿到结构化数据,那么在花一个周末搭健康检查基础设施之前,先花十分钟看看 Thunderbit 的 API 文档 值不值得。两条路都合理——关键是你要解决自己真正遇到的问题,而不是教程默认假设你遇到的问题。想更全面了解 AI 正在如何改变这个领域,可以继续阅读我们关于 AI 网页抓取 以及它与传统方法对比的深入分析。
常见问题解答
在 Puppeteer 里应该多久轮换一次代理?
这取决于目标站点的限流有多严格。对于反爬很强的网站,最好按页面或按会话轮换。对于限制较松的网站,按会话轮换,甚至整个抓取过程都用一个稳定 IP,也完全可以。没有统一数值——把 403、429 和超时当作你该更激进轮换的信号,而不是固定请求次数。
Puppeteer 抓取可以用免费代理吗?
技术上可以,但除了快速测试,我不建议。免费代理列表通常速度慢、不稳定,而且经常已经被你要抓的网站拉黑。只要是生产场景,付费服务商的住宅代理或托管网关都更值得。
puppeteer-extra-plugin-stealth 能对付所有反爬系统吗?
不能,插件文档 也明确这么说。它能减少一些常见的无头 Chrome 特征,但目标站点仍然可以检查网络信誉、TLS 特征、Cookie、客户端提示和行为模式。把它当成一层兼容性补丁,而不是万无一失的解决方案。
proxy-chain 和 Puppeteer 里的 --proxy-server 有什么区别?
--proxy-server 是 Chromium 原生启动参数,它会把一个代理端点分配给整个浏览器实例,而且 Chrome 不支持在那里直接嵌入代理凭据。proxy-chain 会先在本地创建一个匿名隧道,再转发到带认证的上游代理。真正的轮换来自重新启动并切换上游、使用服务商托管的 backconnect 网关,或者自己额外搭建的中继,而不是 proxy-chain 给单个 Puppeteer 页面分配代理。
轮换代理就足以彻底避免被封吗?
不够——这也是最常见的误解。像 Cloudflare 的 bot management 这样的现代反爬系统,会把 IP 信誉和浏览器指纹、行为模式以及会话历史一起关联分析。代理只解决 IP 信誉这一项;你仍然需要 stealth 配置、一致的指纹和合理的时间间隔,才能避免在其他信号上被标记。
了解更多


