Почему сырой Puppeteer ловят на первых же запросах
Если вы хоть раз запускали Puppeteer «из коробки» и получали капчу на втором запросе — вы не одиноки. Проблема не в вашей логике, а в том, что автоматизированный Chromium оставляет десятки сигналов, которые системы антибота (Cloudflare, Datadome, PerimeterX, Kasada) считывают ещё до рендера страницы.
Три основных вектора детекции:
navigator.webdriver = true— флаг, который Chromium автоматически устанавливает при запуске через CDP. Антибот-скрипты проверяют его в первую очередь.- Несогласованный массив плагинов — в headless-режиме
navigator.pluginsпуст, аnavigator.mimeTypesсодержит артефакты, которых у реального пользователя быть не должно. Расхождение — мгновенный бан. - Iframe-артефакты ChromeDriver — CDP-протокол инжектит в страницу скрытые iframe и скрипты (
__webdriver_evaluate,__selenium_unwrapped), которые детектятся черезdocument.querySelectorAll('iframe')и проверкуwindow.cdc_adoQpoasnfa76pfcZLmcfl_Array.
Playwright страдает теми же проблемами — Microsoft тоже использует CDP, и флаги автоматизации никуда не деваются. Поэтому puppeteer-extra stealth proxy — это не опция, а базовое требование для любого production-краулера.
puppeteer-extra + stealth-плагин: что именно патчится
puppeteer-extra — это обёртка над Puppeteer с плагинной архитектурой, а puppeteer-extra-plugin-stealth — набор из 10+ эвазионных модулей. Каждый модуль — это Page.evaluateOnNewDocument, который подменяет свойства до выполнения скриптов сайта.
Ключевые патчи stealth-плагина:
| Модуль | Что патчит | Без патча |
|---|---|---|
webdriver | navigator.webdriver → false/undefined | true — мгновенная детекция |
plugins | Генерирует реалистичный массив плагинов | Пустой navigator.plugins |
languages | Согласует navigator.languages с заголовками | Расхождение Accept-Language и JS |
iframe.contentWindow | Убирает артефакты ChromeDriver из iframe | Видимые CDP-инъекции |
media codecs | Эмулирует поддержку кодеков как в обычном Chrome | Headless-паттерн «нет кодеков» |
user-agent-override | Синхронизирует UA с заголовками | Несоответствие UA и sec-ch-ua |
webgl | Подменяет vendor/renderer на реальные значения | Google Inc. (NVIDIA) → headless-паттерн |
Базовая настройка занимает буквально 5 строк:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto('https://bot.sannysoft.com/');
// Большинство красных строк станут зелёными
await page.screenshot({ path: 'stealth-check.png' });
await browser.close();
})();
Но это только первый слой. Stealth патчит JS-энвайронмент, но не скрывает ваш IP. Если вы делаете 500 запросов с одного datacenter-IP — никакой stealth не спасёт.
Комбинация stealth + residential-прокси: максимальный антидетект-стек
Золотое правило: каждый fingerprint-профиль должен соответствовать уникальному IP-адресу. Residential-прокси дают IP реальных устройств провайдеров, поэтому антибот-системы не могут отличить ваш запрос от обычного пользователя.
ProxyHat предоставляет residential-прокси с гео-таргетингом и sticky-сессиями — это критично для Puppeteer anti-detection, потому что смена IP посреди сессии — мгновенный бан.
Подключение residential-прокси к Puppeteer
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
const PROXY_URL = 'http://user-country-US:PASSWORD@gate.proxyhat.com:8080';
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=${PROXY_URL}`,
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled'
]
});
const page = await browser.newPage();
// Аутентификация прокси
await page.authenticate({
username: 'user-country-US',
password: 'PASSWORD'
});
await page.goto('https://browserleaks.com/ip');
console.log('IP через residential-прокси:', await page.evaluate(() => document.body.innerText));
await browser.close();
})();
Используйте sticky-сессии для многостраничных сценариев (логин → навигация → скрейпинг). Формат: user-session-abc123-country-US — один IP держится до 30 минут.
Кастомные эвалюаторы: рандомизация canvas и WebGL fingerprint
Stealth-плагин убирает явные маркеры автоматизации, но canvas-fingerprint и WebGL-renderer — это пассивные сигналы, которые антибот собирает для построения уникального профиля. Если 100 ваших «уникальных» сессий имеют одинаковый canvas-хэш — вы попали.
Решение: инжектить рандомизацию до выполнения скриптов сайта через evaluateOnNewDocument.
Рандомизация canvas-fingerprint
function getCanvasNoiseScript() {
const seed = Math.random() * 0.01; // Тонкий шум, невидимый глазу
return `
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type) {
const ctx = this.getContext('2d');
if (ctx) {
const imageData = ctx.getImageData(0, 0, this.width, this.height);
for (let i = 0; i < imageData.data.length; i += 4) {
// Добавляем микрошум в альфа-канал
imageData.data[i + 3] = imageData.data[i + 3] + ${seed};
}
ctx.putImageData(imageData, 0, 0);
}
return origToDataURL.apply(this, arguments);
};
`;
}
// Применяем на каждой новой странице
page.evaluateOnNewDocument(getCanvasNoiseScript());
Рандомизация WebGL renderer/vendor
function getWebGLNoiseScript() {
const vendors = ['Google Inc. (NVIDIA)', 'Google Inc. (Intel Inc.)', 'Google Inc. (Apple)'];
const renderers = [
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER, OpenGL 4.6)',
'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.6)',
'ANGLE (Apple, Apple M1 GPU, OpenGL 4.6)'
];
const idx = Math.floor(Math.random() * vendors.length);
return `
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {
if (param === 37445) return '${vendors[idx]}'; // UNMASKED_VENDOR
if (param === 37446) return '${renderers[idx]}'; // UNMASKED_RENDERER
return getParameter.call(this, param);
};
`;
}
page.evaluateOnNewDocument(getWebGLNoiseScript());
Ключевой момент: пара vendor/renderer должна соответствовать платформе. Не ставьте «NVIDIA GeForce» на macOS — это мгновенная аномалия. Привязывайте выбор к navigator.platform и User-Agent.
Ротация прокси на уровне BrowserContext
Открывать новый браузер на каждый запрос — дорого (2–5 секунд запуска, ~200 МБ RAM). BrowserContext (инкогнито-контекст) — это изолированная сессия с собственными cookies, storage и кэшем, но в рамках одного процесса Chromium. Идеально для параллельного скрейпинга с разными прокси.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
const PROXY_CONFIGS = [
{ user: 'user-country-US-session-s1', pass: 'PASSWORD' },
{ user: 'user-country-DE-session-s2', pass: 'PASSWORD' },
{ user: 'user-country-GB-session-s3', pass: 'PASSWORD' },
];
async function createContextWithProxy(browser, proxyConf) {
// Puppeteer не поддерживает прокси на уровне контекста,
// поэтому используем CDP-метод для перехвата авторизации
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.authenticate({
username: proxyConf.user,
password: proxyConf.pass
});
// Инжектим уникальный fingerprint для этого контекста
page.evaluateOnNewDocument(getCanvasNoiseScript());
page.evaluateOnNewDocument(getWebGLNoiseScript());
return { context, page };
}
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-blink-features=AutomationControlled'
]
});
const tasks = PROXY_CONFIGS.map(async (conf) => {
const { context, page } = await createContextWithProxy(browser, conf);
try {
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
const ip = await page.evaluate(() => document.body.innerText);
console.log(`[${conf.user}] IP:`, ip);
} finally {
await context.close(); // Освобождаем ресурсы
}
});
await Promise.all(tasks);
await browser.close();
})();
Ограничение Puppeteer:--proxy-serverзадаётся на уровне браузера, а не контекста. Для истинной ротации прокси на каждый контекст используйте CDPSession и перехватывайте запросы черезFetch.requestPaused, подменяя URL-адрес прокси-сервера. Альтернатива — запускать отдельный браузер-процесс на каждый контекст (дороже, но надёжнее).
Масштабирование: контейнерные флоты, пулы браузеров, управление ресурсами
Когда вы переходите от 10 к 1000 параллельных сессий, архитектура краулера меняется принципиально. Вот три уровня масштабирования.
1. Пул браузеров с рециркуляцией контекстов
Создаём N браузеров (по числу ядер CPU) и переиспользуем контексты внутри каждого:
class BrowserPool {
constructor(size = 4) {
this.browsers = [];
this.size = size;
}
async init() {
for (let i = 0; i < this.size; i++) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage', // Критично в Docker
'--disable-gpu',
`--user-data-dir=/tmp/puppeteer-profile-${i}` // Изоляция профилей
]
});
this.browsers.push(browser);
}
}
async withContext(taskFn) {
const browser = this.browsers.reduce((min, b) =>
(b._activeContexts || 0) < (min._activeContexts || 0) ? b : min
);
browser._activeContexts = (browser._activeContexts || 0) + 1;
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
try {
return await taskFn(page);
} finally {
await context.close();
browser._activeContexts--;
}
}
async close() {
await Promise.all(this.browsers.map(b => b.close()));
}
}
2. Контейнеризация с ограничением ресурсов
Каждый браузер-воркер — отдельный Docker-контейнер с жёсткими лимитами памяти:
# docker-compose.yml
version: '3.8'
services:
scraper-worker:
build: .
deploy:
replicas: 8
resources:
limits:
memory: 512M # Один Chromium = ~300-400 МБ
cpus: '0.5'
environment:
- PROXY_URL=http://user-country-US:PASSWORD@gate.proxyhat.com:8080
- MAX_CONTEXTS=3
- SESSION_TTL=600
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 30s
timeout: 10s
retries: 3
3. Оркестрация с очередью и retry-логикой
Для production-систем добавьте очередь (BullMQ / RabbitMQ) и экспоненциальный backoff:
- Очередь задач — каждая URL-адрес + параметры прокси = одна задача.
- Retry с backoff — при CAPTCHA или блокировке: пауза 2^n секунд, смена sticky-сессии, повтор.
- Метрики — Prometheus-экспортер: success rate, latency, CAPTCHA rate на каждый гео-пул.
- Graceful shutdown — SIGTERM → завершить текущие контексты → закрыть браузеры → выйти.
| Метрика | Целевое значение | Действие при превышении |
|---|---|---|
| Success rate | > 92% | Смена sticky-сессии, проверка прокси |
| P95 latency | < 8 сек | Уменьшить параллелизм, проверить гео |
| CAPTCHA rate | < 5% | Добавить паузы, снизить RPS |
| Memory per worker | < 450 МБ | Проверить утечки, перезапуск контекста |
Этика: stealth для легитимного скрейпинга, а не для мошенничества
Техники антидетекции — это инструмент, а не оружие. Puppeteer residential proxies и stealth-плагины оправданы для:
- Мониторинга цен на собственных товарах и товарах конкурентов (легальная конкурентная разведка).
- Сбора открытых данных (SERP, публичные профили, открытые API).
- QA-тестирования собственных и клиентских веб-приложений.
- Сбора датасетов для обучения ML-моделей из общедоступных источников.
Они неоправданы для:
- Обхода платёжных систем и фрода.
- Массового создания фейковых аккаунтов.
- Нарушения
robots.txtи ToS платформы без веских оснований. - DDoS-атак и намеренной перегрузки серверов.
Соблюдайте robots.txt, ограничивайте RPS до разумных значений (1–3 запроса/сек на домен), и ваш краулер будет работать стабильно и легально. Подробнее об этике скрейпинга — в нашем руководстве по этике веб-скрейпинга.
Ключевые выводы
- Сырой Puppeteer детектится мгновенно —
navigator.webdriver, пустые плагины, CDP-артефакты. Stealth-плагин — обязательный минимум. - Stealth патчит JS-энвайронмент, но не IP — без residential-прокси вы уязвимы на сетевом уровне. Комбинируйте оба слоя.
- Каждая сессия = уникальный fingerprint + уникальный IP — рандомизируйте canvas/WebGL и привязывайте к sticky-сессии через ProxyHat.
- BrowserContext дешевле нового браузера — используйте пулы контекстов, но помните про ограничение ротации прокси на уровне CDP.
- Масштабируйте через контейнеры — 512 МБ на воркер, лимит CPU, healthcheck, graceful shutdown.
- Мониторьте метрики — success rate, CAPTCHA rate, latency. Если CAPTCHA > 5% — снижайте RPS или меняйте гео-пул.
- Скрейпите этично — уважайте
robots.txt, не перегружайте серверы, не используйте антидетект для фрода.
Готовы собрать production-антидетект-краулер? Подберите residential-прокси под вашу задачу на странице тарифов ProxyHat или изучите доступные локации прокси для гео-таргетинга.






