Веб-скрапинг ProxyScrape с Playwright: маршрутизация Headless Chromium через HTTP- и SOCKS5-прокси
Настройте Playwright так, чтобы headless Chromium выходил через ProxyScrape, проверяйте exit IP перед скрапингом, ротируйте HTTP- или SOCKS5-эндпоинты для каждого контекста браузера и правильно классифицируйте ошибки прокси.
Обзор
Headless-браузеры помещают конфигурацию прокси внутрь браузера, а не внутрь вашей HTTP-библиотеки. Это меняет настройку: вы конфигурируете прокси при запуске браузера или создании контекста браузера, вам приходится обрабатывать аутентификацию прокси, а конфигурация для каждого контекста — это то, что делает возможной ротацию без перезапуска Chromium для каждого запроса.
В этом руководстве используется Playwright с Chromium и эндпоинтами ProxyScrape (HTTP или SOCKS5). Оно охватывает запуск с одним эндпоинтом, проверку exit IP, ротацию для каждого контекста по нескольким эндпоинтам, обработку ошибок прокси и запуск в Linux CI без исчерпания памяти.
Когда headless-браузер — подходящий инструмент
- Целевой сайт рендерит контент на стороне клиента, поэтому обычный HTTP-запрос возвращает пустую оболочку.
- Вам нужно взаимодействовать: заполнять формы, кликать, прокручивать или ждать завершения сетевой активности.
- Вам нужно поведение на уровне браузера, такое как cookies,
localStorageили service workers. - Вы проверяете, как сайт ведёт себя для конкретного региона.
Оставайтесь с обычным HTTP-клиентом, когда страница рендерится на сервере; это гораздо дешевле и быстрее.
Предварительные требования
- Node.js 18 или новее и npm.
- Установленный Playwright с Chromium.
- Учётные данные ProxyScrape, а также хост и порт для
httpи/илиsocks5. Резидентные, датацентровые и мобильные прокси здесь все работают; выберите тип, который соответствует защите целевого сайта. - Linux, macOS или Windows. Примечания по CI предполагают раннеры Debian или Ubuntu.
Шаги
Шаг 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-раннерах.
Шаг 2 — Решите, применяется ли прокси к браузеру или к контексту
| Область | Где задаётся | Для чего использовать | Оговорки |
|---|---|---|---|
| Браузер | chromium.launch({ proxy }) |
Один эндпоинт на весь запуск | Каждый контекст использует один и тот же exit IP |
| Контекст | browser.newContext({ proxy }) |
Ротация: один эндпоинт на контекст, параллельно | Поддержка зависит от движка; Chromium — безопасный вариант по умолчанию — проверьте поддержку прокси для каждого контекста в вашей версии Playwright, прежде чем полагаться на неё в Firefox или WebKit |
Два практических правила:
- Передавайте учётные данные как отдельные поля
usernameиpassword. Chromium игнорирует учётные данные, встроенные в URL прокси-сервера. - Значение
serverпринимает схему:http://host:portдля HTTP-эндпоинтов ProxyScrape иsocks5://host:portдля SOCKS5.
Шаг 3 — Запустите через один эндпоинт и проверьте exit IP
// launch-proxy.mjs
import { chromium } from 'playwright';
const proxy = {
server: process.env.PROXYSCRAPE_SERVER, // http://HOST:PORT или 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 — Быстро обнаруживайте, когда трафик не проксируется
Тихая ошибка конфигурации — например, опечатка в строке server — может оставить вас скрапить со своего собственного 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— туннель CONNECT к HTTPS-цели был отклонён; убедитесь, что настроенный протокол соответствует эндпоинту.net::ERR_PROXY_AUTH_REQUESTED— учётные данные были отклонены; перепроверьте имя пользователя и пароль и передавайте их как поля, а не внутри URL сервера.TimeoutError— эндпоинт медленный или цель тяжёлая; увеличьте тайм-аут и снизьте параллелизм.
Когда появляется одна из этих ошибок, удалите этот эндпоинт из очереди до конца запуска и продолжайте с остальными. Повторные попытки с тем же сломанным эндпоинтом только тратят время.
Шаг 7 — Запустите это в Linux CI без исчерпания ресурсов
- Установите зависимости браузера в пайплайне с помощью
npx playwright install --with-deps chromium. - Поддерживайте параллелизм на уровне двух-трёх контекстов на vCPU на CI-раннере; каждый контекст несёт отрендеренную страницу.
- Запускайте браузер один раз на процесс и закрывайте контексты в блоках
finally. - Предпочитайте датацентровые прокси для высокообъёмных целей с низкой защитой в запланированных задачах, а резидентные или мобильные эндпоинты оставляйте для более сложных целей.
- Следите за юнит-экономикой: оплата по мере использования упрощает запуск нерегулярных CI-запусков, а фиксированный месячный план обычно дешевле для регулярной ночной задачи.
Устранение неполадок
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Страницы загружаются, но с вашего собственного IP | Прокси игнорируется (опечатка в server или опция не была передана в контекст) |
Логируйте exit IP с помощью помощника из шага 4 и выбрасывайте ошибку, когда он совпадает с прямым IP |
ERR_PROXY_CONNECTION_FAILED |
Неверный хост или порт, или исходящий трафик CI заблокирован | Проверьте тот же эндпоинт с помощью curl, затем проверьте сетевую политику |
ERR_PROXY_AUTH_REQUESTED |
Неверные учётные данные или они встроены в URL | Передавайте поля username и password, никогда user:pass@host |
ERR_TUNNEL_CONNECTION_FAILED на HTTPS |
Несоответствие протокола между эндпоинтом и настроенной схемой | Попробуйте HTTP-эндпоинт, затем SOCKS5-эндпоинт и оставьте тот, который работает |
| Каждый контекст сообщает один и тот же exit IP | Используется прокси уровня браузера вместо уровня контекста, или движок не поддерживает прокси для каждого контекста | Используйте chromium.launch() и задавайте proxy в каждом newContext() |
| Запуск убивает CI-раннер | Слишком много одновременных контекстов | Снизьте CONCURRENCY до 2 и заново измерьте память |
Итог
- В Playwright прокси принадлежит вызову запуска или контексту браузера, а не отдельным запросам.
- Проверяйте exit IP перед скрапингом; неправильно настроенный прокси может незаметно оставить вас на собственном IP.
- Ротируйте, давая каждому контексту собственный HTTP- или SOCKS5-эндпоинт ProxyScrape и ограничивая количество одновременно работающих контекстов.
- Классифицируйте сбои
ERR_PROXY*иERR_TUNNEL*как проблемы эндпоинта, отбрасывайте эндпоинт и продолжайте запуск.