Scraping avec nodriver : guide complet d'intégration proxy residential

Guide pratique pour remplacer undetected-chromedriver par nodriver (CDP natif, async) et l'associer à des proxies résidentiels ProxyHat. Architecture, middleware, scaling Docker et cas limites inclus.

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

Le scraping avec nodriver est devenu la référence pour les ingénieurs Python qui remplaçaient jusqu'ici undetected-chromedriver. nodriver supprime complètement la couche WebDriver/Selenium et pilote Chrome directement via le Chrome DevTools Protocol (CDP) sur WebSocket, ce qui élimine les fuites les plus couramment détectées par Cloudflare, Imperva et DataDome. Associé à un nodriver proxy résidentiel bien configuré, il devient possible de scraper des pages protégées sans se faire bloquer à la première requête.

Avertissement légal : le scraping doit porter sur des données publiques et autorisées. Aux États-Unis, le Computer Fraud and Abuse Act (CFAA) restreint l'accès non autorisé aux systèmes informatiques — voir la page du DOJ sur les cybercrimes. En UE, le RGPD s'applique aux données personnelles ; consultez les lignes directrices de l'EDPB. Respectez robots.txt, les conditions d'utilisation et les limites de débit raisonnables.

Scraping avec nodriver : pourquoi et comment ça change la donne

nodriver est l'évolution naturelle d'undetected-chromedriver, écrite par le même auteur (Ultrafunkamsterdam). Contrairement à Selenium, qui utilise le W3C WebDriver comme protocole de contrôle, nodriver parle directement le Chrome DevTools Protocol. Conséquence immédiate : plus aucun navigator.webdriver === true, plus de /webdriver dans les capabilities CDP, et donc moins de signaux que les bots-detection vendors cherchent.

Les protections anti-bot modernes se basent sur trois couches : empreinte navigateur (JS, canvas, WebGL), comportement (mouvements souris, timing), et réputation IP. nodriver traite les deux premières ; un nodriver proxy résidentiel traite la troisième. Sans proxy, votre IP datacenter est signalée dans 60 à 80 % des cas dès la première visite sur un site protégé par Cloudflare Turnstile.

Architecture : CDP natif, async, zéro WebDriver

nodriver repose sur trois choix techniques forts :

  • Async natif via asyncio et uc-browser — un fork léger de Chromium optimisé pour le scraping. Pas de threads bloquants.
  • CDP brut sur WebSocket — chaque commande (Page.navigate, Runtime.evaluate) est envoyée comme message JSON-RPC, pas via un pilote HTTP.
  • Pas de chromedriver — Chrome se lance avec --remote-debugging-port et nodriver se connecte dessus. Cela supprime la surface d'attaque typique de WebDriver.

Concrètement, l'objet Browser représente le processus Chrome, et chaque onglet est un Tab exposant les méthodes CDP. Les event hooks (tab.add_handler(CDP.event.Network.responseReceived, cb)) remplacent les WebDriverWait polling de Selenium.

Pourquoi ça aide contre Cloudflare et Imperva

Cloudflare détecte WebDriver via : (1) navigator.webdriver, (2) window.cdc_ (variables injectées par chromedriver), (3) les en-têtes webdriver dans les CDP capabilities. nodriver patche ces trois surfaces au lancement du binaire. Reste l'IP : un proxy datacenter type AWS 54.x.x.x reste flaggé par les listes de réputation. D'où la nécessité d'un proxy résidentiel.

L'API idiomatique : uc.start(), Browser, Tab, event hooks

Le point d'entrée est nodriver.start() (alias uc.start()), qui renvoie un objet Browser. Tout est async :

import nodriver as uc
import asyncio

async def main():
    browser = await uc.start(
        browser_args=[
            "--disable-blink-features=AutomationControlled",
            "--no-first-run",
        ],
    )
    tab = await browser.get("https://example.com")
    # find() retourne un élément, select() supporte les sélecteurs CSS
    heading = await tab.select("h1")
    print(await heading.get_attribute("textContent"))
    browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

Les méthodes clés à connaître :

  • browser.get(url) — ouvre un nouvel onglet et navigue.
  • tab.select(css) — attend et retourne le premier élément (avec timeout interne).
  • tab.find(text) — recherche par texte, utile pour les sites SPA.
  • tab.evaluate(js) — exécute du JS dans le contexte de la page.
  • tab.add_handler(event, callback) — hook CDP natif pour intercepter les réponses réseau.

Pour extraire du JSON d'une API XHR sans re-render, on peut capturer les réponses via un handler Network.responseReceived plutôt que de parser le DOM :

import nodriver as uc
import asyncio, json

async def main():
    browser = await uc.start()
    tab = await browser.get("https://target-site.com/dashboard")

    captured = []
    async def on_response(event):
        if "/api/transactions" in event.request.url:
            body = await tab.send(uc.cdp.Network.getResponseBody(event.response.network_id))
            captured.append(json.loads(body))

    tab.add_handler(uc.cdp.network.ResponseReceived, on_response)
    await asyncio.sleep(5)  # laisser le SPA charger
    print(captured[0])
    browser.stop()

asyncio.run(main())

Configurer un nodriver proxy via browser_args

nodriver n'a pas de rotation de proxy intégrée — c'est volontaire, l'auteur part du principe que c'est à l'utilisateur de gérer l'infrastructure réseau. On passe donc le proxy via l'argument Chrome --proxy-server= :

import nodriver as uc
import asyncio

PROXY_URL = "http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080"

async def main():
    browser = await uc.start(
        browser_args=[
            f"--proxy-server={PROXY_URL}",
            "--disable-blink-features=AutomationControlled",
        ],
    )
    tab = await browser.get("https://api.ipify.org?format=json")
    body = await tab.get_content()
    print(body)  # {"ip":"73.x.x.x"}  -- IP résidentielle US
    browser.stop()

asyncio.run(main())

Important : Chrome n'envoie pas l'authentification proxy dans --proxy-server pour les schémas HTTP/HTTPS standards. Pour un proxy authentifié, deux options :

  1. ProxyHat whitelisted IP — autorisez l'IP sortante de votre scraper dans le dashboard dashboard.proxyhat.com, puis utilisez un proxy sans mot de passe.
  2. Plugin proxy-auth — chargez une extension Chrome qui injecte les en-têtes Proxy-Authorization via Fetch.enable + authRequired.

La méthode whitelist est plus simple et plus performante (zéro overhead runtime). Voir docs.proxyhat.com pour la configuration.

Pourquoi résidentiel et pas datacenter pour nodriver

nodriver rend le navigateur indistinguable d'un Chrome réel côté JS. Mais l'IP reste le signal le plus discriminant pour Cloudflare : un 54.x.x.x AWS a un score de risque élevé dans les bases Threat Intelligence. Un proxy résidentiel renvoie une IP 73.x.x.x Comcast ou 92.x.x.x Orange, classée comme residential ISP. Sur un panel de 50 sites protégés testés, le taux de succès passe typiquement de 20 % (datacenter) à 85 %+ (résidentiel rotatif).

Exemple complet : nodriver + ProxyHat résidentiel US sticky

Voici un script autonome qui lance nodriver via un endpoint résidentiel US avec session sticky abc123, navigue sur une page protégée par Cloudflare, attend le challenge JS, et extrait un JSON caché dans un <script> :

import nodriver as uc
import asyncio, json, os

class ProxyHatConfig:
    GATEWAY = "gate.proxyhat.com"
    HTTP_PORT = 8080
    USER = os.environ["PROXYHAT_USER"]
    PASS = os.environ["PROXYHAT_PASS"]

    @classmethod
    def url(cls, country="US", session="abc123"):
        return (
            f"http://{cls.USER}-country-{country}-session-{session}"
            f":{cls.PASS}@{cls.GATEWAY}:{cls.HTTP_PORT}"
        )

async def scrape_protected():
    proxy = ProxyHatConfig.url(country="US", session="abc123")
    browser = await uc.start(
        browser_args=[
            f"--proxy-server={proxy}",
            "--disable-blink-features=AutomationControlled",
            "--no-first-run",
            "--lang=en-US",
        ],
    )
    tab = await browser.get("https://protected-target.com/products")

    # attendre que Cloudflare Turnstile disparaisse
    await tab.select(".product-card", timeout=30)

    # extraire le JSON embarqué
    raw = await tab.evaluate(
        "document.querySelector('script#__NEXT_DATA__')?.textContent || ''"
    )
    data = json.loads(raw)
    products = data["props"]["pageProps"]["products"]
    for p in products[:5]:
        print(p["name"], p["price"])

    browser.stop()

if __name__ == "__main__":
    asyncio.run(scrape_protected())

Le flag -session-abc123 garantit que toutes les requêtes de cette instance utilisent la même IP résidentielle pendant la session — indispensable pour les sites qui lient le cookie de challenge Cloudflare à l'IP. Pour un scraping massif sans sticky, retirez le flag et chaque requête tournera automatiquement.

Scaling : Tabs concurrents, flotte Docker, per-context proxy

nodriver est async, donc la concurrence est naturelle. Pour scaler, trois patterns :

1. Tabs concurrents dans un seul Browser

Un seul Chrome peut gérer 10–20 onglets actifs avant de saturer la mémoire. Idéal pour des dizaines de requêtes légères :

import nodriver as uc, asyncio

async def fetch(tab, url):
    t = await tab.get(url)
    title = await t.evaluate("document.title")
    return title

async def main():
    browser = await uc.start(
        browser_args=["--proxy-server=http://user-country-US:pass@gate.proxyhat.com:8080"]
    )
    urls = [f"https://example.com/page/{i}" for i in range(15)]
    tasks = [fetch(browser, u) for u in urls]
    results = await asyncio.gather(*tasks)
    print(results)
    browser.stop()

asyncio.run(main())

2. Flotte de conteneurs Docker, un proxy par contexte

Au-delà de 20 onglets, lancez plusieurs conteneurs Chrome. Chaque conteneur reçoit un session-id unique, donc une IP résidentielle distincte :

# Dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y chromium
RUN pip install nodriver
COPY scraper.py /app/scraper.py
WORKDIR /app
CMD ["python", "scraper.py"]
# docker-compose.yml (extrait)
services:
  scraper-1:
    build: .
    environment:
      - PROXYHAT_USER=user
      - PROXYHAT_PASS=pass
      - SESSION_ID=worker-1
      - COUNTRY=US
    deploy:
      replicas: 8

Avec 8 réplicas et 15 onglets par conteneur, vous atteignez ~120 sessions concurrentes, chacune sur une IP résidentielle distincte. Vérifiez votre quota ProxyHat avant de monter en charge — voir /fr/pricing.

3. Arrêt gracieux

Toujours intercepter SIGINT/SIGTERM pour fermer Chrome proprement, sinon les processus zombies s'accumulent :

import signal, nodriver as uc, asyncio

browser = None

async def main():
    global browser
    browser = await uc.start()
    # ... scraping logic ...

async def shutdown():
    if browser:
        browser.stop()

loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
    loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown()))

asyncio.run(main())

Quand NE PAS utiliser un navigateur complet

nodriver est lourd : un Chrome headful consomme ~300 Mo de RAM et ~500 ms de startup. Pour des APIs JSON publiques non protégées par challenge JS, un client HTTP + curl_cffi (qui émule TLS JA3/JA4 de Chrome) est 10 à 50× plus rapide :

Cas d'usagenodriverHTTP + curl_cffi
API JSON publique, pas de challengeOverkill✅ Idéal
Cloudflare Turnstile / Imperva✅ Nécessaire❌ Échoue
SPA avec rendu client✅ Nécessaire⚠️ Sauf si API cachée trouvée
Coût par requête~0,005 $~0,0001 $
Latence médiane~1,2 s~200 ms

Règle empirique : commencez par curl_cffi + proxy résidentiel ProxyHat sur le port 8080. Si vous obtenez une 403 avec challenge Cloudflare, passez à nodriver. Voir /fr/use-cases/web-scraping et /fr/use-cases/serp-tracking pour des patterns HTTP-only.

Erreurs courantes et cas limites

  • Oublier --disable-blink-features=AutomationControlled — sans ce flag, navigator.webdriver peut réapparaître selon la version de Chrome.
  • Utiliser un proxy datacenter pour nodriver — gaspillage : le navigateur est stealth mais l'IP vous trahit.
  • Ne pas gérer les sessions sticky — Cloudflare lie le cookie cf_clearance à l'IP ; si l'IP tourne à mi-session, le challenge recommence.
  • Timeout trop court sur tab.select() — un challenge Turnstile peut prendre 5 à 15 s ; mettez timeout=30.
  • Mélanger browser.stop() et browser.close()stop() tue le processus, close() ferme juste les onglets. Pour un scraper one-shot, utilisez stop().
  • Lancer en root dans Docker — Chrome refuse de démarrer en root sans --no-sandbox, ce qui est acceptable en conteneur isolé mais pas en production.

Setup ProxyHat et liens internes

Pour configurer votre nodriver proxy résidentiel :

  1. Créez un compte sur dashboard.proxyhat.com.
  2. Choisissez un plan résidentiel — voir /fr/pricing.
  3. Récupérez vos identifiants USER/PASS.
  4. Whitelistedz l'IP de votre scraper OU utilisez l'authentification username/password.
  5. Vérifiez les pays disponibles sur /fr/locations pour le géo-ciblage.
  6. Documentation complète : docs.proxyhat.com.

Points clés à retenir

  • nodriver supprime WebDriver et parle CDP natif — navigator.webdriver et les fuites CDP disparaissent.
  • Un nodriver proxy résidentiel est indispensable : le navigateur stealth ne sert à rien avec une IP datacenter flaggée.
  • Pas de rotation intégrée — passez le proxy via --proxy-server= et gérez les sessions via le flag -session-xxx dans le username ProxyHat.
  • Scaling : 15 onglets par Browser, plusieurs conteneurs Docker avec session-id uniques pour des IP distinctes.
  • Pour des APIs non protégées, curl_cffi + proxy résidentiel est 10–50× plus rapide et moins cher.
  • Respectez CFAA, RGPD, robots.txt et les ToS — scraping de données publiques autorisées uniquement.

FAQ

Qu'est-ce que le scraping avec nodriver ?

nodriver est un framework Python async qui pilote Chrome via le Chrome DevTools Protocol natif, sans Selenium ni WebDriver. Il remplace undetected-chromedriver en supprimant les fuites typiques (navigator.webdriver, variables cdc_) qui déclenchent les détecteurs anti-bot. Associé à un proxy résidentiel, il permet de scraper des pages protégées par Cloudflare ou Imperva.

Pourquoi un proxy est-il essentiel avec nodriver ?

nodriver masque l'empreinte navigateur, mais ne change pas votre IP. Les protections anti-bot notent la réputation IP : un datacenter AWS a un score de risque élevé. Un proxy résidentiel ProxyHat renvoie une IP ISP réelle (Comcast, Orange), classée faible risque, ce qui fait passer le challenge Cloudflare dans 85 %+ des cas contre 20 % en datacenter.

Quel type de proxy fonctionne le mieux avec nodriver ?

Les proxies résidentiels sont recommandés pour nodriver car ils offrent des IP d'ISP réelles non flaggées. Les proxies mobiles sont encore plus fiables (score de confiance maximal) mais plus coûteux. Les datacenter proxies sont à éviter pour les sites protégés. ProxyHat propose les trois types sur gate.proxyhat.com:8080 (HTTP) et :1080 (SOCKS5).

Comment éviter les blocages en scraping avec nodriver ?

Combinez : (1) --disable-blink-features=AutomationControlled, (2) proxy résidentiel avec session sticky pour conserver le cookie cf_clearance, (3) timeouts généreux (30 s) pour laisser Turnstile se résoudre, (4) rotation de session entre workers distincts mais pas au sein d'une même session, (5) respect des limites de débit (~1 req/s par IP).

nodriver est-il plus rapide que Selenium ?

Oui, significativement. nodriver évite la couche HTTP WebDriver et utilise directement CDP sur WebSocket, ce qui réduit la latence par commande de ~50 ms. De plus, l'async natif permet de gérer plusieurs onglets concurrents sans threads. Le démarrage reste cependant lent (~500 ms) car Chrome doit se lancer — pour des APIs simples, curl_cffi reste 10× plus rapide.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog