puppeteer-extra stealth proxy: полное руководство по антидетект-скрапингу в Node.js

Как обойти детекцию браузера: от патчей stealth-плагина до ротации residential-прокси. Практическое руководство с кодом для production-краулеров на Node.js.

puppeteer-extra stealth proxy: полное руководство по антидетект-скрапингу в Node.js

Почему сырой 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-плагина:

МодульЧто патчитБез патча
webdrivernavigator.webdriverfalse/undefinedtrue — мгновенная детекция
pluginsГенерирует реалистичный массив плагиновПустой navigator.plugins
languagesСогласует navigator.languages с заголовкамиРасхождение Accept-Language и JS
iframe.contentWindowУбирает артефакты ChromeDriver из iframeВидимые CDP-инъекции
media codecsЭмулирует поддержку кодеков как в обычном ChromeHeadless-паттерн «нет кодеков»
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 или изучите доступные локации прокси для гео-таргетинга.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог