Scraping mit nodriver: Voll asynchrone CDP-Automatisierung mit Residential Proxies
Wenn Sie bislang undetected-chromedriver eingesetzt haben, kennen Sie das Problem: Selenium-basierte Wrapper sind synchron, schwer zu skalieren und hinterlassen Spuren, die moderne Anti-Bot-Systeme wie Cloudflare und Imperva erkennen. Scraping mit nodriver bietet einen fundamental anderen Ansatz — keine WebDriver-Schicht, direkte Kommunikation über das Chrome DevTools Protocol (CDP) und eine vollständig asynchrone API, die sich nativ in Python-Event-Loops einfügt. In diesem Guide zeigen wir, wie Sie nodriver idiomatisch einsetzen und mit nodriver proxy-Konfiguration über ProxyHat residential IPs kombinieren, um auch stark geschützte Seiten zuverlässig zu scrapen.
Rechtlicher Hinweis: Web Scraping ist legal, solange Sie öffentliche Daten erfassen und die Nutzungsbedingungen der Zielseite respektieren. In den USA kann der Computer Fraud and Abuse Act (CFAA) bei unbefugtem Zugriff greifen; in der EU regelt die DSGVO (GDPR) die Verarbeitung personenbezogener Daten. Scrapen Sie nur Seiten, für die Sie autorisiert sind, oder die explizit öffentliche Daten bereitstellen.
Warum nodriver existiert: Die Architektur hinter nodriver undetected
nodriver (früher undetected_chromedriver v2) wurde von denselben Entwicklern erstellt, die schon den Vorgänger gepflegt haben. Der Kernunterschied: nodriver entfernt die gesamte Selenium/WebDriver-Schicht und spricht direkt über Websockets mit dem Chrome DevTools Protocol. Das bedeutet:
- Kein
navigator.webdriver-Flag: Da kein WebDriver-Protokoll verwendet wird, bleibt die Standard-Eigenschaftnavigator.webdriveraufundefined, was der häufigste Erkennungsvektor für Headless-Browser ist. - Keine CDP-Lecks: nodriver patcht bekannte CDP-Spuren, die von Anti-Bot-Systemen abgefragt werden — darunter
Runtime.evaluate-Fingerabdrücke undPage.addScriptToEvaluateOnNewDocument-Artefakte. - Voll asynchron: Jede Interaktion ist ein
await-Aufruf. nodriver async ermöglicht echte Nebenläufigkeit: Sie können dutzende Tabs parallel steuern, ohne Threads blockieren zu müssen.
Die Architektur ist einfach: uc.start() startet einen Chrome-Prozess und öffnet eine CDP-Websocket-Verbindung. Sie erhalten ein Browser-Objekt, von dem aus Sie Tabs (Tab-Objekte) erstellen und steuern. Jeder Tab ist eine eigenständige CDP-Session mit eigenem Kontext.
CDP vs. WebDriver: Warum es gegen Cloudflare hilft
Cloudflare und Imperva nutzen mehrere Heuristiken zur Bot-Erkennung. Eine der wichtigsten ist die Kombination aus navigator.webdriver === true und dem Vorhandensein von WebDriver-spezifischen Chrome-Flags. Da nodriver kein WebDriver-Protokoll spricht, fallen diese Checks weg. Laut der offiziellen CDP-Dokumentation kommuniziert der Browser nativ über JSON-Befehle auf der DevTools-Websocket-Schnittstelle — genau wie ein Entwickler in den Chrome DevTools.
Das allein reicht jedoch nicht. Ohne eine nodriver proxy-Lösung mit residential IPs wird Cloudflare Ihre Rechenzentrum-IP erkennen und eine Challenge auslösen. Mehr dazu unten.
Die idiomatische nodriver API
nodriver ist nodriver undetected und idiomatisch asynchron. Die wichtigsten Konzepte sind uc.start() für den Browser-Launch, Tab-Objekte für die Seitensteuerung und Event-Hooks statt WebDriver-Waits.
Browser und Tab-Objekte
import nodriver as uc
import asyncio
async def main():
browser = await uc.start(
headless=False,
browser_args=["--disable-blink-features=AutomationControlled"]
)
tab = await browser.get("https://example.com")
await asyncio.sleep(2)
# Element finden — kein WebDriverWait, sondern await tab.find()
heading = await tab.find("h1")
print(heading.text)
browser.stop()
asyncio.run(main())
Beachten Sie: tab.find() ist ein await-Aufruf, der das Element abwartet, sobald es im DOM erscheint. Es gibt kein implizites time.sleep oder WebDriverWait. Das ist die idiomatische Art, nodriver web scraping zu betreiben.
Event Hooks statt WebDriver Waits
nodriver nutzt CDP-Events anstelle von Polling. Sie können Handler registrieren, die bei bestimmten CDP-Events feuern:
import nodriver as uc
import asyncio
async def on_request(event):
if event.request and "api" in event.request.url:
print(f"API call detected: {event.request.url}")
async def main():
browser = await uc.start()
tab = await browser.get("https://example.com")
tab.add_handler(uc.cdp.network.RequestWillBeSent, on_request)
await asyncio.sleep(5)
browser.stop()
asyncio.run(main())
Dieser Ansatz ist effizienter als Polling und blockiert nicht den Event-Loop. Für nodriver async-Workflows ist dies der empfohlene Weg, auf dynamische Inhalte zu reagieren.
Proxy-Konfiguration: nodriver proxy mit ProxyHat
nodriver hat keine eingebaute Proxy-Rotation. Sie konfigurieren Proxies über Chrome-Befehlszeilenargumente, genauer über --proxy-server=. Das Problem: Chrome unterstützt keine Proxy-Authentifizierung über Befehlszeilenargumente. nodriver löst dies über CDP: Sie registrieren einen Fetch.authRequired-Handler, der die Anmeldedaten bereitstellt.
ProxyHat verwendet das Format USERNAME:PASSWORD@gate.proxyhat.com:8080. Für nodriver müssen Sie den Proxy-Server und die Authentifizierung separat handhaben:
import nodriver as uc
import asyncio
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "your_password"
async def auth_handler(event):
await event.tab.send(uc.cdp.fetch.continue_with_auth(
request_id=event.request_id,
auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
response="ProvideCredentials",
username=PROXY_USER,
password=PROXY_PASS,
)
))
async def main():
browser = await uc.start(
headless=False,
browser_args=[
f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}",
"--disable-blink-features=AutomationControlled",
]
)
tab = await browser.get("https://example.com")
# Auth-Handler für Proxy registrieren
tab.add_handler(uc.cdp.fetch.AuthRequired, auth_handler)
await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))
await asyncio.sleep(3)
content = await tab.get_content()
print(content[:500])
browser.stop()
asyncio.run(main())
Warum Residential Proxies unverzichtbar sind
Auch der beste Browser-Fingerprint nützt nichts, wenn die IP-Adresse aus einem bekannten Rechenzentrum-Block stammt. Cloudflare und Imperva unterhalten Datenbanken mit AS-Nummern (Autonomous System Numbers) von Cloud-Providern wie AWS, DigitalOcean und Hetzner. Eine IP aus AS14061 (DigitalOcean) wird fast immer als Bot eingestuft.
Residential Proxies verwenden IPs, die von ISPs an Privathaushalte vergeben werden. Sie erscheinen als reguläre Endnutzer und haben eine deutlich niedrigere Block-Rate. In der Praxis sehen wir Block-Raten von 40-60% mit Datacenter-IPs bei Cloudflare-geschützten Seiten, während residential IPs typischerweise unter 5% liegen. Die ProxyHat Locations bieten über 90 Länder zur Auswahl.
Praktisches Beispiel: Geschützte Seite scrapen mit ProxyHat
Hier ist ein vollständiges, ausführbares Beispiel, das nodriver über einen US-Residential-Proxy mit Sticky Session startet, eine geschützte Seite navigiert und JSON-Daten extrahiert:
import nodriver as uc
import asyncio
import json
class ProxyHatConfig:
HOST = "gate.proxyhat.com"
PORT = 8080
def __init__(self, country="US", session_id="abc123"):
self.username = f"user-country-{country}-session-{session_id}"
self.password = "your_proxyhat_password"
@property
def server_url(self):
return f"http://{self.HOST}:{self.PORT}"
def auth_challenge_response(self):
return uc.cdp.fetch.AuthChallengeResponse(
response="ProvideCredentials",
username=self.username,
password=self.password,
)
async def scrape_protected_page(url: str, proxy: ProxyHatConfig):
browser = await uc.start(
headless=False,
browser_args=[
f"--proxy-server={proxy.server_url}",
"--disable-blink-features=AutomationControlled",
"--no-first-run",
"--no-default-browser-check",
]
)
tab = await browser.get(url)
# Proxy-Auth über CDP Fetch.enable
await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))
async def on_auth_required(event):
await tab.send(uc.cdp.fetch.continue_with_auth(
request_id=event.request_id,
auth_challenge_response=proxy.auth_challenge_response()
))
tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth_required)
# Auf Seitenladung warten
await tab.wait_for("body")
await asyncio.sleep(2)
# JSON aus Script-Tag extrahieren
script_tag = await tab.find("script#__NEXT_DATA__")
if script_tag:
raw_json = await script_tag.get_attribute("innerHTML")
data = json.loads(raw_json)
print(json.dumps(data, indent=2)[:1000])
browser.stop()
return data
# Ausführung
proxy = ProxyHatConfig(country="US", session_id="sess-001")
result = asyncio.run(scrape_protected_page(
"https://example.com/protected-page",
proxy
))
Dieser Ansatz kombiniert nodriver undetected-Stealth mit ProxyHat residential IPs. Die session-abc123-Flag sorgt dafür, dass alle Anfragen über dieselbe IP laufen — wichtig für Seiten, die Session-Konsistenz erwarten. Wenn Sie eine neue IP benötigen, ändern Sie einfach die Session-ID. Dies ist die Kernstrategie für nodriver web scraping gegen Anti-Bot-Systeme.
Skalierung: Concurrent Tabs, Headless Fleet, Docker
Mehrere Tabs parallel
Da nodriver nodriver async ist, können Sie mehrere Tabs in einem Browser parallel steuern. Das ist ressourceneffizienter als mehrere Browser-Prozesse:
import nodriver as uc
import asyncio
async def scrape_tab(browser, url, proxy_config):
tab = await browser.get(url)
await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))
async def on_auth(event):
await tab.send(uc.cdp.fetch.continue_with_auth(
request_id=event.request_id,
auth_challenge_response=proxy_config.auth_challenge_response()
))
tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth)
await tab.wait_for("body")
title = await tab.find("title")
print(f"Title: {title.text}")
await tab.close()
async def main():
browser = await uc.start(
headless=True,
browser_args=["--proxy-server=http://gate.proxyhat.com:8080"]
)
urls = [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
]
tasks = [scrape_tab(browser, url, ProxyHatConfig()) for url in urls]
await asyncio.gather(*tasks, return_exceptions=True)
browser.stop()
asyncio.run(main())
Headless Fleet in Docker
Für Produktionssysteme empfiehlt sich eine Container-basierte Architektur. Jeder Container führt eine nodriver-Instanz mit eigenem Proxy-Kontext. Mit 2 GB RAM pro Browser-Context und 100 gleichzeitigen Sessions benötigen Sie etwa 200 GB RAM — oder Sie verteilen die Last über mehrere Worker.
Ein einfaches Dockerfile für nodriver:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
chromium \
chromium-driver \
fonts-liberation \
libnss3 \
libxss1 \
&& 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 scraper.py .
CMD ["python", "scraper.py"]
Per-Context Proxy-Zuweisung und Graceful Shutdown
Für Multi-Proxy-Setups können Sie jedem Browser-Kontext einen anderen Proxy zuweisen. In nodriver entspricht jeder Tab einem Browser-Kontext. Sie können verschiedene Proxy-Session-IDs verwenden, um verschiedene IPs zu rotieren:
async def scrape_with_rotation(urls):
browser = await uc.start(
headless=True,
browser_args=["--proxy-server=http://gate.proxyhat.com:8080"]
)
results = []
for i, url in enumerate(urls):
# Neue Session-ID = neue IP
proxy = ProxyHatConfig(
country="US",
session_id=f"rot-{i:04d}"
)
try:
data = await scrape_tab(browser, url, proxy)
results.append({"url": url, "status": "ok", "data": data})
except Exception as e:
results.append({"url": url, "status": "error", "error": str(e)})
browser.stop()
return results
Verwenden Sie try/finally oder einen Context-Manager, um sicherzustellen, dass browser.stop() immer aufgerufen wird — auch bei Ausnahmen. Ein offengebliebener Chrome-Prozess verbraucht typischerweise 300-500 MB RAM und blockiert den CDP-Port.
Wann Sie keinen Browser benötigen: HTTP mit curl_cffi
Nicht jedes Scraping-Szenario erfordert einen vollständigen Browser. Wenn die Zielseite keine JavaScript-Ausführung für die Datenladung benötigt, ist ein HTTP-Client deutlich schneller und billiger. nodriver ist ideal für Cloudflare-Turnstile, JavaScript-Rendering und interaktive Seiten — aber für einfache API-Calls oder statische HTML-Seiten ist es Overkill.
Eine leichtgewichtige Alternative ist curl_cffi, das TLS-Fingerprinting nutzt und mit ProxyHat kompatibel ist:
from curl_cffi import requests
proxy = "http://user-country-DE-session-001:password@gate.proxyhat.com:8080"
response = requests.get(
"https://httpbin.org/ip",
proxies={"http": proxy, "https": proxy},
impersonate="chrome120"
)
print(response.json())
Dieser Ansatz verbraucht etwa 50 MB RAM pro Request und ist 10-20x schneller als ein vollständiger Browser. Für nodriver web scraping ist der Browser nur dann gerechtfertigt, wenn die Zielseite aktiv JavaScript ausführt, um Inhalte zu laden oder Anti-Bot-Challenges zu bestehen.
| Kriterium | nodriver (Browser) | curl_cffi (HTTP) |
|---|---|---|
| RAM pro Session | 300-500 MB | ~50 MB |
| Latenz | 2000-5000 ms | 200-800 ms |
| Cloudflare Turnstile | Ja (mit residential Proxy) | Nein |
| JavaScript-Rendering | Ja | Nein |
| Skalierbarkeit | ~50-100 concurrent | ~1000+ concurrent |
Häufige Fehler und Edge Cases
1. Proxy-Authentifizierung vergessen
Der häufigste Fehler: --proxy-server wird gesetzt, aber die Authentifizierung fehlt. Chrome zeigt dann einen ERR_PROXY_AUTH_UNSUPPORTED-Fehler. Die Lösung ist der CDP Fetch.authRequired-Handler wie oben gezeigt.
2. Headless-Modus ohne Stealth-Anpassungen
Headless Chrome hat andere Eigenschaften als Headful Chrome — z.B. fehlt navigator.plugins. nodriver patcht einige davon, aber nicht alle. Testen Sie immer mit Fingerprint-Test-Seiten, um sicherzustellen, dass Ihr Setup nicht auffällt.
3. Zu viele gleichzeitige Tabs
Mehr als 10-15 Tabs pro Browser-Prozess führen zu Speicherdruck und CDP-Timeouts. Skalieren Sie horizontal mit mehreren Browser-Instanzen, nicht vertikal mit unendlich vielen Tabs. Bei 100 concurrent Sessions sollten Sie mindestens 4-8 Browser-Instanzen verteilen.
4. Fehlendes graceful shutdown
Wenn browser.stop() nicht aufgerufen wird, bleiben Chrome-Prozesse als Zombies zurück. Verwenden Sie immer try/finally oder ein async with-Pattern.
Key Takeaways
- nodriver entfernt die WebDriver-Schicht und kommuniziert direkt über CDP — das eliminiert
navigator.webdriverund andere Erkennungsvektoren.- Die nodriver async-API nutzt
awaitund Event-Hooks statt synchroner WebDriver-Waits. Das ermöglicht echte Nebenläufigkeit mit mehreren Tabs.- Proxy-Konfiguration erfolgt über
--proxy-server=und CDPFetch.authRequired-Handler, da Chrome keine Proxy-Authentifizierung über CLI-Argumente unterstützt.- Residential Proxies sind unverzichtbar — selbst der beste Browser-Fingerprint nützt nichts mit einer Datacenter-IP. ProxyHat residential IPs bieten über 90 Länder und Sticky Sessions.
- Für einfache HTTP-Anfragen ohne JavaScript-Rendering ist
curl_cffi10-20x schneller und verbraucht 10x weniger RAM.- Skalierung: Container-basierte Architektur mit 2 GB RAM pro Browser-Context,
try/finallyfür graceful shutdown.
Fazit
Scraping mit nodriver ist der natürliche nächste Schritt für Python-Entwickler, die undetected-chromedriver ersetzen möchten. Die CDP-basierte Architektur ist robuster gegen Bot-Erkennung, die asynchrone API ermöglicht effiziente Skalierung, und die Kombination mit ProxyHat residential Proxies sorgt für die nötige IP-Diversität. Beginnen Sie mit einem einzelnen Tab und Proxy, validieren Sie gegen Ihre Zielseite, und skalieren Sie dann schrittweise. Die ProxyHat Preise bieten flexible Optionen für jede Skalierungsstufe.
Weitere Ressourcen: Web Scraping Use Cases, SERP Tracking, und die ProxyHat Dokumentation.






