Skip to content
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`);
  }
}

有序流程:

  1. 运行一个未配置代理的一次性脚本,并将结果记录为 DIRECT_IP。
  2. 再次运行带代理的脚本。
  3. 在进行任何真实抓取之前调用 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 上运行而不耗尽资源

  1. 在流水线中使用 npx playwright install --with-deps chromium 安装浏览器依赖。
  2. 在 CI runner 上,每个 vCPU 保持两到三个上下文的并发;每个上下文都承载一个已渲染页面。
  3. 每个进程只启动一次浏览器,并在 finally 块中关闭上下文。
  4. 对于定时任务中的高流量、低防御目标,优先使用数据中心代理;将住宅或移动端点留给更难的目标。
  5. 关注单位经济性:按量计费使突发性 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* 失败归类为端点问题,丢弃该端点,并让运行继续。