Scraping con nodriver è oggi l'approccio più efficace per automatizzare browser Chrome in Python contro siti protetti da Cloudflare, Imperva o DataDome. Se stai migrando da undetected-chromedriver — il cui sviluppo è fermo e le cui tracce navigator.webdriver sono ormai banalmente rilevabili — nodriver offre un'alternativa fully async basata direttamente sul Chrome DevTools Protocol (CDP) via websocket, senza Selenium e senza WebDriver. In questa guida mostriamo come integrare proxy residenziali ProxyHat per ottenere rotazione IP geolocalizzata e massima affidabilità in produzione.
Avviso legale: raccogli solo dati pubblici a cui hai diritto di accesso. Rispetta i
robots.txt, i Termini di Servizio e normative come il CFAA e il GDPR europeo. L'automazione non autorizzata di siti di terzi può violare la legge.
Perché nodriver sostituisce undetected-chromedriver
undetected-chromedriver (UC) è stato per anni lo standard de facto per lo scraping anti-bot in Python. Funziona patchando il binario Chrome e iniettando JavaScript per nascondere navigator.webdriver = true. Purtroppo, questo approccio ha limiti strutturali:
- Tracce CDP rilevabili: i runtime di Selenium lasciano firme nel
window.cdc_e in altri oggetti che i WAF moderni confrontano con known-bot signatures. - Sincrono: UC eredita l'API blocking di Selenium WebDriver, rendendo difficile gestire concorrenza oltre poche sessioni.
- Manutenzione stagnante: l'ultimo release significativo risale al 2023, mentre Chrome ha introdotto oltre 30 versioni CDP nuove.
nodriver, dello stesso autore (ultrafunkamsterdam), elimina l'intera layer Selenium. Comunica con Chrome via CDP nativo su websocket usando asyncio. Questo significa:
| Aspetto | undetected-chromedriver | nodriver |
|---|---|---|
| Protocollo | WebDriver via Selenium | CDP nativo via websocket |
| Modello concorrenza | Sincrono (threading) | asyncio nativo |
| navigator.webdriver | Patch JS post-avvio | Mai impostato (no Selenium) |
| Firma cdc_ | Presente (rimossa parzialmente) | Assente |
| Rotazione proxy nativa | No | No (usa browser_args) |
| Event-driven | No (WebDriver waits) | Sì (CDP events + hooks) |
La rimozione di navigator.webdriver e delle firme cdc_ non è cosmetica: i sistemi anti-bot come Cloudflare Turnstile e Imperva Incapsula confrontano decine di proprietà del runtime JavaScript. Quando navigator.webdriver è undefined invece di false (come UC lo imposta), la differenza è già un segnale. nodriver non imposta nulla perché non c'è Selenium a farlo.
Architettura di nodriver: CDP senza intermediari
nodriver avvia Chrome come processo figlio, apre una connessione websocket alla porta di debug remoto e invia comandi CDP direttamente. Non c'è un HTTP server WebDriver in mezzo. Questo riduce la latenza di ogni comando di circa 15–30ms rispetto a Selenium, e permette di sottoscrivere eventi CDP (Network.responseReceived, Page.domContentEventFired) invece di fare polling con WebDriverWait.
Componenti chiave
uc.start()— entry point asincrono che lancia Chrome con argomenti configurabili e restituisce un oggettoBrowser.Browser— gestisce il ciclo di vita del processo Chrome e le connessioni websocket. Supporta multipleTab.Tab— rappresenta una pagina/target CDP. Ogni tab è indipendente e può essere gestito conawait tab.get(url),await tab.find(),await tab.select().- Event hooks — callback registrabili via
tab.add_handler()per eventi CDP, sostituendo i pattern di wait espliciti.
Configurare un proxy nodriver con ProxyHat
nodriver non ha rotazione proxy integrata: passa gli argomenti del browser direttamente a Chrome. Il flag --proxy-server= è il modo idiomatico per instradare tutto il traffico del browser attraverso un proxy. Per i proxy autenticati (come quelli residenziali ProxyHat), Chrome non invia credenziali via flag: serve iniettarle via CDP usando Fetch.authRequired o, più semplicemente, incorporarle nell'URL del proxy quando il formato lo supporta.
Il gateway ProxyHat accetta credenziali embedded nell'URL del proxy. Ecco il pattern idiomatico:
import nodriver as uc
import urllib.parse
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "your_password"
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
# Costruisce l'URL del proxy con credenziali
proxy_url = f"http://{urllib.parse.quote(PROXY_USER)}:{urllib.parse.quote(PROXY_PASS)}@{PROXY_HOST}:{PROXY_PORT}"
browser = await uc.start(
browser_args=[
f"--proxy-server={proxy_url}",
"--disable-blink-features=AutomationControlled",
],
headless=False,
)
Perché proxy residenziali? I datacenter proxy usano range IP di provider cloud (AWS, OVH, DigitalOcean) che i WAF classificano come datacenter con un punteggio di rischio elevato. I proxy residenziali ProxyHat usano IP assegnati a ISP reali, con fingerprint di traffico coerente con un utente domestico. Quando un headful Chrome senza navigator.webdriver si presenta da un IP residenziale, il profilo combinato è significativamente più difficile da distinguere da traffico umano organico.
Per il geo-targeting granulare, ProxyHat supporta country e city nel campo username:
# Stati Uniti, sessione sticky
PROXY_USER = "user-country-US-session-abc123"
# Germania, Berlino
PROXY_USER = "user-country-DE-city-berlin"
# Rotazione per-request (cambia sessione ad ogni richiesta)
import uuid
PROXY_USER = f"user-country-US-session-{uuid.uuid4().hex[:12]}"
La sessione sticky mantiene lo stesso IP per tutta la durata della sessione — essenziale quando un sito richiede login o carrello persistente. La rotazione per-request, invece, minimizza la correlazione tra richieste per lo scraping SERP o price monitoring. Esplora tutte le locazioni disponibili per il geo-targeting.
Esempio completo: scraping di una pagina protetta
Vediamo un esempio runnable che lancia nodriver attraverso un endpoint residenziale US con sessione sticky, naviga una pagina protetta da Cloudflare, attende il challenge e estrae dati JSON dalla pagina.
import asyncio
import json
import urllib.parse
import nodriver as uc
async def scrape_protected_page(url: str) -> dict:
# Configurazione proxy ProxyHat residenziale
proxy_user = "user-country-US-session-abc123"
proxy_pass = "your_password"
proxy_url = (
f"http://{urllib.parse.quote(proxy_user)}:"
f"{urllib.parse.quote(proxy_pass)}@gate.proxyhat.com:8080"
)
browser = await uc.start(
browser_args=[
f"--proxy-server={proxy_url}",
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
],
headless=False, # headful per massima stealth
)
try:
tab = await browser.get(url)
# Attende che il challenge Cloudflare si risolva
# nodriver non usa WebDriverWait: usa select() con timeout
await tab.select("body", timeout=30)
# Attende un elemento specifico che indica contenuto caricato
await tab.find("script#__NEXT_DATA__", timeout=20)
# Estrae JSON embedded nella pagina
script_el = await tab.find("script#__NEXT_DATA__")
json_text = await script_el.get_attribute("innerHTML")
data = json.loads(json_text)
return data
finally:
browser.stop()
if __name__ == "__main__":
result = asyncio.run(scrape_protected_page("https://example.com/page"))
print(json.dumps(result, indent=2))
Nota l'uso di tab.select() e tab.find() con timeout — questi sono i sostituti idiomatici di WebDriverWait(driver, 10).until(EC.presence_of_element_located(...)). Non c'è polling attivo: nodriver usa le promise CDP internamente.
Gestione degli eventi CDP
Per intercettare risposte di rete senza dover interrogare il DOM, puoi registrare handler per eventi CDP:
async def on_response(event):
if event.get("mimeType") == "application/json":
body = await tab.send(
uc.cdp.network.get_response_body(request_id=event["requestId"])
)
print(f"JSON response: {body[:200]}")
tab = await browser.get("https://example.com")
tab.add_handler(uc.cdp.network.ResponseReceived, on_response)
# Naviga e lascia che gli handler raccolgano le risposte
await asyncio.sleep(5)
Questo pattern event-driven è impossibile con Selenium WebDriver standard e rappresenta uno dei vantaggi architetturali più significativi di nodriver per il nodriver async scraping.
Scaling: fleet headless in Docker con proxy per-context
Per scalare oltre un singolo browser, nodriver supporta tab concorrenti nello stesso processo e browser multipli in container separati. Il pattern consigliato combina:
- Tab concorrenti per domini diversi nello stesso browser (condividono il pool di connessioni).
- Container Docker separati per worker paralleli, ognuno con il proprio proxy assegnato.
- Graceful shutdown via signal handling per non lasciare processi Chrome orfani.
import asyncio
import signal
import nodriver as uc
async def worker(domain: str, proxy_user: str, proxy_pass: str):
proxy_url = f"http://{proxy_user}:{proxy_pass}@gate.proxyhat.com:8080"
browser = await uc.start(
browser_args=[f"--proxy-server={proxy_url}"],
headless=True,
)
try:
tab = await browser.get(f"https://{domain}")
await tab.select("body", timeout=15)
title = await tab.evaluate("document.title")
return {"domain": domain, "title": title}
finally:
browser.stop()
async def main():
domains = ["site1.com", "site2.com", "site3.com", "site4.com"]
tasks = []
for i, domain in enumerate(domains):
# Ogni worker usa una sessione proxy diversa
proxy_user = f"user-country-US-session-worker-{i}"
tasks.append(worker(domain, proxy_user, "your_password"))
results = await asyncio.gather(*tasks, return_exceptions=True)
for r in results:
if isinstance(r, Exception):
print(f"Error: {r}")
else:
print(r)
# Graceful shutdown
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, lambda: asyncio.create_task(cleanup()))
asyncio.run(main())
Containerizzazione
Per un fleet in produzione, usa Docker con Chrome headless e nodriver preinstallato. Un Dockerfile tipico:
FROM python:3.12-slim
RUN apt-get update && apt-get install -y \
chromium \
chromium-driver \
fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
ENV CHROME_PATH=/usr/bin/chromium
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
WORKDIR /app
CMD ["python", "-m", "scraper"]
Con 100 sessioni concorrenti su container separati, ciascuna con un proxy residenziale ProxyHat dedicato, puoi sostenere throughput di 500–800 richieste/minuto contro target moderatamente protetti, mantenendo un tasso di successo superiore al 90% con rotazione IP appropriata. Consulta il ProxyHat Docs per i limiti di concorrenza del tuo piano.
Errori comuni e edge case
1. Proxy authentication fallita
Chrome non invia credenziali proxy dal flag --proxy-server quando il proxy richiede auth challenge. La soluzione è usare il formato URL con credenziali embedded come mostrato sopra, oppure gestire Fetch.authRequired via CDP. ProxyHat supporta entrambi gli approcci.
2. Headless rilevato
Anche senza navigator.webdriver, Chrome headless ha fingerprint diversi (es. navigator.plugins.length === 0, WebGL vendor = "Google Inc."). Per target ad alta protezione, usa headless=False con Xvfb in Docker, oppure strumenti come nodriver in modalità headful virtuale.
3. Session sticky non rispettata
Se cambi il valore di session- tra le richieste, ProxyHat assegna un nuovo IP. Mantieni lo stesso identificatore di sessione per tutto il flusso che richiede lo stesso IP (login, checkout, navigazione multi-pagina).
4. Memory leak con tab multipli
Ogni tab aperto consuma ~50–150 MB di RAM. Chiudi i tab esplicitamente con await tab.close() e non accumularne centinaia nello stesso browser. Per workload intensivi, preferisci browser separati ogni 10–20 tab.
Quando NON usare un browser completo
nodriver è potente ma costoso: ogni istanza Chrome consuma 200–500 MB di RAM e ha un overhead di avvio di 2–5 secondi. Per molti casi d'uso, una combinazione di richieste HTTP dirette con curl_cffi (che impersona TLS fingerprint di Chrome) e proxy residenziali è 10–50x più veloce e più economica.
Usa nodriver quando:
- La pagina richiede esecuzione JavaScript complessa (SPA con rendering dinamico).
- Il sito usa challenge Cloudflare Turnstile o hCaptcha che richiedono un DOM reale.
- Serve interazione utente simulata (click, scroll, form submission con eventi reali).
Usa HTTP + curl_cffi + proxy quando:
- La pagina è HTML statico o JSON da un'API interna.
- Il target non ha protezione anti-bot avanzata.
- Serve massimo throughput (migliaia di richieste al secondo).
Per scenari di web scraping su larga scala, valuta entrambi gli approcci: usa nodriver per i target più protetti e HTTP diretto per il resto. Per il SERP tracking, spesso curl_cffi con proxy residenziali è sufficiente.
Key Takeaways
- nodriver elimina Selenium — comunica via CDP nativo, rimuovendo
navigator.webdrivere le firmecdc_alla radice.- Proxy via
--proxy-server— nodriver non ha rotazione nativa; usabrowser_argscon URL proxy ProxyHat contenente credenziali.- Residenziali > datacenter per stealth — IP ISP reali abbassano il risk score dei WAF molto più dei datacenter proxy.
- Event-driven > polling — usa
tab.add_handler()per eventi CDP invece diWebDriverWait.- Valuta HTTP + curl_cffi — non ogni target richiede un browser; il costo computazionale di Chrome è 10–50x superiore.
- Etica prima di tutto — rispetta
robots.txt, ToS, CFAA e GDPR. Automatizza solo dati a cui hai diritto di accesso.
Pronto a iniziare? Esplora i piani ProxyHat e configura il tuo primo scraper nodriver con proxy residenziali in pochi minuti.






