Si estás migrando de undetected-chromedriver a scraping con nodriver, ya conoces el problema: Cloudflare devuelve 403, Imperva lanza challenges interminables, y tu pipeline se rompe a las 2 a.m. nodriver es el sucesor async de ultrafunkamsterdam que elimina Selenium por completo y habla directamente con Chrome DevTools Protocol (CDP) sobre websockets. Esta guía muestra cómo integrarlo con proxies residenciales de ProxyHat para mantener tus scrapers en producción.
Aviso legal: El scraping debe limitarse a datos públicos y autorizados. En EE.UU., el CFAA penaliza el acceso no autorizado a sistemas protegidos. En la UE, el GDPR regula el tratamiento de datos personales. Consulta los términos de servicio de cada sitio antes de automatizar.
Por qué el scraping con nodriver resuelve los bloqueos de bots
El problema con Selenium y WebDriver es que dejan huellas detectables. Cada vez que Chrome se inicia bajo control de WebDriver, la propiedad navigator.webdriver se establece en true, y Chrome añade cabeceras CDP específicas que los sistemas anti-bot como Cloudflare e Imperva reconocen instantáneamente. El protocolo CDP es la interfaz nativa de depuración de Chromium, pero Selenium lo envuelve añadiendo capas detectables.
nodriver elimina esa capa. Al comunicarse directamente con CDP vía websockets, no inyecta artefactos de WebDriver. No hay navigator.webdriver = true, no hay cabeceras webdriver en las peticiones, y el navegador se comporta como una instancia manual. Esto es lo que lo convierte en la herramienta preferida para nodriver undetected — la evolución natural del ecosistema undetected-chromedriver.
Según la documentación del proyecto en GitHub, nodriver opera sin binarios adicionales más allá del propio Chrome/Chromium. La latencia de conexión CDP típica es de 50-100 ms, frente a los 200-500 ms de Selenium WebDriver. Esto significa pipelines más rápidos y menos tiempo de exposición a detección.
Comparación: nodriver vs undetected-chromedriver vs Selenium
| Característica | nodriver | undetected-chromedriver | Selenium WebDriver |
|---|---|---|---|
| Arquitectura | CDP directo (async) | WebDriver parcheado | W3C WebDriver |
| navigator.webdriver | No expuesto | Parcheado (detectable) | Visible |
| Latencia de conexión | 50-100 ms | 200-500 ms | 200-500 ms |
| Soporte async nativo | Sí (asyncio) | No | Parcial |
| Detección por Cloudflare | Muy baja | Media | Alta |
| Rotación de proxies integrada | No (config manual) | No | No |
Arquitectura de nodriver: CDP sin intermediarios
nodriver no es un wrapper de Selenium. Es un cliente CDP puro que abre un websocket al endpoint de depuración de Chrome (--remote-debugging-port). Cada Tab es un objeto que mapea directamente a un Target en CDP. No hay sesión WebDriver intermediaria, no hay HTTP /session, no hay JSON Wire Protocol.
Esto tiene implicaciones prácticas importantes:
- Menos superficie de detección: los scripts anti-bot que buscan artefactos de WebDriver no encuentran nada.
- Mayor velocidad: sin la capa HTTP de Selenium, cada comando viaja por websocket en milisegundos.
- Eventos nativos: puedes subscribirte a eventos CDP como
Network.responseReceivedoPage.domContentEventFiredsin polls. - Control granular: acceso directo a dominios CDP como
Network,Runtime,Page,Emulation.
API idiomática: uc.start(), Browser y Tab
La API de nodriver es completamente nodriver async. El punto de entrada es nodriver.start(), que devuelve un objeto Browser. Desde ahí, obtienes Tab objects y operas sobre ellos con await.
Los patrones clave son:
browser = await nodriver.start()— lanza Chrome y conecta vía CDP.tab = await browser.get(url)— navega y devuelve un Tab.element = await tab.find("selector", timeout=10)— busca elementos sin WebDriverWait.await tab.select("text")— interactúa con selectores.- Hooks de eventos:
tab.add_handler(CDP.event, callback)— responde a eventos CDP en tiempo real.
En lugar de WebDriverWait(driver, 10).until(...), nodriver usa await tab.find() con timeout nativo. El modelo es más limpio, más rápido y más idiomático para Python async moderno.
Configuración de proxy nodriver: el argumento --proxy-server
nodriver no incluye rotación de proxies integrada. Pasa la configuración del proxy a Chrome mediante browser_args en nodriver.start(). El flag --proxy-server= acepta un host:puerto, pero Chrome no soporta autenticación de proxy vía línea de comandos. Necesitas manejar la autenticación con un handler CDP Fetch.authRequired.
Para configurar un nodriver proxy con ProxyHat, el patrón básico es:
import nodriver as uc
import asyncio
async def main():
proxy_host = "gate.proxyhat.com:8080"
browser = await uc.start(
browser_args=[
f"--proxy-server=http://{proxy_host}",
]
)
tab = await browser.get("https://httpbin.org/ip")
await asyncio.sleep(3)
content = await tab.get_content()
print(content)
asyncio.run(main())
Pero esto no maneja autenticación. Para proxies autenticados de ProxyHat, necesitas interceptar el evento Fetch.authRequired y proporcionar credenciales:
import nodriver as uc
from nodriver.cdp import fetch
import asyncio
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "tu_password"
async def auth_handler(event: fetch.AuthRequired, tab):
await tab.send(fetch.continueWithAuth(
request_id=event.request_id,
auth_challenge_response=fetch.AuthChallengeResponse(
response="ProvideCredentials",
username=PROXY_USER,
password=PROXY_PASS,
)
))
async def main():
browser = await uc.start(
browser_args=[
"--proxy-server=http://gate.proxyhat.com:8080",
]
)
tab = await browser.get("about:blank")
await tab.send(fetch.enable(
handle_auth_requests=True,
patterns=[fetch.RequestPattern(url_pattern="*")]
))
tab.add_handler(fetch.AuthRequired, auth_handler)
tab = await browser.get("https://httpbin.org/ip")
await asyncio.sleep(3)
content = await tab.get_content()
print(content)
asyncio.run(main())
El flag --proxy-server de Chrome solo acepta host:puerto, no credenciales. Por eso el handler Fetch.authRequired es esencial cuando usas proxies residenciales autenticados de ProxyHat.
Por qué los proxies residenciales son esenciales para nodriver
Incluso con nodriver eliminando las huellas de WebDriver, tu IP sigue siendo visible. Un IP de datacenter (por ejemplo, AWS us-east-1) es trivialmente detectable por ASN. Cloudflare e Imperva mantienen bases de datos de ASN de datacenters y bloquean o desafían automáticamente. Los proxies residenciales de ProxyHat usan IPs de ISPs reales, lo que hace que el tráfico parezca de un usuario doméstico normal.
Los proxies residenciales de ProxyHat ofrecen:
- Rotación automática: cada petición puede salir por una IP diferente sin cambiar la configuración del navegador.
- Geo-targeting: especifica país y ciudad en el username (
user-country-US-city-new_york). - Sesiones sticky: mantén la misma IP durante una sesión completa con
session-abc123. - Pool masivo: cientos de miles de IPs residenciales reducen la probabilidad de rate limiting.
Consulta las ubicaciones disponibles y la página de precios para elegir el plan adecuado a tu volumen.
Ejemplo completo: scraping con nodriver y ProxyHat
Aquí tienes un ejemplo runnable que construye la URL del proxy con el formato de ProxyHat, lanza nodriver con una sesión residencial estadounidense, navega a una página protegida y extrae datos JSON embebidos:
import nodriver as uc
from nodriver.cdp import fetch
import asyncio
import json
def build_proxy_auth(country="US", session="abc123"):
"""Construye credenciales de proxy ProxyHat con geo-targeting."""
username = f"user-country-{country}-session-{session}"
password = "tu_password"
return username, password
async def auth_handler(event: fetch.AuthRequired, tab):
username, password = build_proxy_auth()
await tab.send(fetch.continueWithAuth(
request_id=event.request_id,
auth_challenge_response=fetch.AuthChallengeResponse(
response="ProvideCredentials",
username=username,
password=password,
)
))
async def scrape_protected_page(url: str):
proxy_host = "gate.proxyhat.com:8080"
browser = await uc.start(
browser_args=[
f"--proxy-server=http://{proxy_host}",
"--disable-blink-features=AutomationControlled",
],
headless=False,
)
tab = await browser.get("about:blank")
await tab.send(fetch.enable(
handle_auth_requests=True,
patterns=[fetch.RequestPattern(url_pattern="*")]
))
tab.add_handler(fetch.AuthRequired, auth_handler)
tab = await browser.get(url)
await tab.find("body", timeout=15)
await asyncio.sleep(2)
json_data = await tab.evaluate(
'document.querySelector(\'script[type="application/json"]\')'
'?.textContent || "{}"'
)
data = json.loads(json_data) if json_data else {}
print(f"Extraídos {len(data)} campos")
browser.stop()
return data
data = asyncio.run(scrape_protected_page(
"https://ejemplo-protegido.com/data"
))
Este patrón combina nodriver async con proxies residenciales para maximizar la tasa de éxito. La sesión abc123 mantiene la misma IP durante toda la navegación, lo que evita que Cloudflare detecte cambios de IP a mitad de sesión y bloquee el acceso.
Escalado: tabs concurrentes y flota en Docker
Para escalar el scraping con nodriver, necesitas manejar concurrencia correctamente. Cada Browser es un proceso Chrome independiente, pero puedes abrir múltiples Tab objects dentro de un mismo browser. Sin embargo, todos comparten la misma IP de proxy. Para IPs diferentes por contexto, necesitas browsers separados.
Patrón: un browser por proxy
import nodriver as uc
from nodriver.cdp import fetch
import asyncio
PROXY_ENDPOINTS = [
{"country": "US", "session": "sess-001"},
{"country": "DE", "session": "sess-002"},
{"country": "GB", "session": "sess-003"},
]
async def scrape_with_proxy(config, url):
username = f"user-country-{config['country']}-session-{config['session']}"
browser = await uc.start(
browser_args=[
"--proxy-server=http://gate.proxyhat.com:8080",
],
headless=True,
)
tab = await browser.get("about:blank")
await tab.send(fetch.enable(handle_auth_requests=True))
async def handler(event: fetch.AuthRequired, tab):
await tab.send(fetch.continueWithAuth(
request_id=event.request_id,
auth_challenge_response=fetch.AuthChallengeResponse(
response="ProvideCredentials",
username=username,
password="tu_password",
)
))
tab.add_handler(fetch.AuthRequired, handler)
tab = await browser.get(url)
await tab.find("body", timeout=15)
content = await tab.get_content()
browser.stop()
return content
async def run_fleet(url):
tasks = [scrape_with_proxy(c, url) for c in PROXY_ENDPOINTS]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
asyncio.run(run_fleet("https://ejemplo.com"))
Flota headless en Docker
Para producción, ejecuta cada browser en su propio contenedor Docker con Chrome headless. Un patrón común es usar docker-compose con N réplicas, cada una con un session ID único. Los recursos típicos por contenedor son 512 MB de RAM y 1 CPU core para scraping ligero; 2 GB y 2 cores para páginas con JavaScript pesado.
Considera un orquestador como Kubernetes o Docker Swarm para autoescalar basado en la tasa de éxito. Si la tasa de respuestas 200 OK cae por debajo del 90%, escala horizontalmente y rota session IDs para obtener IPs frescas del pool residencial de ProxyHat.
Graceful shutdown
nodriver maneja el cierre con browser.stop(), que cierra el proceso Chrome y limpia el websocket CDP. En producción, envuelve tu código en try/finally para garantizar que los browsers se cierren incluso si una excepción ocurre:
async def safe_scrape(url):
browser = None
try:
browser = await uc.start(
browser_args=["--proxy-server=http://gate.proxyhat.com:8080"],
headless=True,
)
tab = await browser.get(url)
await tab.find("body", timeout=15)
return await tab.get_content()
finally:
if browser:
browser.stop()
Cuándo NO usar un navegador completo
nodriver es potente, pero no siempre es la herramienta correcta. Si el sitio objetivo no requiere JavaScript para renderizar el contenido, un cliente HTTP como curl_cffi con proxies es 10-50x más rápido y consume 100x menos memoria. Una petición HTTP con curl_cffi tarda 200-500 ms y usa 20 MB de RAM; un navegador headless con nodriver tarda 3-8 segundos y usa 500 MB+.
Usa nodriver cuando:
- El sitio requiere JavaScript para renderizar contenido dinámico.
- Hay challenges de Cloudflare Turnstile o Imperva que necesitan un navegador real.
- Necesitas interactuar con elementos (clicks, scrolls, formularios).
Usa HTTP + curl_cffi cuando:
- El HTML está disponible sin JavaScript.
- Solo necesitas consumir una API JSON pública.
- El volumen es alto (>1000 requests/minuto) y el coste por request importa.
Para casos de uso de web scraping y SERP tracking, evalúa primero si necesitas un navegador antes de elegir nodriver.
Errores comunes y casos límite
1. Olvidar el handler de autenticación
El error más común es pasar --proxy-server sin configurar Fetch.authRequired. Chrome mostrará un diálogo de autenticación que nodriver no puede manejar automáticamente, y la página cargará indefinidamente. Siempre configura el handler antes de navegar.
2. Usar headless=True en sitios con detección avanzada
Algunos sitios detectan --headless=new de Chrome. Si experimentas tasas de éxito bajas, prueba con headless=False y un servidor X virtual (Xvfb) en Linux. La diferencia puede ser del 40% en tasa de éxito en sitios con detección agresiva.
3. No rotar session IDs
Una sesión sticky de ProxyHat mantiene la misma IP, pero si esa IP es bloqueada, necesitas un nuevo session ID. Implementa rotación de session IDs cada N requests o cuando detectes un 403/429. Un patrón robusto es generar un UUID por sesión y cambiarlo tras 50-100 requests.
4. No respetar robots.txt ni términos de servicio
Antes de scraping, verifica /robots.txt. Aunque no es legalmente vinculante en todos los países, demuestra buena fe y evita disputas. Consulta la documentación de ProxyHat para más detalles sobre uso ético y límites de concurrencia.
Key Takeaways
- nodriver elimina la huella de WebDriver: al usar CDP directo sin Selenium, evita
navigator.webdrivery cabeceras detectables por Cloudflare e Imperva.- Los proxies residenciales son obligatorios: sin IPs de ISP reales, los sistemas anti-bot detectan el ASN del datacenter y bloquean automáticamente.
- La autenticación de proxy requiere handler CDP: Chrome no acepta credenciales en
--proxy-server; usaFetch.authRequiredcon nodriver.- Una sesión sticky por browser: para mantener cookies y estado, usa
session-abc123en el username de ProxyHat.- Escalado = un browser por IP: para IPs diferentes, lanza browsers separados, no tabs en el mismo browser.
- Evalúa si necesitas navegador: HTTP + curl_cffi es 10-50x más rápido para sitios sin JS.
nodriver representa un salto cualitativo en nodriver web scraping: menos detección, mayor velocidad y un modelo async idiomático. Combinado con proxies residenciales de ProxyHat, obtienes una infraestructura robusta capaz de superar las protecciones anti-bot más agresivas. Empieza hoy configurando tu primer scraper con el código de esta guía y escala según tus necesidades.






