Scraping con nodriver: guida pratica ai proxy residenziali e all'architettura async CDP

Una guida completa allo scraping con nodriver, il successore async di undetected-chromedriver. Configura proxy residenziali, gestisci sessioni sticky e scala in Docker con esempi di codice pronti.

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

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 webdriver nel 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.enable detection, 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:

CaratteristicaResidenzialiDatacenterMobile
Reputazione IPAlta (ISP reali)Bassa (range noti)Altissima
Success rate su Cloudflare85-95%30-50%90-98%
Latenza media200-800ms50-100ms300-1200ms
Costo per GBMedioBassoAlto
Rotazione IPPer-request o stickyPer-requestPer-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 elemento
  • await tab.find(text) — cerca elementi per testo
  • await tab.evaluate(js) — esegue JavaScript nel contesto della pagina
  • tab.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:

Criterionodriver + proxy residenzialecurl_cffi + proxy residenziale
RAM per richiesta150-300 MB5-15 MB
Latency per richiesta2-5s (page load)200-800ms
Cloudflare JS challengeSuperabileNon superabile
Rendering JS dinamicoNo
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-server negli browser_args, ma l'autenticazione richiede l'iniezione via CDP Fetch.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() in finally per 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.

Pronto per iniziare?

Accedi a oltre 50M di IP residenziali in oltre 148 paesi con filtraggio AI.

Vedi i prezziProxy residenziali
← Torna al Blog