Scraping z nodriver: Przewodnik po asynchronicznym CDP i proxy residential

Kompletny przewodnik po nodriver — asynchronicznym następcy undetected-chromedriver. Dowiedz się, jak skonfigurować proxy residential, skalować równoległe karty i omijać Cloudflare bez wycieków CDP.

Scraping With nodriver: A Practical Guide to Async CDP + Residential Proxies

Jeśli kiedykolwiek walczyłeś z navigator.webdriver === true, błędami 403 od Cloudflare albo nagłym zablokowaniem konta po 200 żądaniach, wiesz, że klasyczny Selenium z ChromeDriver nie wystarcza w 2025 roku. Scraping z nodriver to podejście, które eliminuje te problemy u źródła: zamiast warstwy WebDriver, nodriver łączy się bezpośrednio z Chrome przez Chrome DevTools Protocol (CDP) over websockets. W tym przewodniku pokażę, jak zintegrować nodriver z proxy residential ProxyHat, skalować równoległe karty w Dockerze i kiedy w ogóle zrezygnować z pełnej przeglądarki na rzecz curl_cffi.

Ważne zastrzeżenie prawne: Poniższe techniki stosuj wyłącznie do pobierania publicznie dostępnych danych z witryn, na które masz autoryzację lub które wyraźnie na to pozwalają. Naruszenia Computer Fraud and Abuse Act (CFAA) w USA oraz RODO/GDPR w UE mogą skutkować odpowiedzialnością cywilną i karną. Szanuj robots.txt i regulaminy serwisów.

Dlaczego scraping z nodriver działa lepiej niż undetected-chromedriver

Chrome DevTools Protocol to binarny protokół JSON przez websocket, który Google Chrome wystawia domyślnie na porcie 9222. Klasyczny Selenium korzysta z tego protokołu pośrednio — przez ChromeDriver, który sam jest serwerem HTTP tłumaczącym komendy W3C WebDriver na CDP. Ta warstwa pośrednia zostawia ślady: ustawia navigator.webdriver = true, dodaje flagę --enable-automation i rejestruje nasłuchiwacze CDP, które same w sobie są wykrywalne.

nodriver (autor: ultrafunkamsterdam, ten sam zespół co undetected-chromedriver) omija ten problem całkowicie. Nie ma ChromeDrivera, nie ma serwera W3C, nie ma Selenium. Biblioteka uruchamia Chrome z flagą --remote-debugging-port i łączy się bezpośrednio przez websocket. Dzięki temu:

  • Nie ma navigator.webdriver do usunięcia — Chrome nigdy nie ustawia tej flagi bez ChromeDrivera.
  • Nie ma wycieków CDP typu Runtime.enable czy Page.enable, które systemy antybot jak Imperva wykrywają przez window.cdc_* i timing.
  • Brak narzutu procesu ChromeDriver — mniej pamięci, szybszy start (~800 ms vs ~2,5 s dla Selenium).

Cloudflare i Imperva sprawdzają między innymi obecność window.chrome.runtime, spójność User-Agent z platformą, oraz Notification.permission. nodriver uruchamia pełnego Chrome, więc te wartości są natywne — nie trzeba ich patchować. To zasadnicza różnica względem rozwiązań headless-only typu Playwright z stealth plugin, gdzie patchowanie często bywa niedokończone.

Architektura nodriver w skrócie

nodriver jest w pełni asynchroniczny (oparty na asyncio) i używa biblioteki websockets do komunikacji z Chrome. Główne obiekty:

  • uc.start() — fabryka zwracająca obiekt Browser, który zarządza procesem Chrome i listą otwartych kart.
  • Browser — reprezentuje instancję Chrome; można z niego tworzyć nowe Tab obiektów.
  • Tab — pojedyncza karta; wszystkie operacje DOM są asynchroniczne: await tab.select('h1'), await tab.find('button').
  • Haczyki zdarzeń (event hooks) — zamiast WebDriverWait, nodriver używa tab.add_handler(CDP.event.Network.responseReceived, callback).

Ta architektura ma dwie konsekwencje praktyczne. Po pierwsze, cały kod musi być w async def — nie można mieszać synchronicznego API. Po drugie, brak driver.find_element(By.ID, ...) oznacza, że selektory są uproszczone: await tab.select('#price') przyjmuje selektor CSS i zwraca element natychmiast, jeśli istnieje, lub czeka do timeoutu.

Konfiguracja proxy w nodriver — --proxy-server i autoryzacja

nodriver nie ma wbudowanego zarządzania proxy ani rotacji IP. Konfiguruje się je przez argumenty Chrome, dokładnie tak jak w normalnej przeglądarce. Kluczowa flaga to:

--proxy-server=http://gate.proxyhat.com:8080

Problem: Chrome nie obsługuje autoryzacji proxy przez http://user:pass@host:port w --proxy-server. Musisz użyć rozszerzenia lub — co jest prostsze — przekazać dane logowania przez interfejs CDP. nodriver wystawia metodę browser.connection.send(cdp.network.set_extra_http_headers(...)), ale dla proxy z autoryzacją najczystszym podejściem jest użycie flagi --proxy-auth przez rozszerzenie lub — jeszcze prościej — poleganie na ProxyHat z adresem URL zawierającym poświadczenia w części autoryzacji, jeśli używasz SOCKS5.

W praktyce najpopularniejszy wzorzec to:

  1. Ustaw --proxy-server=http://gate.proxyhat.com:8080 w browser_args.
  2. Dodaj rozszerzenie Chrome lub użyj CDP Fetch.authRequired event handlera do automatycznej autoryzacji.
  3. Lub — alternatywnie — użyj SOCKS5 na porcie 1080, gdzie niektóre implementacje akceptują poświadczenia w URL.

Dlaczego residential, nie datacenter? Węzły datacenter (AWS, DigitalOcean, OVH) mają bloki IP, które Cloudflare flaguje od pierwszego żądania. Według raportu Cloudflare, ponad 40% ruchu z adresów datacenter jest klasyfikowane jako podejrzane. Residential IP pochodzą od dostawców ISP i są przypisane do realnych użytkowników domowych, co dramatycznie zmniejsza prawdopodobieństwo wyzwania CAPTCHA. Dla scraping z nodriver, gdzie celem jest pełna niewykrywalność headful Chrome, residential to jedyna sensowna opcja.

Budowanie URL proxy z ProxyHat

ProxyHat używa flag w nazwie użytkownika do kontroli geolokalizacji i sesji:

  • user-country-US — ruch wychodzi z USA.
  • user-country-DE-city-berlin — konkretne miasto.
  • user-session-abc123 — sticky session: ten sam IP przez czas życia sesji.

Pełny URL HTTP wygląda tak:

http://user-country-US-session-abc123:password@gate.proxyhat.com:8080

Dla SOCKS5:

socks5://user-country-US-session-abc123:password@gate.proxyhat.com:1080

Praktyczny przykład: nodriver + ProxyHat residential

Poniżej kompletny, uruchamialny skrypt. Zakłada pip install nodriver oraz konto ProxyHat. Skrypt uruchamia Chrome przez residential proxy US, nawiguje na stronę chronioną Cloudflare i ekstraktuje dane JSON z odpowiedzi API.

import asyncio
import nodriver as uc
import json

def build_proxyhat_url(country: str, session_id: str, password: str) -> str:
    """Buduje URL proxy ProxyHat z geo i sticky session."""
    username = f"user-country-{country}-session-{session_id}"
    return f"http://{username}:{password}@gate.proxyhat.com:8080"

async def scrape_protected_page(url: str, proxy_url: str):
    # nodriver przyjmuje proxy przez browser_args
    browser_args = [
        f"--proxy-server={proxy_url}",
        "--disable-blink-features=AutomationControlled",
        "--no-first-run",
        "--no-default-browser-check",
    ]
    
    browser = await uc.start(
        headless=False,
        browser_args=browser_args,
    )
    
    try:
        tab = await browser.get(url)
        
        # Czekaj na kluczowy element zamiast sleep
        price_el = await tab.select('.price-value', timeout=15)
        price = await price_el.get_attribute('textContent')
        
        # Ekstrakcja JSON z ukrytego script tag
        data_script = await tab.select('#__NEXT_DATA__', timeout=10)
        raw = await data_script.get_attribute('innerHTML')
        data = json.loads(raw)
        
        print(f"Price: {price.strip()}")
        print(f"Props: {data['props']['pageProps']}")
        return data
    
    finally:
        browser.stop()

if __name__ == "__main__":
    PROXYHAT_PASS = "twoje_haslo"
    proxy = build_proxyhat_url("US", "abc123", PROXYHAT_PASS)
    
    # UWAGA: Chrome nie obsługuje user:pass w --proxy-server.
    # Dla autoryzacji użyj SOCKS5 lub rozszerzenia autoryzującego.
    # Alternatywnie, stwórz rozszerzenie proxy_auth.crx.
    
    asyncio.run(scrape_protected_page(
        "https://example-protected.com/product/123",
        proxy
    ))

Powyższy kod ma jeden haczyk: Chrome nie natywnie obsługuje user:pass@ w --proxy-server. Dwie produkcyjne ścieżki rozwiązania:

Opcja A: Rozszerzenie autoryzujące (zalecane dla HTTP)

Stwórz minimalne rozszerzenie Chrome, które nasłuchuje zdarzenia onAuthRequired i wstrzykuje poświadczenia:

// background.js
chrome.webRequest.onAuthRequired.addListener(
  function(details) {
    return {
      authCredentials: {
        username: 'user-country-US-session-abc123',
        password: 'twoje_haslo'
      }
    };
  },
  { urls: ['*://gate.proxyhat.com:8080/*'] },
  ['blocking']
);

Pakujesz to jako .crx i ładujesz przez --load-extension=proxy_auth. nodriver wspiera tę flagę w browser_args.

Opcja B: SOCKS5 z poświadczeniami w URL

browser_args = [
    "--proxy-server=socks5://gate.proxyhat.com:1080",
]
# Autoryzacja SOCKS5 przez CDP Fetch.authRequired handler

Dla SOCKS5 musisz obsłużyć Fetch.authRequired przez handler CDP. nodriver wystawia tab.add_handler do rejestracji callbacków na zdarzenia CDP.

Skalowanie: równoległe karty, flota Docker, per-context proxy

Pojedyncza instancja nodriver obsługuje wiele kart asynchronicznie — to naturalne dla asyncio. Ale prawdziwe skalowanie wymaga kontenerów i przypisania różnych proxy do każdego kontekstu.

Wzorzec 1: Wiele kart w jednej instancji

import asyncio
import nodriver as uc

async def scrape_page(browser, url, session_id):
    proxy = f"http://user-country-US-session-{session_id}:pass@gate.proxyhat.com:8080"
    # Każda karta dziedziczy proxy z browser_args, więc dla per-tab proxy
    # potrzebujesz osobnych instancji browser lub CDP Network.setRequestInterception
    tab = await browser.get(url)
    title = await tab.select('title')
    text = await title.get_attribute('textContent')
    await tab.close()
    return text

async def main():
    browser = await uc.start(headless=True)
    urls = [f"https://example.com/page/{i}" for i in range(10)]
    sessions = [f"sess-{i:04d}" for i in range(10)]
    
    tasks = [scrape_page(browser, u, s) for u, s in zip(urls, sessions)]
    results = await asyncio.gather(*tasks)
    browser.stop()

asyncio.run(main())

Limit praktyczny: ~5–8 jednoczesnych kart na instancję Chrome. Powyżej tego Chrome zużywa >2 GB RAM i zaczyna throttling GC. Dla 100+ równoległych zadań potrzebujesz floty.

Wzorzec 2: Flota w Docker

Każdy kontener uruchamia jedną instancję nodriver z własnym proxy. Docker Compose:

# docker-compose.yml
version: '3.8'
services:
  scraper-1:
    build: .
    environment:
      - PROXYHAT_SESSION=worker-1
      - PROXYHAT_COUNTRY=US
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '1.0'
  scraper-2:
    build: .
    environment:
      - PROXYHAT_SESSION=worker-2
      - PROXYHAT_COUNTRY=DE
    deploy:
        limits:
          memory: 2G
          cpus: '1.0'

Dockerfile bazowy:

FROM python:3.12-slim

RUN apt-get update && apt-get install -y \
    chromium \
    chromium-driver \
    && rm -rf /var/lib/apt/lists/*

RUN pip install nodriver

COPY scraper.py /app/
WORKDIR /app
CMD ["python", "scraper.py"]

W scraper.py czytasz PROXYHAT_SESSION z środowiska i budujesz URL proxy per-worker. Dzięki temu każdy kontener ma inny sticky IP przez ProxyHat.

Per-context proxy przez CDP

Chromium wspiera ProxyConfig per browser context (od Chrome 116). Przez CDP:

await browser.connection.send(cdp.target.createBrowserContext(
    proxyConfig=cdp.network.ProxyConfig(
        rules=cdp.network.ProxyRules(
            proxyRules="gate.proxyhat.com:8080",
            # UWAGA: autoryzacja nadal wymaga rozszerzenia lub handlera
        )
    )
))

To pozwala na różne proxy per karta w jednej instancji, ale autoryzacja nadal jest problematyczna — ProxyHat wymaga user:pass, a CDP ProxyConfig nie ma pola na poświadczenia. Dlatego w produkcji najprościej jest uruchamiać osobne instancje Chrome per proxy.

Graceful shutdown

nodriver nie ma wbudowanego signal handlera. Dodaj własny:

import signal

browser = None

def shutdown(sig, frame):
    if browser:
        browser.stop()
    sys.exit(0)

signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)

W Dockerze SIGTERM jest wysyłany przy docker stop — bez handlera proces Chrome zostaje sierotą i zajmuje pamięć.

Kiedy NIE używać pełnej przeglądarki

nodriver jest potężny, ale kosztowny: ~300 MB RAM na instancję, ~800 ms start, ~150 MB obraz Docker. Jeśli strona nie wymaga JavaScript do renderowania danych, pełna przeglądarka to overkill.

PodejścieRAMStartPrzepustowośćStealth
nodriver + residential~300 MB~800 ms~5 req/sWysoka
curl_cffi + residential~20 MB~50 ms~50 req/sŚrednia (TLS fingerprint)
httpx + datacenter~10 MB~10 ms~200 req/sNiska

curl_cffi reimplementuje TLS fingerprint Chrome (JA3/JA4) w czystym Pythonie. Jeśli strona sprawdza tylko TLS fingerprint i nagłówki — a nie wykonuje challenge'u JS — curl_cffi z ProxyHat residential jest 10x szybszy i 15x lżejszy niż nodriver.

Reguła kciuka:

  • nodriver: Cloudflare Turnstile, Imperva Incapsula, rendering SPA, logowanie.
  • curl_cffi: API endpoints z TLS check, proste scrapowanie HTML.
  • httpx: wewnętrzne API, niechronione endpoints, testy.

Etyka i legalność: CFAA, RODO i autoryzacja

Scraping publicznie dostępnych danych jest legalny w wielu jurysdykcjach, ale granice są nieostre. W USA Van Buren v. United States (2021) zawęził definicję „exceeds authorized access” w CFAA, ale nie zalegalizował scrapowania za bypass paywall czy rate limit. W UE RODO/GDPR ogranicza przetwarzanie danych osobowych — nawet publicznie dostępnych — bez podstawy prawnej.

Praktyczne zasady:

  • Sprawdzaj robots.txt i regulamin (ToS) przed scrapowaniem.
  • Ustawiaj rate limit poniżej 1 req/s dla małych stron.
  • Nie scrapuj danych osobowych bez podstawy prawnej (Art. 6 RODO).
  • Cacheuj wyniki — nie pobieraj tego samego 1000 razy.

ProxyHat nie zachęca do naruszania regulaminów serwisów. Proxy są narzędziem neutralnym — odpowiedzialność za sposób użycia spoczywa na użytkowniku.

Konfiguracja ProxyHat — krok po kroku

Aby uruchomić powyższe przykłady, potrzebujesz konta ProxyHat:

  1. Zaloguj się na dashboard.proxyhat.com.
  2. Wybierz pakiet residential — sprawdź cennik ProxyHat.
  3. Skopiuj username i password z sekcji „Credentials”.
  4. Ustaw flagi geo: user-country-US dla USA, user-country-DE dla Niemiec. Pełna lista na stronie lokalizacji.
  5. Dla sticky session dodaj -session-{id} — ten sam IP przez sesję.

Sprawdź też nasze przypadki użycia: web scraping i SERP tracking. Dokumentacja techniczna dostępna jest na docs.proxyhat.com.

Najczęstsze błędy i przypadki brzegowe

1. Zapominanie o await

nodriver jest w pełni asynchroniczny. tab.select(...) bez await zwraca coroutine, nie element. Lintuj z ruff i regułą ASYNC100.

2. headless=True bez --headless=new

Stary headless Chrome (--headless=old) ma inne User-Agent i fingerprint niż headful. nodriver domyślnie używa --headless=new od Chrome 112+, ale jeśli wymuszasz starszą wersję, dodaj flagę ręcznie.

3. Brak rotacji sesji

Sticky session jest przydatna dla logowania, ale dla czystego scrapingu danych, rotacja IP co ~50 żądań zmniejsza ryzyko blokady. Używaj różnych session-{id} dla każdego batcha.

4. Niezamykanie kart

Każda otwarta karta zużywa ~40 MB. Po 100 zadaniach bez await tab.close() Chrome zużywa >4 GB i crashuje z OOM.

5. Zbyt krótki timeout

Cloudflare challenge może trwać 5–8 sekund. Domyślny timeout tab.select to 10 s — zwiększ do 20 s dla stron z JS challenge.

Kluczowe wnioski (Key Takeaways)

  • nodriver eliminuje wycieki CDP i navigator.webdriver — to fundamentalna przewaga nad Selenium/undetected-chromedriver.
  • Proxy konfiguruje się przez --proxy-server w browser_args — autoryzacja wymaga rozszerzenia lub handlera CDP.
  • Residential proxy są obowiązkowe dla headful stealth — datacenter IP są flagowane od pierwszego żądania.
  • Skaluj przez kontenery Docker, nie przez jedną instancję z 50 kartami — limit to ~5–8 kart na Chrome.
  • Dla prostych API używaj curl_cffi — 10x szybszy, 15x lżejszy, jeśli nie potrzebujesz JS.
  • Szanuj regulaminy i RODO — proxy są neutralne, odpowiedzialność jest Twoja.

FAQ

Czym jest scraping z nodriver?

nodriver to asynchroniczna biblioteka Python, która łączy się z Chrome bezpośrednio przez Chrome DevTools Protocol (CDP) over websockets, z pominięciem ChromeDrivera i Selenium. Eliminuje to wycieki takie jak navigator.webdriver i flagi automatyzacji, co czyni go skuteczniejszym przeciwko Cloudflare i Imperva niż klasyczny undetected-chromedriver.

Dlaczego scraping z nodriver ma znaczenie dla użytkowników proxy?

Ponieważ nodriver uruchamia pełnego Chrome, fingerprint przeglądarki jest natywny — nie wymaga patchowania. W połączeniu z residential proxy (jak ProxyHat) tworzy parę, która maksymalizuje stealth: realny IP ISP + realny fingerprint Chrome. Datacenter IP z nodriver nadal będą flagowane, bo Cloudflare sprawdza ASN, nie tylko przeglądarkę.

Który typ proxy działa najlepiej z nodriver?

Residential proxy są najlepsze dla nodriver, ponieważ ich adresy IP pochodzą od dostawców ISP i są przypisane do realnych użytkowników domowych. Mobile proxy są jeszcze bardziej zaufane, ale droższe. Datacenter proxy są nieodpowiednie do scrapowania stron chronionych Cloudflare — ponad 40% ruchu z tych bloków jest klasyfikowane jako podejrzane.

Jak unikać blokad przy scrapingu z nodriver?

Kluczowe praktyki: używaj residential proxy z rotacją sesji (różne session-{id} co ~50 żądań), ustawiaj realistyczne opóźnienia (1–3 s między żądaniami), zamykaj karty po użyciu (await tab.close()), zwiększaj timeout do 20 s dla stron z JS challenge, i unikaj headless=old — używaj --headless=new lub trybu headful. Dla autoryzacji proxy używaj rozszerzenia Chrome z onAuthRequired.

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