Advanced
/
4 min read
使用 Playwright 进行 ProxyScrape 网页抓取:通过 HTTP 和 SOCKS5 代理路由无头 Chromium
配置 Playwright,使无头 Chromium 通过 ProxyScrape 出口,在抓取前验证出口 IP,按浏览器上下文轮换 HTTP 或 SOCKS5 端点,并正确分类代理错误。
Linux
HTTP(S)
网页抓取
概述
无头浏览器将代理配置放在浏览器内部,而不是你的 HTTP 库中。这改变了设置方式:你在启动浏览器或创建浏览器上下文时配置代理,必须处理代理认证,而按上下文配置使得无需为每个请求重启 Chromium 即可实现轮换。
本教程使用 Playwright、Chromium 和 ProxyScrape 端点(HTTP 或 SOCKS5)。涵盖单端点启动、出口 IP 检查、跨多个端点的按上下文轮换、代理错误处理,以及在 Linux CI 上运行而不耗尽内存。
何时适合使用无头浏览器
- 目标在客户端渲染内容,因此普通 HTTP 请求只会返回空壳。
- 你需要交互:填写表单、点击、滚动或等待网络空闲。
- 你需要浏览器级别的行为,例如 cookies、
localStorage或 service workers。 - 你要验证站点在特定区域的行为。
当页面是服务端渲染时,请坚持使用普通 HTTP 客户端;它便宜得多,也快得多。
先决条件
- Node.js 18 或更新版本以及 npm。
- 已安装 Playwright 和 Chromium。
- ProxyScrape 凭据,以及
http和/或socks5的主机和端口。住宅、数据中心和移动代理在这里都可用;选择与目标防御相匹配的类型。 - Linux、macOS 或 Windows。CI 注意事项假定使用 Debian 或 Ubuntu runner。
步骤
步骤 1 — 安装 Playwright 和 Chromium
mkdir proxyscrape-playwright
cd proxyscrape-playwright
npm init -y
npm install playwright
npx playwright install --with-deps chromium
--with-deps 会安装 Chromium 在 Linux 镜像和 CI runner 上所需的操作系统库。
步骤 2 — 决定代理应用于浏览器还是上下文
| 范围 | 设置位置 | 用于 | 注意事项 |
|---|---|---|---|
| 浏览器 | chromium.launch({ proxy }) |
整个运行使用单个端点 | 每个上下文共享同一个出口 IP |
| 上下文 | browser.newContext({ proxy }) |
轮换:每个上下文一个端点,并行运行 | 引擎支持不同;Chromium 是安全默认值——在 Firefox 或 WebKit 中依赖它之前,请确认你的 Playwright 版本支持按上下文代理 |
两条实用规则:
- 将凭据作为单独的
username和password字段传递。Chromium 会忽略嵌入代理服务器 URL 中的凭据。 server值需要包含协议:ProxyScrape 的 HTTP 端点使用http://host:port,SOCKS5 使用socks5://host:port。
步骤 3 — 通过一个端点启动并检查出口 IP
// launch-proxy.mjs
import { chromium } from 'playwright';
const proxy = {
server: process.env.PROXYSCRAPE_SERVER, // http://HOST:PORT or socks5://HOST:PORT
username: process.env.PROXYSCRAPE_USERNAME,
password: process.env.PROXYSCRAPE_PASSWORD,
};
const browser = await chromium.launch({ proxy, headless: true });
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
console.log('exit ip:', await page.textContent('body'));
} finally {
await browser.close();
}
export PROXYSCRAPE_SERVER="http://host-from-your-plan:port-from-your-plan"
export PROXYSCRAPE_USERNAME="your-username"
export PROXYSCRAPE_PASSWORD="your-password"
node launch-proxy.mjs
如果这打印出一个 IP,则代理链路端到端可用。如果卡住或抛出异常,请先跳到故障排除表,再编写更多代码。
步骤 4 — 当流量未被代理时快速失败
静默的错误配置——例如服务器字符串中的拼写错误——可能会让你从自己的 IP 抓取。先捕获一次你的直连 IP,然后据此进行断言。
// exit-ip.mjs
export async function getExitIp(page) {
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
const body = await page.textContent('body');
return JSON.parse(body).ip;
}
export function assertProxied(exitIp, directIp) {
if (directIp && exitIp === directIp) {
throw new Error(`Traffic was not proxied: exit IP ${exitIp} equals the direct IP`);
}
}
有序流程:
- 运行一个未配置代理的一次性脚本,并将结果记录为
DIRECT_IP。 - 再次运行带代理的脚本。
- 在进行任何真实抓取之前调用
assertProxied(exitIp, process.env.DIRECT_IP),并在断言触发时使运行失败。
步骤 5 — 跨浏览器上下文轮换端点
一个浏览器,多个上下文,每个上下文一个端点。
// rotate-contexts.mjs
import { chromium } from 'playwright';
const ENDPOINTS = (process.env.PROXYSCRAPE_ENDPOINTS ?? '')
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const [scheme, host, port, username, password] = entry.split('|');
return { server: `${scheme}://${host}:${port}`, username, password };
});
if (ENDPOINTS.length === 0) {
throw new Error('Set PROXYSCRAPE_ENDPOINTS to http|HOST|PORT|USER|PASS,socks5|HOST|PORT|USER|PASS');
}
const TARGETS = (process.env.TARGETS ?? 'https://example.com,https://example.org').split(',');
const CONCURRENCY = Number(process.env.CONCURRENCY ?? 3);
const USER_AGENT =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
function isProxyError(error) {
const message = String(error && error.message ? error.message : '');
return /ERR_PROXY|ERR_TUNNEL|ERR_PROXY_AUTH/i.test(message);
}
async function runBatch(browser, proxy, urls) {
const context = await browser.newContext({ proxy, userAgent: USER_AGENT });
context.setDefaultTimeout(30000);
const results = [];
try {
const page = await context.newPage();
for (const url of urls) {
try {
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
results.push({ url, endpoint: proxy.server, status: response ? response.status() : null });
} catch (error) {
results.push({
url,
endpoint: proxy.server,
error: isProxyError(error) ? `proxy: ${error.message}` : error.message,
});
}
}
} finally {
await context.close();
}
return results;
}
const browser = await chromium.launch({ headless: true });
try {
const queue = ENDPOINTS.map((proxy, index) => ({
proxy,
urls: TARGETS.filter((_, targetIndex) => targetIndex % ENDPOINTS.length === index),
}));
const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
const collected = [];
while (queue.length > 0) {
const job = queue.shift();
collected.push(...(await runBatch(browser, job.proxy, job.urls)));
}
return collected;
});
const results = (await Promise.all(workers)).flat();
for (const result of results) {
console.log(JSON.stringify(result));
}
} finally {
await browser.close();
}
export PROXYSCRAPE_ENDPOINTS="http|HOST|PORT|USER|PASS,socks5|HOST|PORT|USER|PASS"
export TARGETS="https://example.com,https://example.org,https://example.net"
export CONCURRENCY=3
node rotate-contexts.mjs
为什么这种结构有效:
- 浏览器只启动一次;上下文开销低,且每个上下文携带自己的端点。
- 工作池限制了同时存在的上下文数量,这是 Playwright 中主要的内存控制手段。
- 每个结果都记录服务它的端点,因此无需猜测即可归因失败。
步骤 6 — 将代理失败视为端点失败,而非页面失败
net::ERR_PROXY_CONNECTION_FAILED— 代理主机或端口错误,或不可达。net::ERR_TUNNEL_CONNECTION_FAILED— 到 HTTPS 目标的 CONNECT 隧道被拒绝;确认你配置的协议与端点匹配。net::ERR_PROXY_AUTH_REQUESTED— 凭据被拒绝;重新检查用户名和密码,并将它们作为字段传递,而不是放在服务器 URL 内。TimeoutError— 端点慢或目标较重;提高超时并降低并发。
当出现其中一种错误时,在本次运行的剩余时间将该端点从队列中移除,并继续使用其他端点。反复重试同一个损坏的端点只会浪费时间。
步骤 7 — 在 Linux CI 上运行而不耗尽资源
- 在流水线中使用
npx playwright install --with-deps chromium安装浏览器依赖。 - 在 CI runner 上,每个 vCPU 保持两到三个上下文的并发;每个上下文都承载一个已渲染页面。
- 每个进程只启动一次浏览器,并在
finally块中关闭上下文。 - 对于定时任务中的高流量、低防御目标,优先使用数据中心代理;将住宅或移动端点留给更难的目标。
- 关注单位经济性:按量计费使突发性 CI 运行易于启动,而固定月度套餐对于固定的夜间任务通常更便宜。
故障排除
| 症状 | 可能原因 | 修复 |
|---|---|---|
| 页面加载,但来自你自己的 IP | 代理被忽略(server 中有拼写错误,或该选项未传递给上下文) |
使用步骤 4 的辅助函数记录出口 IP,并在其与直连 IP 匹配时抛出错误 |
ERR_PROXY_CONNECTION_FAILED |
主机或端口错误,或 CI 出口被阻止 | 用 curl 测试同一端点,然后检查网络策略 |
ERR_PROXY_AUTH_REQUESTED |
凭据错误,或嵌入 URL 中 | 传递 username 和 password 字段,绝不要使用 user:pass@host |
HTTPS 上的 ERR_TUNNEL_CONNECTION_FAILED |
端点与你配置的协议不匹配 | 先试 HTTP 端点,再试 SOCKS5 端点,保留可用的那个 |
| 每个上下文报告相同的出口 IP | 使用了浏览器级代理而非上下文级代理,或引擎不支持按上下文代理 | 使用 chromium.launch(),并在每个 newContext() 上设置 proxy |
| 运行导致 CI runner 被杀 | 并发上下文过多 | 将 CONCURRENCY 降到 2,并重新测量内存 |
总结
- 在 Playwright 中,代理属于启动调用或浏览器上下文,而不属于单个请求。
- 抓取前验证出口 IP;配置错误的代理可能会让你静默地留在自己的 IP 上。
- 通过给每个上下文自己的 ProxyScrape HTTP 或 SOCKS5 端点,并限制同时运行的上下文数量来进行轮换。
- 将
ERR_PROXY*和ERR_TUNNEL*失败归类为端点问题,丢弃该端点,并让运行继续。