Se stai migrando da undetected-chromedriver a nodriver, probabilmente hai già incontrato blocchi Cloudflare, challenge Imperva o fingerprinting che Selenium non riesce a superare. Lo scraping con nodriver risolve la maggior parte di questi problemi eliminando del tutto il layer WebDriver e comunicando direttamente con Chrome tramite il Chrome DevTools Protocol (CDP) su websocket. In questa guida implementiamo nodriver con proxy residenziali ProxyHat, dalla configurazione base fino al scaling in Docker.
Nota legale: questa guida presuppone che tu stia raccogliendo dati pubblici autorizzati, nel rispetto dei Termini di Servizio dei siti target, del CFAA e del GDPR. ProxyHat non incoraggia lo scraping non autorizzato o l'elusione di misure di sicurezza senza consenso esplicito.
Perché lo scraping con nodriver funziona meglio di Selenium
nodriver (già noto come undetected_chromedriver v2) è una libreria Python completamente async sviluppata da ultrafunkamsterdam, lo stesso autore di undetected-chromedriver. A differenza di Selenium, che usa il protocollo WebDriver W3C e inietta navigator.webdriver = true nel DOM, nodriver:
- Non usa Selenium né WebDriver — nessuna traccia
webdrivernel navigator, nessuna estensione CDP rilevabile. - Comunica via CDP nativo su websocket, lo stesso protocollo usato da Chrome DevTools.
- È fully async — basato su
asyncio, con un event loop nativo per gestire tab multipli e callback eventi. - Patcha Chrome a runtime rimuovendo i leak CDP più comuni (
Runtime.enabledetection,cdc_variabili, e altro).
Queste differenze sono cruciali contro sistemi anti-bot come Cloudflare Turnstile e Imperva Incapsula, che combinano fingerprinting del browser con detection del protocollo WebDriver. Un browser Selenium genera pattern di traffico CDP distinguibili (es. chiamate Page.navigate ripetute con timing predicibile), mentre nodriver mantiene un profilo più simile a un utente reale che usa DevTools manualmente.
Architettura: zero WebDriver, CDP puro
Il cuore di nodriver è la classe Browser, che avvia un processo Chrome headless o headful e apre una connessione websocket al endpoint di debugging (--remote-debugging-port). Ogni tab è un oggetto Tab con metodi async come tab.get(), tab.select(), tab.find() e tab.evaluate(). Non esiste WebDriverWait: gli elementi vengono selezionati tramite query selector con retry built-in basati su asyncio.
Questo significa che ogni operazione è non-bloccante. Puoi aprire 10 tab contemporaneamente, attendere che ciascuno carichi contenuti diversi, e gestire popup o challenge in parallelo — tutto nello stesso event loop.
Configurazione del proxy in nodriver
nodriver non ha un sistema built-in per la rotazione proxy. Passi il proxy a Chrome tramite --proxy-server= negli browser_args, esattamente come faresti con Chrome normale. La sfida è l'autenticazione: Chrome non supporta credenziali proxy via command-line, quindi devi gestire l'auth tramite un header Proxy-Authorization iniettato via CDP, oppure usare un URL proxy con credenziali embedded (supportato solo per alcuni schemi).
Con ProxyHat, il modo più pulito è usare l'autenticazione nel username con il formato standard http://USER:PASS@gate.proxyhat.com:8080. Poiché Chrome --proxy-server non supporta credenziali nell'URL, usiamo l'API CDP Fetch.enable con authRequired per iniettare l'header di autenticazione. Ecco un middleware proxy idiomatico per nodriver:
import asyncio
import nodriver as uc
import base64
PROXY_GATEWAY = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "tua_password"
async def setup_proxy_auth(browser, tab):
"""Inietta Proxy-Authorization via CDP Fetch domain."""
auth_value = base64.b64encode(
f"{PROXY_USER}:{PROXY_PASS}".encode()
).decode()
async def on_auth_required(event):
await tab.send(uc.cdp.fetch.continue_with_auth(
request_id=event["requestId"],
auth_challenge_response={
"response": "ProvideCredentials",
"username": PROXY_USER,
"password": PROXY_PASS,
}
))
await tab.send(uc.cdp.fetch.enable(
handle_auth_requests=True,
patterns=[{"urlPattern": "*"}],
))
tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth_required)
async def main():
browser = await uc.start(
browser_args=[
f"--proxy-server=http://{PROXY_GATEWAY}:{PROXY_PORT}",
"--disable-blink-features=AutomationControlled",
],
headless=True,
)
tab = await browser.get("https://httpbin.org/ip")
await setup_proxy_auth(browser, tab)
await asyncio.sleep(2)
content = await tab.get_content()
print(content)
browser.stop()
if __name__ == "__main__":
asyncio.run(main())
Il flag --proxy-server=http://gate.proxyhat.com:8080 instrada tutto il traffico del browser attraverso il gateway ProxyHat. L'autenticazione viene gestita a livello CDP, intercettando le richieste authRequired e fornendo le credenziali ProxyHat. Questo è il pattern corretto per nodriver proxy — non un hack, ma l'estensione ufficiale del dominio Fetch di CDP.
Proxy residenziali vs datacenter per nodriver
Quando usi un browser headless o headful, l'IP del proxy è solo uno strato della tua identità. I sistemi anti-bot come Cloudflare e Imperva combinano fingerprinting del browser con reputazione IP. Un browser con navigator.webdriver = false ma con un IP datacenter noto (es. AWS, DigitalOcean) viene comunque flaggato con alta probabilità. Ecco perché i proxy residenziali sono essenziali per nodriver:
| Caratteristica | Residenziali | Datacenter | Mobile |
|---|---|---|---|
| Reputazione IP | Alta (ISP reali) | Bassa (range noti) | Altissima |
| Success rate su Cloudflare | 85-95% | 30-50% | 90-98% |
| Latenza media | 200-800ms | 50-100ms | 300-1200ms |
| Costo per GB | Medio | Basso | Alto |
| Rotazione IP | Per-request o sticky | Per-request | Per-request o sticky |
I proxy residenziali di ProxyHat offrono sessioni sticky fino a 30 minuti, che sono ideali per nodriver perché mantengono lo stesso IP durante un flusso di navigazione multi-pagina (login, browsing, checkout). La rotazione per-request è invece utile per scraping distribuito su molti siti. Puoi consultare tutte le locazioni disponibili e i piani tariffari sulla dashboard.
Esempio completo: nodriver con sessione sticky residenziale
Vediamo un esempio runnable che lancia nodriver attraverso un endpoint residenziale ProxyHat con sessione sticky, naviga una pagina protetta ed estrae dati JSON dalla pagina. Usiamo un helper per costruire l'URL del proxy con geo-targeting e sessione:
import asyncio
import nodriver as uc
import base64
import json
class ProxyHatConfig:
GATEWAY = "gate.proxyhat.com"
PORT = 8080
USERNAME = "tuo_username"
PASSWORD = "tua_password"
@staticmethod
def build_username(country="US", city=None, session=None):
parts = [f"country-{country}"]
if city:
parts.append(f"city-{city}")
if session:
parts.append(f"session-{session}")
return "-".join(parts)
@classmethod
def proxy_server_url(cls, country="US", session=None):
user = cls.build_username(country=country, session=session)
return f"http://{cls.GATEWAY}:{cls.PORT}", user, cls.PASSWORD
async def scrape_protected_page():
proxy_url, proxy_user, proxy_pass = ProxyHatConfig.proxy_server_url(
country="US", session="abc123"
)
browser = await uc.start(
browser_args=[
f"--proxy-server={proxy_url}",
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-dev-shm-usage",
],
headless=True,
)
page = await browser.get("https://example.com/protected-data")
# Auth via CDP
async def auth_handler(event):
await page.send(uc.cdp.fetch.continue_with_auth(
request_id=event["requestId"],
auth_challenge_response={
"response": "ProvideCredentials",
"username": proxy_user,
"password": proxy_pass,
}
))
await page.send(uc.cdp.fetch.enable(
handle_auth_requests=True,
patterns=[{"urlPattern": "*"}],
))
page.add_handler(uc.cdp.fetch.AuthRequired, auth_handler)
# Attendi il contenuto via selector
await page.select("#data-container", timeout=15)
# Estrai JSON embedded nella pagina
raw = await page.evaluate(
"document.querySelector('#data-container').textContent"
)
data = json.loads(raw)
print(f"Estratte {len(data)} righe")
browser.stop()
return data
if __name__ == "__main__":
results = asyncio.run(scrape_protected_page())
In questo esempio, session-abc123 garantisce che tutte le richieste escano dallo stesso IP residenziale per la durata della sessione. Il flag country-US instrada attraverso un ISP statunitense. Il metodo tab.select() attende che il selettore #data-container appaia nel DOM, con un timeout di 15 secondi — niente WebDriverWait, niente polling esplicito.
API idiomatica di nodriver: Tab, eventi e hook
La differenza più grande tra Selenium e nodriver è il modello di interazione. In Selenium usi driver.find_element() e WebDriverWait. In nodriver usi:
await tab.select(selector)— attende e ritorna un elementoawait tab.find(text)— cerca elementi per testoawait tab.evaluate(js)— esegue JavaScript nel contesto della paginatab.add_handler(event_type, callback)— registra handler per eventi CDP
Gli event hooks sono particolarmente potenti. Puoi registrare handler per Network.requestWillBeSent, Page.frameNavigated, o Runtime.consoleAPICalled e reagire in tempo reale. Per esempio, puoi intercettare risposte XHR e salvare JSON senza dover parsare il DOM:
async def intercept_api_responses(tab):
responses = []
async def on_response(event):
url = event["response"]["url"]
if "/api/" in url:
body = await tab.send(
uc.cdp.network.get_response_body(
request_id=event["requestId"]
)
)
responses.append({"url": url, "body": body})
await tab.send(uc.cdp.network.enable())
tab.add_handler(
uc.cdp.network.ResponseReceived,
on_response
)
return responses
Questo pattern è impossibile con Selenium standard senza estensioni. Con nodriver, intercettare risposte API è nativo e async — ideale per nodriver web scraping di SPA (Single Page Applications) che caricano dati via XHR/fetch.
Scaling: tab concorrenti e fleet in Docker
Per scalare lo scraping con nodriver, hai due approcci: tab multipli nello stesso browser (leggero, ma condividono il fingerprint) o browser multipli in container separati (isolato, ma più costoso in memoria). Un'istanza Chrome headless consuma circa 150-300 MB di RAM; con 100 tab concorrenti su un singolo browser, il consumo può superare 2 GB.
Pattern 1: tab concorrenti con proxy per-tab
import asyncio
import nodriver as uc
class ScraperPool:
def __init__(self, proxy_config, max_concurrent=5):
self.proxy_config = proxy_config
self.semaphore = asyncio.Semaphore(max_concurrent)
async def scrape_url(self, browser, url, session_id):
async with self.semaphore:
proxy_user = self.proxy_config.build_username(
country="US", session=session_id
)
tab = await browser.get(url)
await self._setup_auth(tab, proxy_user)
await tab.select("body", timeout=10)
content = await tab.get_content()
await tab.close()
return content
async def run(self, urls):
browser = await uc.start(
browser_args=[
f"--proxy-server=http://{self.proxy_config.GATEWAY}:8080",
],
headless=True,
)
tasks = [
self.scrape_url(browser, url, f"sess-{i}")
for i, url in enumerate(urls)
]
results = await asyncio.gather(*tasks, return_exceptions=True)
browser.stop()
return results
Pattern 2: fleet in Docker con per-context proxy
Per isolamento completo, esegui istanze nodriver separate in container Docker, ciascuna con un proxy e una sessione dedicati. Un orchestrazione tipica usa docker-compose con N repliche:
# docker-compose.yml
version: '3.8'
services:
scraper:
build: .
environment:
- PROXYHAT_USER=user-country-US-session-${SESSION_ID}
- PROXYHAT_PASS=tua_password
- TARGET_URL=https://example.com/data
deploy:
replicas: 10
restart: unless-stopped
mem_limit: 512m
Each container esegue un'istanza nodriver indipendente con la sua sessione ProxyHat sticky. Con 10 container a 512 MB ciascuno, consumi circa 5 GB di RAM e gestisci 10 pagine concorrenti con fingerprint isolati. Il graceful shutdown è fondamentale: usa browser.stop() in un blocco finally per evitare processi Chrome zombie:
async def safe_scrape(url):
browser = None
try:
browser = await uc.start(headless=True)
tab = await browser.get(url)
await tab.select("body", timeout=10)
return await tab.get_content()
except Exception as e:
print(f"Errore: {e}")
return None
finally:
if browser:
browser.stop()
Quando NON usare un browser completo
nodriver è potente ma costoso in termini di risorse. Se il sito target non usa challenge JavaScript complesse o fingerprinting aggressivo, un client HTTP con TLS fingerprinting è molto più efficiente. curl_cffi replica il TLS fingerprint di Chrome a livello HTTP, senza avviare un browser:
| Criterio | nodriver + proxy residenziale | curl_cffi + proxy residenziale |
|---|---|---|
| RAM per richiesta | 150-300 MB | 5-15 MB |
| Latency per richiesta | 2-5s (page load) | 200-800ms |
| Cloudflare JS challenge | Superabile | Non superabile |
| Rendering JS dinamico | Sì | No |
| Concorrenza su 8GB RAM | ~25-30 tab | ~500+ richieste |
La regola pratica: usa nodriver solo quando il sito richiede rendering JS o supera challenge anti-bot basate su browser fingerprinting. Per API pubbliche, SERP tracking, o pagine statiche, curl_cffi con proxy residenziali è 10-50x più efficiente. Consulta il nostro caso d'uso su web scraping e SERP tracking per pattern specifici.
Errori comuni e edge case
1. Dimenticare l'autenticazione proxy
Il flag --proxy-server di Chrome non supporta credenziali nell'URL. Se passi http://user:pass@gate.proxyhat.com:8080 a --proxy-server, Chrome ignorerà le credenziali e le richieste falliranno con 407 Proxy Authentication Required. Devi sempre gestire l'auth via CDP Fetch.enable come mostrato sopra.
2. Headless detection
Anche se nodriver patcha navigator.webdriver, alcuni siti rilevano HeadlessChrome nello User-Agent o testano navigator.permissions. Usa headless=False in produzione quando possibile, o configura un User-Agent custom via CDP Network.setUserAgentOverride.
3. Sessioni sticky troppo lunghe
Le sessioni ProxyHat mantengono lo stesso IP per circa 30 minuti. Se fai scraping intensivo con la stessa sessione per ore, l'IP può essere rate-limited o flaggato. Rota la sessione (cambia session-ID) ogni 10-15 minuti per distribuire il carico.
4. Processi Chrome zombie
Se lo script crasha senza browser.stop(), i processi Chrome rimangono attivi e consumano RAM. Usa sempre try/finally o atexit per garantire il cleanup. In Docker, imposta --shm-size=2g per evitare crash per shared memory insufficiente.
Key Takeaways
- nodriver elimina WebDriver e comunica via CDP nativo, riducendo i fingerprint rilevabili da Cloudflare e Imperva.
- Il proxy si configura via
--proxy-serverneglibrowser_args, ma l'autenticazione richiede l'iniezione via CDPFetch.enable— non nell'URL. - I proxy residenziali sono essenziali per nodriver: un browser stealth con IP datacenter viene comunque flaggato. Le sessioni sticky ProxyHat mantengono l'IP per 30 minuti.
- Scala con tab concorrenti per workload leggeri o container Docker isolati per fingerprint separati. Monitora RAM: 150-300 MB per istanza Chrome.
- Non usare un browser se non serve: curl_cffi con proxy residenziali è 10-50x più efficiente per siti senza challenge JS.
- Gestisci sempre il graceful shutdown con
browser.stop()infinallyper evitare processi zombie.
Per approfondire l'integrazione con i proxy ProxyHat, consulta la documentazione ufficiale e i nostri piani tariffari. Se hai bisogno di geo-targeting specifico, controlla le locazioni disponibili in oltre 150 paesi.






