Puppeteer-Extra Stealth + Proxy: Kompletny przewodnik po antydetekecji w Node.js

Dowiedz się, jak połączyć puppeteer-extra z wtyczką stealth i proxy residentalnymi od ProxyHat, aby zbudować crawler niewykrywalny przez bot-management — z rotacją IP per context i randomizacją fingerprintów.

Puppeteer-Extra Stealth + Proxy: Kompletny przewodnik po antydetekecji w Node.js

Dlaczego surowy Puppeteer jest łatwy do wykrycia

Każdy, kto próbował scrapować stronę chronioną przez Cloudflare, Datadome czy PerimeterX, wie: domyślna instancja Puppeteer pada już przy pierwszym requescie. Dlaczego? Bo przeglądarka Chromium kontrolowana przez DevTools Protocol zostawia ślady, które bot-detection systemy czytają jak otwarte książki.

Najważniejsze sygnały deanonimizujące:

  • navigator.webdriver — w automatyzowanym Chrome ustawione na true; bot-detektory sprawdzają to jako pierwszy filtr.
  • plugins/mimeTypes — pusta tablica navigator.plugins zamiast domyślnych wtyczek Chrome (PDF Viewer, Chrome PDF Viewer itd.).
  • iframe contentWindow — ramka z chrome.runtime lub chrome.csi nie istnieje w automatyzowanym Chrome, podczas gdy prawdziwa przeglądarka je posiada.
  • WebGL renderer — Headless Chrome raportuje „SwiftShader" lub „Mesa", co jest natychmiastowym czerwonym flagiem.
  • User-Agent niespójny z platformą — Headless Chrome domyślnie wysyła HeadlessChrome w UA stringu.
  • window.chrome — obiekt obecny w normalnym Chrome, nieobecny w headless.

Bot-detection nie sprawdza jednego sygnału — buduje score. Jeśli navigator.webdriver === true, dostajesz +30 pkt. Jeśli WebGL renderer to SwiftShader, kolejne +20. Przy progu 50–70 pkt trafiasz na CAPTCHĘ lub blokadę. Dlatego patchowanie pojedynczych sygnałów nie wystarcza — potrzebujesz systemowej naprawy.

Puppeteer-Extra + Stealth Plugin: co dokładnie patchuje

puppeteer-extra to wrapper nad Puppeteerem z architekturą wtyczkową. Wtyczka puppeteer-extra-plugin-stealth aplikuje zestaw ewaluacji (page.evaluateOnNewDocument), które nadpisują właściwości przeglądarki przed załadowaniem jakiejkolwiek strony.

Oto sygnały, które stealth patchuje out-of-the-box:

SygnałCo stealth robi
navigator.webdriverNadpisuje na undefined / false
navigator.pluginsWstrzykuje realistyczną listę wtyczek Chrome
navigator.languagesUstawia spójną z UA listę języków
window.chromeTworzy obiekt z runtime, loadTimes, csi
iframe contentWindow.chromeReplikuje chrome object w iframe'ach
WebGL vendor/rendererZmienia SwiftShader na realistyczny GPU
navigator.permissionsNaprawia query API dla notifications/geolocation
Chrome DevTools artifactsUsuwa ślady CDP z window

Podstawowa konfiguracja jest trywialna:

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-blink-features=AutomationControlled'],
  });
  const page = await browser.newPage();
  await page.goto('https://bot.sannysoft.com/');
  // Zobaczysz zielone checkmarki zamiast czerwonych flag
  await new Promise(r => setTimeout(r, 30000));
  await browser.close();
})();

Flaga --disable-blink-features=AutomationControlled jest kluczowa — wyłącza ustawianie navigator.webdriver = true na poziomie Chromium, zanim stealth w ogóle zadziała. Bez niej niektóre detektory i tak wyłapują automatyzację.

Łączenie stealth z proxy residentalnymi — najmocniejszy stack

Stealth patchuje sygnały przeglądarki. Ale bot-detection bierze pod uwagę też sygnały sieciowe:

  • IP z datacenter (ASN przypisane do AWS/GCP/OVH) = natychmiastowy +40 pkt do score.
  • IP z regionu niespójnego z językiem UA (np. UA=pl-PL, IP z Lagos) = kolejne +20.
  • Wiele requestów z jednego IP w krótkim czasie = rate-limit / CAPTCHA challenge.

Dlatego residential proxies to nie „opcja premium" — to fundament. IP z puli residentalnej ma ASN zwykłego ISP, przechodzi przez większość GeoIP checków i nie trafi na blocklisty datacenter-ASN.

Konfiguracja ProxyHat z puppeteer-extra:

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

const PROXY_USER = 'user-country-US';
const PROXY_PASS = 'twoje_haslo';

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled',
      `--proxy-server=http://gate.proxyhat.com:8080`,
    ],
  });

  const page = await browser.newPage();
  await page.authenticate({
    username: PROXY_USER,
    password: PROXY_PASS,
  });

  await page.goto('https://httpbin.org/ip');
  const ip = await page.$eval('pre', el => el.textContent);
  console.log('Moje IP przez ProxyHat:', ip);

  await browser.close();
})();

Geo-targetowanie w ProxyHat działa przez username — zmiana kraju to zmiana jednego ciągu znaków, nie zmiana endpointu. Chcesz IP z Niemiec? user-country-DE. Z Berlina? user-country-DE-city-berlin. To kluczowe dla spójności: Twój UA mówi „de-DE", Twoje IP jest z Frankfurtu, WebGL raportuje niemieckie GPU — detektor nie ma powodu do podejrzeń.

Custom evaluatory: randomizacja canvas i WebGL per sesja

Stealth plugin normalizuje sygnały, ale nie randomizuje fingerprintu. Jeśli 100 sesji z Twojego crawlera raportuje identyczny canvas hash, to z czasorem detektorzy to wyłapią jako bot-cluster. Potrzebujesz per-session noise injection.

Kluczowe obszary randomizacji:

  • Canvas fingerprint — dodaj sub-pixelowy noise do operacji rysowania.
  • WebGL fingerprint — zmień parametry RENDERER/VENDOR oraz dodaj noise do readPixels.
  • AudioContext — delikatny noise do oscylatora.
function createFingerprintNoise(seed) {
  const noise = (Math.random() * 0.01 - 0.005).toFixed(6);
  return `
    // Canvas noise — sub-pixel shift w toDataURL
    const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
    HTMLCanvasElement.prototype.toDataURL = function(type) {
      const ctx = this.getContext('2d');
      if (ctx) {
        const imgData = ctx.getImageData(0, 0, this.width, this.height);
        for (let i = 0; i < imgData.data.length; i += 4) {
          imgData.data[i] += Math.round(${noise} * 2);     // R
          imgData.data[i+1] += Math.round(${noise} * 3);   // G
        }
        ctx.putImageData(imgData, 0, 0);
      }
      return origToDataURL.apply(this, arguments);
    };

    // WebGL noise — zmień UNMASKED_RENDERER
    const getParamOrig = WebGLRenderingContext.prototype.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(param) {
      if (param === 0x1F00) return 'NVIDIA GeForce GTX 1060'; // UNMASKED_VENDOR
      if (param === 0x1F01) return 'NVIDIA GeForce GTX 1060'; // UNMASKED_RENDERER
      return getParamOrig.call(this, param);
    };

    // Analogicznie dla WebGL2
    if (typeof WebGL2RenderingContext !== 'undefined') {
      const getParam2Orig = WebGL2RenderingContext.prototype.getParameter;
      WebGL2RenderingContext.prototype.getParameter = function(param) {
        if (param === 0x1F00) return 'NVIDIA GeForce GTX 1060';
        if (param === 0x1F01) return 'NVIDIA GeForce GTX 1060';
        return getParam2Orig.call(this, param);
      };
    }
  `;
}

// Użycie per page:
const noiseScript = createFingerprintNoise(Date.now());
await page.evaluateOnNewDocument(noiseScript);

Ważne: noise musi być deterministyczny per sesję (aby kolejne wizyty tej samej sesji dawały ten sam fingerprint), ale różny między sesjami. Dlatego seedujesz go z session ID. Sticky session w ProxyHat (user-session-abc123:pass) + session-seeded noise = spójna tożsamość przeglądarki przez całą sesję.

Rotacja proxy per browser context

Puppeteer pozwala tworzyć wiele BrowserContextów (odpowiednik profili przeglądarki) w jednej instancji Chrome. Problem: --proxy-server ustawia się na poziomie launch, nie kontekstu. Jak rotować IP per kontekst?

Rozwiązanie: proxy per page z użyciem CDP lub osobna instancja browsera per kontekst. W produkcji ta druga opcja jest stabilniejsza.

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());

const CREDENTIALS = [
  { username: 'user-country-US', password: 'twoje_haslo' },
  { username: 'user-country-DE-city-berlin', password: 'twoje_haslo' },
  { username: 'user-country-JP', password: 'twoje_haslo' },
];

async function createContextProxy(credentials) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled',
      `--proxy-server=http://gate.proxyhat.com:8080`,
    ],
  });

  const page = await browser.newPage();
  await page.authenticate({
    username: credentials.username,
    password: credentials.password,
  });

  return { browser, page };
}

(async () => {
  // Uruchom 3 izolowane konteksty z różnymi IP
  const contexts = await Promise.all(
    CREDENTIALS.map(creds => createContextProxy(creds))
  );

  for (const { page, browser } of contexts) {
    await page.goto('https://httpbin.org/ip');
    const ip = await page.$eval('pre', el => el.textContent);
    console.log('IP:', ip.trim());
  }

  await Promise.all(contexts.map(c => c.browser.close()));
})();

Dla lepszego zarządzania zasobami, zbuduj browser pool:

class BrowserPool {
  constructor(maxSize = 5) {
    this.maxSize = maxSize;
    this.available = [];
    this.inUse = new Set();
  this.creds = [
    { username: 'user-country-US', password: 'twoje_haslo' },
    { username: 'user-country-GB', password: 'twoje_haslo' },
    { username: 'user-country-FR', password: 'twoje_haslo' },
    { username: 'user-country-DE', password: 'twoje_haslo' },
    { username: 'user-country-ES', password: 'twoje_haslo' },
  ];
    this.credIndex = 0;
  }

  async acquire() {
    if (this.available.length > 0) {
      const ctx = this.available.pop();
      this.inUse.add(ctx);
      return ctx;
    }
    if (this.inUse.size < this.maxSize) {
      const creds = this.creds[this.credIndex % this.creds.length];
      this.credIndex++;
      const ctx = await this._launch(creds);
      this.inUse.add(ctx);
      return ctx;
    }
    // Czekaj na zwolnienie
    return new Promise(resolve => {
      const check = setInterval(async () => {
        if (this.available.length > 0) {
          clearInterval(check);
          const ctx = this.available.pop();
          this.inUse.add(ctx);
          resolve(ctx);
        }
      }, 500);
    });
  }

  async release(ctx) {
    this.inUse.delete(ctx);
    this.available.push(ctx);
  }

  async _launch(creds) {
    const puppeteer = require('puppeteer-extra');
    const StealthPlugin = require('puppeteer-extra-plugin-stealth');
    puppeteer.use(StealthPlugin());

    const browser = await puppeteer.launch({
      headless: 'new',
      args: [
        '--no-sandbox',
        '--disable-blink-features=AutomationControlled',
        `--proxy-server=http://gate.proxyhat.com:8080`,
      ],
    });
    const page = await browser.newPage();
    await page.authenticate({ username: creds.username, password: creds.password });
    return { browser, page, id: creds.username };
  }

  async drain() {
    for (const ctx of [...this.inUse, ...this.available]) {
      await ctx.browser.close();
    }
    this.inUse.clear();
    this.available = [];
  }
}

// Użycie:
const pool = new BrowserPool(5);
const ctx = await pool.acquire();
try {
  await ctx.page.goto('https://example.com');
  // ... scraping logic
} finally {
  await pool.release(ctx);
}

Skalowanie: konteneryzacja, fleet management, resource management

Pojedyncza maszyna z 8 GB RAM utrzyma ~5–8 instancji Chrome (każda ~500 MB–1 GB). Do produkcyjnego scrapowania potrzebujesz fleet.

Docker + browser pool

  • Każdy worker to kontener z Node.js + Chromium.
  • Orkiestracja przez Docker Compose (małe fleet) lub Kubernetes (duże fleet).
  • Limity pamięci per kontener: mem_limit: 1.5g — Chrome z OOM killer to częsty problem.
# docker-compose.yml
version: '3.8'
services:
  scraper-worker:
    build: .
    deploy:
      replicas: 5
      resources:
        limits:
          memory: 1500M
          cpus: '1.0'
    environment:
      - PROXY_USER=user-country-US
      - PROXY_PASS=twoje_haslo
      - MAX_CONCURRENT=3
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
  redis:
    image: redis:7-alpine

Resource management — best practices

  • Auto-close idle browsers: ustaw timer — jeśli strona nie wykonuje akcji przez 60s, zamknij kontekst. Wycieki pamięci to główna przyczyna crashy.
  • Health checks: co 30s sprawdź browser.version(). Jeśli timeout — zabij i odtwórz.
  • Queue-based architecture: Redis jako task queue, workerzy pobierają URL-i, scrapują, odkładają wyniki. Zero koordynacji między workerami.
  • Sticky sessions: dla stron z logowaniem, użyj user-session-abc123 w ProxyHat, aby wszystkie requesty jednej sesji szły przez jedno IP. Czas życia sesji: dostosuj do TTL strony (zwykle 10–30 min).
  • Concurrency tuning: nie przekraczaj 3 stron per browser. Chrome współdzieli pamięć GPU między tabami — przy 10+ tabach crash jest kwestią czasu.

Porównanie strategii skalowania

StrategiaZłożonośćKosztNiezawodnośćNajlepsze dla
Pojedynczy server, 1 browserMinimalna$5–20/mies.NiskaPrototypy, małe joby
Pojedynczy server, browser poolŚrednia$50–100/mies.ŚredniaŚredni traffic, 1 target
Docker Compose, 3–5 workerówŚrednia$150–300/mies.WysokaMulti-target, produkcja
Kubernetes, auto-scalingWysoka$500+/mies.Bardzo wysokaEnterprise, 100k+ stron/dzień

Etyka: stealth do legalnego scrapingu, nie do oszustwa

Techniki antydetekecji to potężne narzędzie. Używaj ich odpowiedzialnie:

  • Scraping publicznych danych (ceny, wyniki wyszukiwania, dane kontaktowe) — legalny use case w większości jurysdykcji, pod warunkiem przestrzegania robots.txt i rate-limitów.
  • Omijanie paywalli, kradzież tożsamości, credential stuffing — nielegalne i nieetyczne. Stealth nie jest po to stworzony.
  • GDPR/CCPA — jeśli scrapujesz dane osobowe, musisz mieć podstawę prawną. Technika nie zwalnia z obowiązków prawnych.
  • ToS stron — łamanie regulaminu może skutkować blokadą konta lub pozwem cywilnym. Bądź świadomy ryzyka.

Stealth + residential proxy to stack do odpornego, niezawodnego scrapingu — nie do maskowania złośliwej aktywności. Szanuj granice prawne i etyczne.

Key Takeaways

  • Surowy Puppeteer jest łatwo wykrywalny — navigator.webdriver, puste plugins, SwiftShader w WebGL to natychmiastowe flagi.
  • puppeteer-extra-plugin-stealth patchuje 9+ sygnałów, ale nie randomizuje fingerprintu — musisz dodać własny noise injection per sesja.
  • Residential proxies są niezbędne — datacenter IP jest wykrywane na podstawie ASN. ProxyHat z geo-targetowaniem w username daje spójność IP ↔ język ↔ lokalizacja.
  • Rotacja IP per kontekst wymaga osobnej instancji browsera (lub CDP-level proxy override). Browser pool z limitem pamięci to produkcyjny standard.
  • Skalowanie = konteneryzacja + queue + auto-recovery. Kubernetes dla dużych fleet, Docker Compose dla średnich.
  • Etyka first — scrapuj legalnie, przestrzegaj robots.txt, GDPR i ToS.

Kolejne kroki

Gotowy, by zbudować niewykrywalny crawler? Zacznij od planu ProxyHat, który daje Ci dostęp do residentalnych IP w 190+ lokalizacjach, połącz go z puppeteer-extra-stealth i fingerprint randomization — i scrapuj z confidence, nie z lękiem przed CAPTCHĄ.

Więcej wzorców scrapingu znajdziesz w naszym przewodniku po best practices i na stronie use case web-scraping.

Gotowy, aby zacząć?

Dostęp do ponad 50 mln rezydencjalnych IP w ponad 148 krajach z filtrowaniem AI.

Zobacz cenyProxy rezydencjalne
← Powrót do Bloga