Puppeteer 驱动的是一个真正的 Chromium——当一个目标用 JavaScript 渲染内容、把数据藏在交互之后、或对任何不是真浏览器的东西做指纹时,这正是你想要的。接入一个住宅代理只是两行,但 Puppeteer 把这活儿拆到两个不同的地方,其方式几乎让每个人第一次都栽跟头:代理地址放进启动参数,而凭据则完全放到另一处。
这是与用 Playwright 使用住宅代理并列的 Puppeteer 篇;如果你想要的是朴素的 HTTP 客户端、而不是一个完整浏览器,见Node.js 里的代理。这里我们专注于浏览器特有的那些坑。
下面的一切都使用 Shifter 的住宅 gateway:一个端点 p.shifter.io:443,所有定位都编码在用户名里。换成别家供应商,就换掉主机和凭据;形态是一样的。
一段话讲清 gateway 模型
代理用户名同时承载你的认证和你的定位。你不是靠切换端点来换国家或换会话,而是靠改用户名字符串:
customer-USERNAME-country-us-sid-abc123-ttl-600country-us 定位美国,sid 固定一个粘性会话,ttl 把那个 IP 保持 N 秒。去掉 sid/ttl,每一条新连接都会轮换。密码是恒定的。在 Puppeteer 里,主机放进一个启动 flag,而那个用户名放进 page.authenticate。
搭建:代理放启动参数,凭据放 page.authenticate
Chromium 把代理服务器当作一个命令行 flag——--proxy-server——经由 Puppeteer 的 args 传入。它不会在那里接受 user:pass@host,Chromium 不从那个 flag 读凭据。取而代之,你按页面用 page.authenticate 提供凭据,它会用正确的 Proxy-Authorization 回应代理的 407 挑战。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true, args: ['--proxy-server=http://p.shifter.io:443'], // 只有主机,没有凭据});
const page = await browser.newPage();await page.authenticate({ username: `${process.env.SHIFTER_USER}-country-us`, // 定位就住在这里 password: process.env.SHIFTER_PASS,});
await page.goto('https://api.ipify.org');console.log(await page.evaluate(() => document.body.innerText)); // 一个美国住宅 IPawait browser.close();有两件事要内化。用户名里包含了定位标志(-country-us),因为地理住在那里、而不是在 URL 里。而对一个需认证的代理,page.authenticate 不是可选项——没有它,每一次导航都死在一个 407 上。这个拆分——主机在 flag 里、凭据在 page.authenticate 里——正是最常见的 Puppeteer 代理错误。
经一个 gateway 轮换地理与会话
这就是那个设计带来的有用后果。代理主机在浏览器启动时被固定、你无法按页面改它,但你并不需要。因为定位住在用户名里、而 page.authenticate 是按页面设置的,给每个页面一个不同的用户名,就会把它经同一个 gateway 上的不同身份路由出去。一个浏览器、许多地理、无需重启。
async function pageFor(browser, country, sid) { const page = await browser.newPage(); const user = `${process.env.SHIFTER_USER}-country-${country}` + (sid ? `-sid-${sid}-ttl-600` : ''); await page.authenticate({ username: user, password: process.env.SHIFTER_PASS }); return page;}
const de = await pageFor(browser, 'de', 'job-42'); // 德国粘性会话const us = await pageFor(browser, 'us', null); // 轮换的美国给每个逻辑工作单元它自己的 sid,并在单元之间轮换、而不是在会话中途轮换(粘性 vs 轮换讲了这个区分),并按负载均衡那篇所述把工作映射到身份。要在身份之间隔离 cookie 和存储,就把每个身份放进它自己的浏览器上下文:
const context = await browser.createBrowserContext(); // 隔离的 cookie/存储const page = await context.newPage();await page.authenticate({ username: userFor('gb'), password: pass });注意名字:较新的 Puppeteer 把它叫作 createBrowserContext(),旧版本叫它 createIncognitoBrowserContext()。同一个思路,改了名。
坑 1:复用浏览器,绝不每个请求启一个
启动 Chromium 很重——每个请求都开一个新鲜浏览器,每次都要付几百毫秒的启动加上真实内存,正是延迟指南存在的意义所要消除的那部分开销。在启动时开一个浏览器并复用它,为并发工作开页面或上下文、干完就关掉它们。一个长生命周期浏览器之下的页面池,是正确的形态。
坑 2:拦掉你不需要的资源
一个浏览器会像真浏览器那样把一切都抓下来:图片、字体、媒体、样式表、分析脚本。如果你只想要 HTML 或几个字段,那就是你在付的带宽、和你在等的时间。Puppeteer 让你拦截请求、并中止你不需要的那些,这会大幅削减你的页面加载时间和你的带宽账单。
await page.setRequestInterception(true);page.on('request', (req) => { const blocked = ['image', 'font', 'media', 'stylesheet']; if (blocked.includes(req.resourceType())) req.abort(); else req.continue();});要有选择:有些站点没有它们的 CSS 或某个特定脚本就渲染不出你想要的内容,所以先激进地拦,然后确认数据仍然出现。当它出现时,这是一个 headless 浏览器里能拿到的最便宜的提速。
坑 3:浏览器只是”不被封”的一半
一个住宅 IP 处理了”看起来像人”的网络那一半,但 Puppeteer 驱动的仍是 headless 的 Chromium,而站点也会对浏览器做指纹——navigator.webdriver、headless 特有的怪癖、以及自动化信号。一个有良好信誉的干净 IP 能让你避开很多挑战,但它并不能给一个明显自动化的浏览器打掩护。用一个当前版本的 Puppeteer 和 Chromium,好让你拿到现代的 headless 模式、而不是那个老旧、易被检测的模式;把视口和 user-agent 保持真实;并以人的节奏驱动页面。那些触发检测的错误对浏览器这一层和对 IP 这一层同样适用,而这两者必须对得上。
坑 4:给你的并发封顶
每个打开的页面都是一个占着真实内存的真实浏览器标签页,所以你不能像发 HTTP 请求那样开成千上万个。保持一个有界的页面或上下文池并复用它们,并按目标主机给在途工作封顶,好让脆弱的站点不被猛击、宽松的站点也不被饿死。越过一个目标的容忍度之后,更多并行买来的是封锁和内存溢出崩溃,而不是吞吐量(如何避免被封)。
验证你确实在走代理
在给别的任何东西做基准或调试之前,从页面内部确认出口 IP:
await page.goto('http://ip-api.com/json');console.log(await page.evaluate(() => document.body.innerText)); // 期望是目标国家返回你自己的 IP,说明 --proxy-server flag 没生效。卡在一个 407 对话框上,说明 page.authenticate 缺失、或凭据不对。整体挂住,说明本地出站被挡了。这三种都在超时诊断指南里有讲。
常见问题
为什么把 user:pass@host 放进 --proxy-server 不管用?
Chromium 不从 --proxy-server flag 里读代理凭据。那里只传主机,并用 page.authenticate({ username, password }) 提供凭据,它会处理代理的 407 挑战。因为 gateway 把定位编码在用户名里,那个完整用户名(带 -country-...)要进 page.authenticate。
如果代理是在启动时固定的,我怎么按页面用不同国家?
你不改代理主机,你改 page.authenticate 的用户名。因为定位住在用户名里、而 gateway 主机是恒定的,每个页面都能用不同的用户名认证、并经不同的身份出口。一个浏览器服务许多地理。
我能不重启浏览器就轮换代理吗?
能——对身份和地理而言,靠按页面或按上下文变换 page.authenticate 的用户名。你只有在需要一个真正不同的代理主机时才需要重启,而用一个单一的 gateway 端点你并不需要。
在 Puppeteer 里我怎么削减带宽? 开启请求拦截,并中止你不需要的资源类型(图片、字体、媒体,常常还有样式表)。确认目标仍然渲染出你想要的数据,然后保留这些拦截。它是一个 headless 浏览器里对速度和成本最大的那个单一杠杆。
Puppeteer 还是 Playwright?
两者都驱动真浏览器、也都能和住宅代理配合得很好。Playwright 在它的上下文选项里直接接收代理(连同凭据);Puppeteer 把它拆成启动 flag 加 page.authenticate。按生态契合度和你已有的代码来选;代理的概念是一样的。
底线
一旦那个拆分想通了,Puppeteer 加住宅代理就很直接:代理主机在启动时进 --proxy-server,而把你的定位承载在用户名里的凭据,按页面进 page.authenticate。靠变换那个用户名来轮换地理与会话、而不是重启,用浏览器上下文隔离身份,复用一个长生命周期的浏览器,拦掉你不需要的资源,并记住浏览器指纹必须和 IP 一样看起来像人。
把这些做对,Puppeteer 就能搞定那些朴素 HTTP 客户端搞不定的、重 JavaScript 的目标。把它指向住宅 gateway,并记住池的质量决定了你到底会不会频繁被挑战(IP 信誉)。定价页面有按 GB 计费的套餐,可以拿它对着你自己的目标试用。