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.webdriverdo usunięcia — Chrome nigdy nie ustawia tej flagi bez ChromeDrivera. - Nie ma wycieków CDP typu
Runtime.enableczyPage.enable, które systemy antybot jak Imperva wykrywają przezwindow.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 obiektBrowser, który zarządza procesem Chrome i listą otwartych kart.Browser— reprezentuje instancję Chrome; można z niego tworzyć noweTabobiektó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żywatab.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:
- Ustaw
--proxy-server=http://gate.proxyhat.com:8080wbrowser_args. - Dodaj rozszerzenie Chrome lub użyj CDP
Fetch.authRequiredevent handlera do automatycznej autoryzacji. - 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ście | RAM | Start | Przepustowość | Stealth |
|---|---|---|---|---|
| nodriver + residential | ~300 MB | ~800 ms | ~5 req/s | Wysoka |
| curl_cffi + residential | ~20 MB | ~50 ms | ~50 req/s | Średnia (TLS fingerprint) |
| httpx + datacenter | ~10 MB | ~10 ms | ~200 req/s | Niska |
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.txti 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:
- Zaloguj się na dashboard.proxyhat.com.
- Wybierz pakiet residential — sprawdź cennik ProxyHat.
- Skopiuj username i password z sekcji „Credentials”.
- Ustaw flagi geo:
user-country-USdla USA,user-country-DEdla Niemiec. Pełna lista na stronie lokalizacji. - 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-serverwbrowser_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.






