跳到主要内容
NextProxyNextProxy文档

Playwright 与 Selenium

浏览器自动化框架里配置代理认证的正确方式,以及每上下文独立代理和轮换的做法。

浏览器自动化配代理有一个共同麻烦:Chromium 的 --proxy-server 启动参数不支持带凭据。写 --proxy-server=http://user:pass@host:port 会被忽略,然后浏览器弹认证框卡住。各框架有各自的绕法。

Playwright(Python)

Playwright 把凭据单独传,这是最干净的方案:

python
from playwright.sync_api import sync_playwright

PROXY = {
    "server": "http://GATEWAY_HOST:58971",
    "username": "USERNAME-country-US-session-job1-time-30",
    "password": "PASSWORD",
}

with sync_playwright() as playwright:
    browser = playwright.chromium.launch(headless=True)
    context = browser.new_context(proxy=PROXY)
    page = context.new_page()

    page.goto("https://api.ipify.org?format=json")
    print(page.inner_text("body"))

    context.close()
    browser.close()

每个上下文一个独立会话

python
from playwright.sync_api import sync_playwright

GATEWAY = "http://GATEWAY_HOST:58971"
BASE_USER = "USERNAME"
PASSWORD = "PASSWORD"


def proxy_for(session_id: str, country: str = "US", minutes: int = 30) -> dict:
    return {
        "server": GATEWAY,
        # session 值只允许 [A-Za-z0-9.-],别用下划线
        "username": f"{BASE_USER}-country-{country}-session-{session_id}-time-{minutes}",
        "password": PASSWORD,
    }


with sync_playwright() as playwright:
    browser = playwright.chromium.launch(headless=True)

    for index in range(4):
        context = browser.new_context(
            proxy=proxy_for(f"ctx.{index}"),
            locale="en-US",
            timezone_id="America/Los_Angeles",
            viewport={"width": 1440, "height": 900},
        )
        page = context.new_page()
        page.goto("https://api.ipify.org", wait_until="domcontentloaded")
        print(index, page.inner_text("body"))
        context.close()

    browser.close()

异步版本

python
import asyncio

from playwright.async_api import async_playwright


async def scrape(session_id: str, url: str) -> str:
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(headless=True)
        context = await browser.new_context(proxy={
            "server": "http://GATEWAY_HOST:58971",
            "username": f"USERNAME-country-US-session-{session_id}-time-30",
            "password": "PASSWORD",
        })
        page = await context.new_page()
        await page.goto(url, wait_until="domcontentloaded")
        text = await page.inner_text("body")
        await browser.close()
        return text


async def main() -> None:
    results = await asyncio.gather(*(
        scrape(f"job.{i}", "https://api.ipify.org") for i in range(4)
    ))
    for result in results:
        print(result)


asyncio.run(main())

Playwright(Node.js)

javascript
import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });

const context = await browser.newContext({
  proxy: {
    server: 'http://GATEWAY_HOST:58971',
    username: 'USERNAME-country-US-session-job1-time-30',
    password: 'PASSWORD',
  },
  locale: 'en-US',
  timezoneId: 'America/Los_Angeles',
});

const page = await context.newPage();
await page.goto('https://api.ipify.org?format=json');
console.log(await page.innerText('body'));

await browser.close();

Puppeteer

Puppeteer 没有 Playwright 那样的 proxy 对象,凭据要通过 page.authenticate() 给:

javascript
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
  // 启动参数只放 host:port,凭据不能写在这里
  args: ['--proxy-server=http://GATEWAY_HOST:58971'],
});

const page = await browser.newPage();

// 凭据必须在 goto 之前设置
await page.authenticate({
  username: 'USERNAME-country-US-session-job1-time-30',
  password: 'PASSWORD',
});

await page.goto('https://api.ipify.org?format=json');
console.log(await page.evaluate(() => document.body.innerText));

await browser.close();

Selenium(Python)

Selenium 4 配带认证的代理最麻碎。三种方案,从好到差:

方案一:Selenium Wire(推荐)

shell
pip install selenium-wire
python
from seleniumwire import webdriver

PROXY = "http://USERNAME-country-US-session-job1-time-30:PASSWORD@GATEWAY_HOST:58971"

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")

driver = webdriver.Chrome(
    options=options,
    seleniumwire_options={
        "proxy": {"http": PROXY, "https": PROXY, "no_proxy": "localhost,127.0.0.1"},
    },
)

try:
    driver.get("https://api.ipify.org?format=json")
    print(driver.find_element("tag name", "body").text)
finally:
    driver.quit()

方案二:用扩展注入凭据

不想引入 Selenium Wire 时,可以现场生成一个 Chrome 扩展:

python
import os
import tempfile
import zipfile

from selenium import webdriver

MANIFEST = """
{
  "version": "1.0.0",
  "manifest_version": 3,
  "name": "NextProxy Auth",
  "permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
  "host_permissions": ["<all_urls>"],
  "background": {"service_worker": "background.js"}
}
"""

BACKGROUND = """
chrome.proxy.settings.set({
  value: {
    mode: 'fixed_servers',
    rules: {
      singleProxy: { scheme: 'http', host: '%(host)s', port: %(port)d },
      bypassList: ['localhost', '127.0.0.1']
    }
  },
  scope: 'regular'
});

chrome.webRequest.onAuthRequired.addListener(
  () => ({ authCredentials: { username: '%(user)s', password: '%(password)s' } }),
  { urls: ['<all_urls>'] },
  ['blocking']
);
"""


def build_auth_extension(host: str, port: int, user: str, password: str) -> str:
    """生成一个把代理凭据带上的临时扩展,返回 crx 路径。"""
    path = os.path.join(tempfile.mkdtemp(), "np-auth.zip")
    with zipfile.ZipFile(path, "w") as archive:
        archive.writestr("manifest.json", MANIFEST)
        archive.writestr("background.js", BACKGROUND % {
            "host": host, "port": port, "user": user, "password": password,
        })
    return path


options = webdriver.ChromeOptions()
options.add_extension(build_auth_extension(
    "GATEWAY_HOST", 58971,
    "USERNAME-country-US-session-job1-time-30", "PASSWORD",
))

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://api.ipify.org?format=json")
    print(driver.find_element("tag name", "body").text)
finally:
    driver.quit()

方案三:用免密端口(最省事)

如果你的出口 IP 固定,提取接口 拿到的端口不需要凭据,Selenium 直接配就行:

python
import requests
from selenium import webdriver


def fetch_one_port(api_key: str) -> str:
    response = requests.get(
        "https://api.example.com/api/v1/proxy/extract",
        params={"apikey": api_key, "num": 1, "type": "json",
                "country": "US", "session": "sticky", "time": 30},
        timeout=15,
    )
    response.raise_for_status()
    item = response.json()["data"][0]
    return f"{item['ip']}:{item['port']}"


options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
# 免密端口,没有凭据问题
options.add_argument(f"--proxy-server=http://{fetch_one_port('YOUR_API_KEY')}")

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://api.ipify.org?format=json")
    print(driver.find_element("tag name", "body").text)
finally:
    driver.quit()

Selenium(Java)

java
import java.util.HashMap;
import java.util.Map;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

public class SeleniumProxyExample {

    public static void main(String[] args) {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless=new");
        // 只放 host:port;Chromium 不接受启动参数里的凭据
        options.addArguments("--proxy-server=http://GATEWAY_HOST:58971");

        ChromeDriver driver = new ChromeDriver(options);
        try {
            // 用 CDP 提供凭据,避免弹认证框
            Map<String, Object> headers = new HashMap<>();
            String credentials = java.util.Base64.getEncoder().encodeToString(
                    "USERNAME-country-US:PASSWORD".getBytes());
            headers.put("Proxy-Authorization", "Basic " + credentials);

            driver.executeCdpCommand("Network.enable", new HashMap<>());
            driver.executeCdpCommand("Network.setExtraHTTPHeaders",
                    Map.of("headers", headers));

            driver.get("https://api.ipify.org?format=json");
            System.out.println(driver.findElement(
                    org.openqa.selenium.By.tagName("body")).getText());
        } finally {
            driver.quit();
        }
    }
}

资源占用与并发

浏览器自动化的流量比纯 HTTP 请求大一个量级——一个现代网页首屏动辄几 MB。用动态住宅按 GB 计费时这一点很要紧。

减少流量的常用做法:

python
# Playwright:拦掉图片、字体、媒体
def block_heavy_resources(route):
    if route.request.resource_type in {"image", "media", "font"}:
        route.abort()
    else:
        route.continue_()

context.route("**/*", block_heavy_resources)

这一步通常能省掉 60–80% 的流量。

排错清单

现象原因
浏览器弹认证对话框凭据写进了 --proxy-server,Chromium 会忽略
无头模式下扩展方案失效Chrome 无头模式不完整支持扩展,改用 Selenium Wire
页面加载超时网关空闲超时 5 分钟,但浏览器可能等更久;显式设 page.set_default_timeout()
出口 IP 和浏览器时区矛盾被风控同步设置 locale / timezone_id
所有页面 IP 都一样Puppeteer 的代理是浏览器级的,用 Playwright 的 per-context
400 / 402 / 403 / 407 / 429 / 5xx错误码对照

这篇解决你的问题了吗?