Se você está migrando do undetected-chromedriver para o nodriver, já percebeu que a arquitetura mudou radicalmente: não há Selenium, não há WebDriver e toda a comunicação acontece via Chrome DevTools Protocol (CDP) sobre websockets. Este guia mostra como fazer scraping com nodriver de forma idiomática — integrando rotação de proxies residenciais como uma camada de configuração nativa, não um hack.
Aviso legal: Este artigo aborda automação autorizada em dados públicos. O acesso não autorizado a sistemas pode violar a Computer Fraud and Abuse Act (CFAA) nos EUA e o GDPR na UE. Respeite robots.txt, Termos de Serviço e limites de taxa. Consulte advogado antes de operar em escala.
O que é scraping com nodriver e por que importa
Scraping com nodriver é a prática de automação de navegador usando a biblioteca Python nodriver, criada pelo mesmo autor do undetected-chromedriver, ultrafunkamsterdam. Ao contrário do Selenium, que usa o protocolo W3C WebDriver, o nodriver fala diretamente com o Chrome via CDP — o mesmo protocolo usado pelas DevTools do navegador. Isso elimina o atributo navigator.webdriver=true e várias outras assinaturas que sistemas como Cloudflare, Imperva e DataDome detectam.
Para engenheiros de scraping que precisam extrair dados de páginas protegidas, o nodriver async oferece três vantagens concretas:
- Zero WebDriver: o Chrome não é iniciado em modo automation, então
navigator.webdriverretornafalsepor padrão. - Assíncrono nativo: toda a API é
async/await, permitindo centenas de abas concorrentes em um único processo. - Controle fino de CDP: você pode interceptar rede, modificar headers e injetar scripts sem extensões.
No entanto, mesmo o navegador mais stealth pode ser bloqueado se o IP de saída for um datacenter conhecido. É aqui que o nodriver proxy entra: combinar a invisibilidade do CDP com IPs residenciais reais é a estratégia mais eficaz contra detecção em 2026.
Arquitetura do nodriver: por que remover WebDriver ajuda contra anti-bot
O Selenium WebDriver funciona iniciando o Chrome com a flag --remote-debugging-port e conectando um driver que controla o navegador via HTTP. O problema é que o Chrome, quando detecta automação via WebDriver, define navigator.webdriver=true, adiciona flags como --enable-automation e expõe variáveis CDP que sistemas anti-bot fingerprintam.
O nodriver segue um caminho diferente: ele usa uc.start() para lançar o Chrome com argumentos personalizados e conecta diretamente via websockets ao endpoint de debugging. Como não há driver intermediário, o navegador se comporta quase identicamente a uma sessão manual. A documentação oficial do Chrome DevTools Protocol confirma que CDP é a interface de debugging nativa — não um protocolo de automação.
Assinaturas eliminadas pelo nodriver
| Assinatura | Selenium/UC | nodriver |
|---|---|---|
navigator.webdriver | true (requer patch) | false nativo |
--enable-automation | Presente | Ausente |
Variável cdc_ no DOM | Presente | Ausente |
| Runtime.evaluate CDP detectável | Sim | Sim, mas sem padrão de driver |
Ainda assim, o nodriver não é invencível. Soluções como Cloudflare Turnstile ainda podem detectar padrões de comportamento, TLS fingerprinting e — crucialmente — reputação de IP. Um IP de datacenter com score baixo em serviços como IPQualityScore pode disparar um desafio mesmo com navegador perfeito.
API idiomática do nodriver: Browser, Tab e hooks de eventos
A API do nodriver é orientada a objetos assíncronos. O ponto de entrada é uc.start(), que retorna um objeto Browser. Cada aba é um Tab que você obtém e manipula com await. Não há WebDriverWait — em vez disso, você usa seletores e hooks de eventos.
Exemplo básico: iniciar e navegar
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 tab.select("h1") # aguarda elemento aparecer
heading = await tab.find("h1")
print(await heading.get_attribute("textContent"))
browser.stop()
asyncio.run(main())
O método tab.select() aguarda até que o seletor CSS esteja presente no DOM — é o equivalente idiomático ao WebDriverWait, mas nativo e não bloqueante. Para interagir, você encadeia await tab.find(), await element.click() e await element.send_keys().
Hooks de eventos em vez de polling
Em vez de fazer polling de mudanças de página, o nodriver expõe browser.add_handler() para eventos CDP como Network.responseReceived ou Page.frameNavigated. Isso permite interceptar respostas JSON sem precisar parsear HTML:
async def on_response(event):
if "api/products" in event.request.url:
body = await event.response_body
print(f"Capturado: {len(body)} bytes")
browser.add_handler(uc.cdp.network.ResponseReceived, on_response)
await tab.get("https://loja-protegida.com/catalogo")
Esse padrão é fundamental para scraping de APIs ocultas: muitos sites carregam dados via XHR/Fetch e expõem JSON estruturado que você pode capturar diretamente, sem renderizar todo o DOM.
Configurando nodriver proxy via browser_args
O nodriver não tem um parâmetro proxy= nativo. Em vez disso, você passa --proxy-server=http://gate.proxyhat.com:8080 em browser_args. O problema: o Chrome via CDP não exibe diálogo de autenticação de proxy, então credenciais devem ser passadas via URL ou injetadas via CDP.
A forma mais robusta é usar autenticação na URL do proxy (formato http://user:pass@host:port) em conjunto com --proxy-server. No ProxyHat, o usuário incorpora flags de geo e sessão:
| Parâmetro | Formato no username | Exemplo |
|---|---|---|
| País | user-country-US | Residencial EUA |
| Cidade | user-country-DE-city-berlin | Berlim, Alemanha |
| Sessão sticky | user-session-abc123 | IP fixo por ~30 min |
Por que proxies residenciais são essenciais para headful stealth
Um navegador nodriver sem proxy sai com o IP do seu servidor ou VPS. IPs de datacenter (AWS, DigitalOcean, OVH) têm alta probabilidade de estar em listas de bloqueio de anti-bot. Proxies residenciais usam IPs de ISPs reais — Comcast, AT&T, Deutsche Telekom — que aparecem como tráfego doméstico legítimo.
Em testes de mercado, taxas de sucesso em sites com Cloudflare Turnstile podem saltar de ~15% com datacenter para ~85%+ com residencial. A diferença é especialmente dramática em headless: sem IP residencial, o navegador stealth é quase irrelevante. Veja nossa página de localizações de proxy para cobertura geográfica.
Exemplo executável: nodriver + ProxyHat residencial
Abaixo está um exemplo completo que lança o nodriver através de um endpoint residencial dos EUA com sessão fixa, navega em uma página protegida e captura JSON via handler de rede. O proxy URL é construído a partir de credenciais ProxyHat:
import nodriver as uc
import asyncio
import urllib.parse
# Credenciais ProxyHat
PROXYHAT_USER = "user-country-US-session-abc123"
PROXYHAT_PASS = "sua_senha_aqui"
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = "8080"
# Monta a URL de proxy com autenticação
proxy_url = f"http://{urllib.parse.quote(PROXYHAT_USER)}:{urllib.parse.quote(PROXYHAT_PASS)}@{PROXY_HOST}:{PROXY_PORT}"
async def scrape_protegido():
browser = await uc.start(
headless=True,
browser_args=[
f"--proxy-server={proxy_url}",
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-dev-shm-usage",
],
)
captured = []
async def on_response(event):
url = event.request.url if hasattr(event, "request") else ""
if "/api/data" in url:
try:
body = await event.response_body
captured.append(body)
except Exception:
pass
browser.add_handler(uc.cdp.network.ResponseReceived, on_response)
tab = await browser.get("https://site-protegido.com/pagina")
await tab.select(".content-ready", timeout=20)
# Extrai conteúdo renderizado
content = await tab.evaluate("document.body.innerText")
print(f"Conteúdo: {content[:500]}")
print(f"Respostas de API capturadas: {len(captured)}")
browser.stop()
return captured
asyncio.run(scrape_protegido())
Notas importantes:
- O
--proxy-serveraceita a URL com credenciais embutidas, mas alguns builds do Chrome exigem injeção deFetch.authRequiredvia CDP. Se você ver erro 407, adicione um handlerAuthRequired. - A flag
session-abc123mantém o mesmo IP por aproximadamente 30 minutos, ideal para fluxos multi-página que exigem cookies consistentes. - Para rotação por requisição, omita
session-e cada nova conexão receberá um IP diferente do pool residencial.
Alternativa SOCKS5
Para casos que exigem menor latência ou tunelamento UDP, use o endpoint SOCKS5 na porta 1080:
socks5_proxy = f"socks5://{PROXYHAT_USER}:{PROXYHAT_PASS}@gate.proxyhat.com:1080"
browser = await uc.start(
browser_args=[f"--proxy-server={socks5_proxy}"],
)
Escalando nodriver: abas concorrentes, fleet em Docker e shutdown gracioso
Abas concorrentes em um único browser
Como o nodriver é assíncrono, você pode abrir múltiplas abas no mesmo Browser e aguardar todas com asyncio.gather. Isso é mais eficiente em memória que múltiplos processos Chrome:
async def scrape_url(browser, url):
tab = await browser.get(url)
await tab.select("body")
text = await tab.evaluate("document.title")
await tab.close()
return text
async def batch_scrape(urls):
browser = await uc.start(headless=True, browser_args=[f"--proxy-server={proxy_url}"])
tasks = [scrape_url(browser, u) for u in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
browser.stop()
return results
Em prática, 10–20 abas concorrentes por processo Chrome é um limite seguro. Acima disso, o consumo de CPU e RAM aumenta rápido — cada aba renderiza JavaScript e mantém um contexto V8 separado.
Fleet headless em Docker
Para escalar horizontalmente, containerize cada instância nodriver com seu próprio proxy. Um padrão comum é um docker-compose com N réplicas, cada uma com uma sessão proxy diferente:
# Dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y chromium
RUN pip install nodriver
COPY scraper.py .
CMD ["python", "scraper.py"]
No scraper.py, injete o identificador de sessão via variável de ambiente para que cada contêiner use um IP diferente:
import os
session_id = os.environ.get("SESSION_ID", "default")
proxy_user = f"user-country-US-session-{session_id}"
proxy_url = f"http://{proxy_user}:{PROXYHAT_PASS}@gate.proxyhat.com:8080"
Com Kubernetes ou Docker Swarm, você pode orquestrar 50+ pods, cada um com 10 abas — 500 sessões concorrentes, cada uma com IP residencial único. A página de preços do ProxyHat detalha planos por tráfego e concorrência.
Shutdown gracioso
O nodriver não fecha processos Chrome órfãos automaticamente em crashes. Use try/finally e signal handlers:
import signal
async def run_with_cleanup():
browser = await uc.start(browser_args=[f"--proxy-server={proxy_url}"])
def cleanup(*args):
browser.stop()
sys.exit(0)
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
try:
await do_work(browser)
finally:
browser.stop()
Quando NÃO usar um navegador completo
O nodriver é poderoso, mas caro: cada instância Chrome consome 200–500 MB de RAM e exige CPU para renderizar. Se a página-alvo não exige JavaScript para entregar dados, você pode obter 10x mais throughput com HTTP puro + curl_cffi, que imita TLS fingerprint do Chrome a nível de biblioteca:
from curl_cffi import requests
r = requests.get(
"https://api-publica.com/data",
impersonate="chrome120",
proxies={"https": "http://user-country-US:pass@gate.proxyhat.com:8080"}
)
print(r.json())
Use navegador completo quando:
- A página exige execução de JS para renderizar conteúdo (SPA com React/Vue).
- Há desafios CAPTCHA interativos que exigem comportamento humano.
- Você precisa capturar respostas de API injetadas via XHR pós-carregamento.
Use HTTP + curl_cffi quando:
- O endpoint retorna JSON diretamente e não valida headers de navegador.
- Você precisa de 1000+ requisições por segundo.
- O custo de infraestrutura é o fator decisivo.
Para casos de uso de web scraping em escala e rastreamento de SERP, combine ambas as abordagens: HTTP para volume, navegador para páginas problemáticas.
Erros comuns e edge cases
1. Esquecer de fechar abas
Abas não fechadas acumulam memória. Sempre chame await tab.close() após processar cada URL, senão o Chrome cresce até OOM em poucas horas.
2. Usar datacenter proxy com nodriver
Como mencionado, o navegador stealth não compensa IP de datacenter. Se você vir desafios Cloudflare consistentes, troque para residencial antes de ajustar mais flags do Chrome.
3. Timeout muito curto em select()
O tab.select() padrão pode aguardar apenas 10 segundos. Páginas pesadas com Cloudflare podem levar 15–25s para liberar conteúdo. Use timeout=30.
4. Não tratar AuthRequired do proxy
Alguns builds do Chrome ignoram credenciais na URL do proxy. Se você receber 407, registre um handler CDP Fetch.authRequired que injeta authChallengeResponse com suas credenciais. A documentação em docs.proxyhat.com cobre esse padrão.
Key Takeaways
- nodriver elimina WebDriver: CDP direto remove
navigator.webdrivere outras assinaturas, mas IP ainda é o fator #1 de detecção.- Proxy vai em browser_args: use
--proxy-server=http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080— sem código de rotação nativo.- Residencial > datacenter: taxas de sucesso saltam de ~15% para ~85%+ em sites com Turnstile.
- Abas concorrentes: 10–20 por processo é seguro; use Docker + sessões únicas para escalar a 500+.
- HTTP puro quando possível: curl_cffi + ProxyHat dá 10x mais throughput se JS não for necessário.
Conclusão e próximos passos
O nodriver representa a evolução natural do scraping de navegador em Python: assíncrono, sem WebDriver, com controle fino de CDP. Mas sem proxies residenciais, toda a stealth do navegador é desperdiçada. A combinação nodriver + ProxyHat residencial oferece o melhor dos dois mundos — invisibilidade de navegador e reputação de IP real.
Para começar, crie uma conta no dashboard ProxyHat, pegue suas credenciais e adapte o exemplo acima. Se você está migrando do undetected-chromedriver, a mudança de API é pequena e o ganho de estabilidade é significativo.






