Browser Scraper
Servidor MCP local que controla o Chrome real via CDP bruto sem Runtime.enable. Snapshot de acessibilidade com refs clicáveis, Markdown que inclui shadow roots fechados e iframes de origem cruzada, ferramenta Cloudflare Turnstile. Gratuito, MIT, sem chave de API.
Documentação
Browser Scraper: o navegador stealth para web scraping e agentes de IA
Chrome não detectado para Node.js que lê e opera sites que bloqueiam bots. Use como biblioteca, CLI ou servidor MCP para Claude, Cursor e Codex. Ele transforma qualquer página, incluindo Shadow DOM fechado e iframes de origem cruzada, em Markdown limpo, dados estruturados ou um snapshot de acessibilidade pelo qual um agente pode navegar. Local, gratuito, MIT, traga seu próprio LLM.
import { scrape } from "@eurafaeldecarvalho/browser-scraper";
const page = await scrape({ url: "https://example.com", formats: ["markdown", "metadata"] });
console.log(page.tier); // "http" | "browser" | "stealth": the cheapest tier that worked
console.log(page.markdown); // clean, LLM-ready Markdown of the main content
# Give Claude Code a browser that gets through Cloudflare:
claude mcp add browser-scraper -- npx -y @eurafaeldecarvalho/browser-scraper mcp
@eurafaeldecarvalho/browser-scraper controla o Chrome real pelo protocolo DevTools (CDP) bruto. Ele nunca envia Runtime.enable, um dos sinais de automação mais amplamente identificados, e cada camada da identidade do navegador conta a mesma história. Sobre esse núcleo stealth, ele adiciona os recursos que cargas de trabalho de IA precisam: extração de Markdown e metadados, um snapshot de acessibilidade com referências clicáveis, um scrape() de chamada única que escala de HTTP simples para um navegador stealth apenas quando necessário, um servidor MCP e extração opcional com LLM usando qualquer modelo.
Por que este:
- Passa onde navegadores headless são bloqueados. Não há vazamento de
Runtime.enable, a persona (UA, Client Hints, WebGL, fuso horário, localidade) é coerente, e a entrada de mouse e teclado é semelhante à humana.solveTurnstile()lida com a caixa de seleção do Cloudflare. - Vê tudo.
find(),markdown()esnapshot()alcançam raízes shadow fechadas e iframes de origem cruzada, que o snapshot do Playwright e extratores estilo Readability não capturam. - Feito para agentes. O servidor MCP e a CLI funcionam sem instalação extra. Credenciais são digitadas por meio de placeholders
{{SECRET}}para que o modelo nunca as veja, uma lista de permissões restringe domínios, e o conteúdo da página é delimitado como dados não confiáveis. - LLM apenas onde compensa.
generateExtractor()faz o modelo escrever seletores CSS uma vez. Depois disso,run()extrai sem LLM, erunOrHeal()regenera os seletores quando o site muda. - Local e gratuito. Sem nuvem, sem chave de API, sem cobrança por página. Você traz o proxy e, opcionalmente, o modelo (OpenAI, Anthropic, Google, Ollama… via Vercel AI SDK, ou sua própria função).
Passa nos detectores de bot que as pessoas realmente usam
Com a configuração certa — um IP residencial/móvel limpo, além de uma GPU real (EC2 g4dn/g5) para alvos de hash de renderização — browser-scraper é feito para passar nos conjuntos de fingerprint e stacks anti-bot que bloqueiam automação headless:
| Detecção / conjunto de fingerprint | Fornecedor Anti-bot / WAF | ||
|---|---|---|---|
bot.sannysoft.com — 0 falhas | ✅ | Cloudflare (Turnstile) | ✅ |
| CreepJS — 0% headless, 0% stealth | ✅ | DataDome | ✅ |
bot-detector.rebrowser.net | ✅ | Kasada | ✅ |
| Pixelscan | ✅ | Akamai Bot Manager | ✅ |
| IPHey | ✅ | PerimeterX / HUMAN | ✅ |
| Browserscan | ✅ | Imperva (Incapsula) | ✅ |
| Incolumitas | ✅ | Shape / F5 | ✅ |
| Fingerprint.com | ✅ | AWS WAF Bot Control | ✅ |
Um ✅ significa que a camada de fingerprint + comportamento passa — não é uma garantia de bypass. Contra alvos difíceis, sua reputação de IP é o teto: um ASN de datacenter perde antes mesmo do JavaScript rodar, então roteie por um proxy residencial/móvel (veja A stack de produção).
browser-scrapernão é um resolvedor de CAPTCHA — para desafios de imagem reCAPTCHA v2 / hCaptcha / Arkose (FunCaptcha), combine com um serviço resolvedor. Ele mantém pontuações altas do reCAPTCHA v3 via entrada semelhante à humana e perfis aquecidos.
Os números foram obtidos em um Chrome desktop. Em uma Lambda sem GPU rodando chrome-headless-shell (
@sparticuz/chromium), as superfícies diferem — reexecute os conjuntos contra seu binário de produção real e trate isso como fonte da verdade (veja Implantação em nuvem).
Esta é uma ferramenta de precisão, não um bypass mágico — ela é honesta sobre suas limitações (veja Notas e limitações), e essa honestidade é exatamente o que torna as verificações verdes acima válidas.
Conteúdo
- Instalação
- Início rápido
- Para agentes de IA e LLMs — Markdown,
scrape(), snapshot + referências, servidor MCP, CLI, extração com LLM - O tour — o que faz, por exemplos
- Referência da API
- Stealth, em profundidade
- WebGL / GPU em hosts de nuvem
- A stack de produção — proxy · VPS · resolvedor
- Implantação em nuvem (Lambda vs EC2-GPU)
- Identidade e persistência
- WebRTC
- Alternativas — como se compara
- FAQ
- Notas e limitações
- Suporte
Instalação
npm install @eurafaeldecarvalho/browser-scraper
ou:
pnpm add @eurafaeldecarvalho/browser-scraper
Requer Node.js ≥ 20 e Chrome/Chromium. O preenchimento do histórico do perfil usa o node:sqlite embutido do Node (Node ≥ 22.5) ou o better-sqlite3 opcional; se nenhum estiver disponível, é simplesmente ignorado. Os helpers de LLM precisam de ai (e zod se você usar esquemas Zod), e apenas se você importar /ai.
Requer Node.js ≥ 20 e um Chrome/Chromium local (é encontrado automaticamente, ou aponte para um com chromePath). Para AWS Lambda, instale também o @sparticuz/chromium opcional — veja Implantação em nuvem.
Início rápido
import { Browser } from "@eurafaeldecarvalho/browser-scraper";
const browser = new Browser();
try {
const tab = await browser.newTab();
await tab.goto({ url: "https://example.com" });
const heading = await tab.find({ selector: "h1" });
console.log(await heading?.text());
await tab.screenshot({ path: "example.png" });
} finally {
await browser.close();
}
Essa é a forma completa da biblioteca: um Browser, um Tab e Elements que você encontra e age sobre eles. Tudo abaixo é detalhe sobre esses três.
Para agentes de IA e LLMs
Markdown limpo, metadados e links
const tab = await browser.newTab();
await tab.goto({ url: "https://shop.example.com/product/42" });
const { markdown, title, links, wordCount } = await tab.markdown({
mainContent: true, // drop nav/header/footer/cookie banners (default)
query: "price shipping", // optional: keep only the relevant blocks (BM25), saves tokens
links: "inline", // "inline" | "reference" | "none"
includeIframes: false, // true = also inline iframe documents, cross-origin included
});
const meta = await tab.metadata();
// { title, description, canonical, lang, image, openGraph, twitter, jsonLd: [...] }
// JSON-LD often already has the price, rating and availability: check it before you prompt.
markdown() lê a árvore DOM do próprio Chrome via CDP (DOM.getDocument com pierce). Nenhum script é injetado na página, e o conteúdo de raízes shadow fechadas é incluído na ordem de leitura, com slots compostos. Para HTML que você já tem, htmlToMarkdown(html) e extractMetadata(html) fazem o mesmo sem navegador.
scrape(): uma chamada, primeiro o nível mais barato
import { Browser, scrape } from "@eurafaeldecarvalho/browser-scraper";
const browser = new Browser({ proxy: process.env.PROXY }); // reuse it across URLs
const page = await scrape({
url,
browser,
formats: ["markdown", "metadata", "links", "html", "screenshot"],
strategy: "auto", // http → browser → stealth (or pin one tier)
http: { curlImpersonate: "curl_chrome131" }, // optional: real Chrome TLS for the http tier
});
page.tier; // which tier produced the result
page.attempts; // [{ tier: "http", ok: false, reason: "needs JavaScript (empty app shell)", ms: 4 }, …]
| Nível | O que executa | Usado quando |
|---|---|---|
http | Uma requisição, sem Chrome (fetch do Node, ou curl-impersonate para um JA3/JA4 Chrome genuíno) | Páginas estáticas, APIs |
browser | O Chrome stealth | A resposta HTTP foi bloqueada (403/429/503), um desafio, ou um shell de app JS vazio |
stealth | A mesma aba mais um aquecimento comportamental e uma resolução interativa de Turnstile | O navegador ainda vê uma página de desafio |
Uma página de desafio nunca é retornada como conteúdo: se todos os níveis forem bloqueados, scrape() lança um ScrapeError que carrega o attempts.
Snapshot de acessibilidade + referências (no que os agentes clicam)
console.log(await tab.snapshot());
// - main
// - heading "Sign in" [level=1]
// - textbox "Email" [ref=e3]
// - button "Continue" [ref=e4]
// - Iframe [ref=e5]
// - checkbox "Verify you are human" [ref=e6] ← cross-origin iframe, still addressable
await (await tab.byRef({ ref: "e3" })).type({ text: "me@example.com" }); // human-like typing
await (await tab.byRef({ ref: "e4" })).click(); // human-like mouse
A árvore de acessibilidade é calculada na árvore composta, então elementos dentro de raízes shadow fechadas aparecem e podem ser clicados. Passe { interactive: true } para obter apenas a lista plana de elementos clicáveis ou digitáveis.
Servidor MCP (Claude Code, Claude Desktop, Cursor, Codex…)
claude mcp add browser-scraper -- npx -y @eurafaeldecarvalho/browser-scraper mcp
Ou em qualquer configuração de cliente MCP (claude_desktop_config.json, .cursor/mcp.json, …):
{
"mcpServers": {
"browser-scraper": {
"command": "npx",
"args": ["-y", "@eurafaeldecarvalho/browser-scraper", "mcp", "--allowed-domains", "example.com", "--secrets", "/path/to/.env"]
}
}
}
Ferramentas:
| Ferramenta | O que faz |
|---|---|
navigate | Abre uma URL e retorna o snapshot |
snapshot | Estrutura de acessibilidade com [ref=…] |
click | Clica em um elemento por referência, com entrada semelhante à humana |
type | Digita em um elemento por referência, com entrada semelhante à humana |
press_key | Pressiona uma tecla |
go_back | Volta no histórico |
wait | Espera um número de segundos |
markdown | Conteúdo principal como Markdown, com um query opcional |
metadata | JSON-LD, OpenGraph e o restante dos metadados da página |
screenshot | Captura de tela PNG |
solve_turnstile | Resolve um Cloudflare Turnstile interativo |
scrape | Fetch de chamada única com escalonamento de nível |
check_egress | Mostra se seu IP de saída parece de datacenter |
Proteções:
--allowed-domains a.com,b.comrecusa navegação para outros lugares e reverte cliques que saiam da lista.--secrets .envpermite que o agente digite{{PASSWORD}}. O valor é preenchido no momento da digitação e redigido de tudo que é retornado.- O conteúdo da página sempre volta dentro de marcadores
<untrusted-page-content>. - Outras opções:
--proxy,--headful,--user-data-dir,--geo BR. O servidor também está listado para o Registro MCP (server.json), e uma Skill de Agente está incluída.
CLI
npx @eurafaeldecarvalho/browser-scraper markdown https://example.com --query pricing
npx @eurafaeldecarvalho/browser-scraper scrape https://example.com --format markdown,metadata,links --json
npx @eurafaeldecarvalho/browser-scraper snapshot https://example.com --interactive
npx @eurafaeldecarvalho/browser-scraper scrape https://example.com --format screenshot --out page.png
Extração com LLM: @eurafaeldecarvalho/browser-scraper/ai
Opcional, e nada extra é instalado a menos que você use. Passe um modelo Vercel AI SDK (npm i ai @ai-sdk/anthropic, ou openai, google, ollama…) ou sua própria função llm. Esquemas podem ser Zod ou JSON Schema simples.
import { extract, generateExtractor, Extractor, observe } from "@eurafaeldecarvalho/browser-scraper/ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const Product = z.object({ name: z.string(), price: z.number(), inStock: z.boolean() });
const model = anthropic("claude-sonnet-5-5");
// 1) One LLM call per page.
const product = await extract({ tab, schema: Product, model });
// input: "markdown" (default) | "snapshot" | "image" (screenshot for vision models)
// 2) The LLM writes the selectors ONCE; later pages cost nothing.
const extractor = await generateExtractor({ tab, schema: z.array(Product), model });
await extractor.save({ path: ".extractors/products.json" });
const again = await Extractor.load({ path: ".extractors/products.json", schema: z.array(Product), model });
const items = await again.run({ tab }); // no LLM, runs in the isolated world
const { data, via } = await again.runOrHeal({ tab }); // via: "selectors" | "healed" | "llm"
// 3) Find elements by description: the model only LOCATES, your code acts.
const [login] = await observe({ tab, model, instruction: "the login button" });
await login.element.click();
Sem modelo à mão? llm: async ({ system, prompt, schema, images }) => myClient.json(...) funciona com qualquer provedor ou um modelo local.
O tour
Uma olhada guiada nas peças que você usará mais, cada uma mostrada da maneira que você realmente usaria.
Atravesse Shadow DOM fechado e iframes de origem cruzada com um único find()
find() é o destaque. Dê a ele um seletor CSS e ele busca no documento principal, raízes shadow fechadas e iframes de origem cruzada — em uma única chamada. A caixa de seleção de captcha que vive três camadas dentro de um iframe sandboxed é encontrada exatamente da mesma forma que um <h1> de nível superior.
// Reaches into a closed shadow root or an OOP iframe automatically — no frame plumbing.
const checkbox = await tab.find({ selector: "input[type='checkbox']", timeout: 12_000 });
await checkbox?.click();
// Returns null on timeout instead of throwing, so branching is clean:
const banner = await tab.find({ selector: ".cookie-banner", timeout: 2_000 });
if (banner) await banner.click();
// Collect every match (also shadow- and iframe-aware):
const links = await tab.findAll({ selector: "a[href]" });
for (const link of links) console.log(await link.getAttribute({ name: "href" }));
Espere o momento certo
Páginas reais se estabilizam em etapas. Escolha a condição que corresponde ao que você realmente está esperando em vez de dormir e torcer.
// Navigation: same waitUntil names as Puppeteer — load | domcontentloaded | networkidle0 | networkidle2
await tab.goto({ url, waitUntil: "networkidle2", timeout: 45_000 });
// "ready" = visible AND interactable (enabled, not busy) — the state you want before a click
await tab.waitForSelector({ selector: "button.submit", state: "ready", timeout: 30_000 });
// Arbitrary page predicate
await tab.waitForFunction({ expression: "window.__APP_READY__ === true" });
// Whichever finishes first — selector appears, or a JS condition flips. Great for success-or-challenge forks:
const winner = await tab.race({
selectors: [".dashboard"],
jsFunctions: ["document.querySelector('.captcha') !== null"],
timeout: 30_000,
});
Interaja como um humano
Entradas são humanizadas por padrão — o realismo está na biblioteca, não no seu código.
const button = await tab.find({ selector: "#cta" });
await button?.click(); // cursor travels a Bézier path, then presses
await button?.click({ humanLike: false }); // opt out: dispatch straight at the target
await button?.click({ removeNewTabTarget: true }); // strip target/formtarget, stay in this tab
const input = await tab.find({ selector: "#email" });
await input?.type({ text: "me@example.com", clear: true }); // real keyDown/keyUp, lognormal timing
await input?.type({ text: "me@example.com", typos: true }); // opt-in: occasional misstroke + self-correct
// Ambient motion before a token-minting call (grecaptcha.execute) or a high-value submit:
// a "blind execute" with no prior pointer/scroll activity tanks a reCAPTCHA v3 score.
await tab.ambientActivity({ durationMs: 1200 });
type() e pressKey() enviam eventos keyDown/keyUp genuínos com keyCode/code corretos, então teclas nomeadas (Enter, Tab, ArrowDown) se comportam como um teclado real em vez de um toque sintético de value.
Controle a rede
Corte a largura de banda do proxy medido, mate rastreadores ou reescreva requisições — sem quebrar a página de uma forma que pareça bot.
// Block by HOST so the page still renders normally — safest near anti-bot/captcha,
// which read a broken page as a bot signal:
await tab.blockResources({ urls: ["*googletagmanager.com*", "*adobeaemcloud.com*"] });
// Or block whole resource types to trim bandwidth:
await tab.blockResources({ types: ["Image", "Media", "Font"] });
// Full control: inspect each request and decide:
tab.network.intercept({
pattern: "*",
resourceType: "Image",
handler: async (req) => {
if (req.url.includes("keep-me")) return req.continueRequest();
await req.abort({ reason: "BlockedByClient" });
},
});
// Just watch the traffic go by:
tab.network.on({ event: "response", handler: (res) => console.log(res.status, res.url) });
O bloqueio compõe-se limpo com um proxy autenticado: um único Fetch.enable carrega tanto as credenciais do proxy quanto suas regras de bloqueio, então nenhum sobrescreve o outro.
Fingerprint de navegador coerente (User-Agent, Client Hints, WebGL, fuso horário)
A defesa de fingerprint mais forte não é nenhum spoof único — é que nada contradiz nada. Defina a persona uma vez e cada camada segue.
const browser = new Browser({
geoCountry: "BR", // sets timezone + locale + Accept-Language together, in sync
spoofWebGL: true, // OS-coherent WebGL string derived from the UA (soft targets)
proxy: "http://user:pass@residential-proxy:8000",
});
// Or adjust a live tab — one call keeps language, navigator.languages,
// Accept-Language, and timezone consistent. Match it to your proxy's region:
await tab.emulateLocale({ locale: "pt-BR", timezone: "America/Sao_Paulo" });
Não sabe o país de saída do proxy antecipadamente? Defina autoGeo: true e o primeiro newTab() consulta através do proxy e configura a geolocalização para você.
Perfis persistentes e aquecimento
O reCAPTCHA v3 pontua reputação, e um perfil descartável não tem nenhuma. Mantenha uma identidade, em um IP fixo, e aqueça-a para que os servidores emitam cookies reais.
import { Browser, ProfileStore, warmProfile } from "@eurafaeldecarvalho/browser-scraper";
const store = new ProfileStore({ baseDir: "/mnt/efs/profiles" }); // EFS, or sync to S3 between runs
const browser = new Browser({
userDataDir: store.dirFor("identity-42"),
proxy: "http://user:pass@sticky-residential:8000",
geoCountry: "BR",
});
const tab = await browser.newTab();
await warmProfile(tab); // genuine navigations + ambient motion so cookies/history accrue
Uma identidade, um IP fixo, um fingerprint — um perfil aquecido atrás de um IP de datacenter rotativo não ganha nada.
Referência da API
Browser
new Browser({
chromePath: null,
headless: false,
userDataDir: null,
proxy: null, // http(s):// or socks5:// (auth only on http(s) — Chrome can't auth SOCKS)
extraArgs: [],
autoSeed: true,
// --- stealth / persona (all derived coherently from the resolved UA) ---
userAgent: null, // override the UA for the WHOLE persona (WebGL OS default,
// Client-Hints, platform) and apply it via --user-agent at launch.
// Keep the Chrome major matching the host build.
spoofWebGL: false, // OS-coherent WebGL identity (soft targets only; off on real GPU)
webglVendor: null, // explicit override (must match the UA's OS)
webglRenderer: null,
geoCountry: null, // ISO-2, e.g. "BR" => timezone + locale + Accept-Language together
autoGeo: false, // detect the proxy exit country at first newTab() and set geoCountry
timezone: null, // explicit overrides for the above
locale: null,
acceptLanguage: null,
hardwareConcurrency: null, // re-applied per worker session to stay coherent main↔worker
screen: null, // { width, height } => device-metrics + window.screen/outerWidth
windowSize: null, // { width, height } => --window-size (defaults to screen or 1920x1080)
evasions: true, // webdriver / Notification / screen init scripts
lambda: false, // adds --no-sandbox --disable-dev-shm-usage for containers
warnOnDirectEgress: true, // warn once when launching with no proxy
// --- Lambda / chrome-headless-shell (@sparticuz/chromium) ---
channel: null, // "headless-shell" => binary is chrome-headless-shell:
// never add --headless=new, own the software-WebGL flags,
// ensure HOME/XDG/user-data dirs, DBUS=/dev/null, and
// auto-apply the shell shims (window.chrome, connection).
chromium: null, // pre-resolved { executablePath, args, shellMode } from
// `await resolveSparticuz()`; implies channel:"headless-shell".
onStderr: null, // (chunk) => void : receive Chrome's stderr live. Even
// without it, stderr is captured and attached to the
// BrowserError on a launch timeout (real crash cause).
screenCoordOffset: null,// OPT-IN { x, y }: MouseEvent.screenX/screenY = clientX+x /
// clientY+y to counter the CDP screenX==clientX artifact.
// VERIFY on your exact binary first (a ~Sep-2025 Chromium
// fix may already report correct coords).
});
await browser.newTab({ url: "about:blank" });
await browser.checkEgress(); // optional: report exit IP ASN + hosting flag
await browser.close();
Lacuna de endurecimento conhecida (não abordada): a biblioteca expõe CDP sobre um TCP
--remote-debugging-portvinculado a127.0.0.1. Uma sondagem na página de mesma origem delocalhostnão pode ser totalmente bloqueada pela porta aleatória. Fechá-la exige trocar todo o transporte para--remote-debugging-pipe(fd 3/4), o que remove os endpoints HTTP/json/*e colapsa o modelo WebSocket por aba em uma única conexão multiplexada por sessão — uma reescrita de transporte profunda e isolada. Isso é deliberadamente deixado como uma mudança separada, em vez de ser entregue pela metade; é um vetor de baixa gravidade (porta aleatória + bind em localhost já mitigam varredura remota).
Aba
await tab.goto({ url, waitUntil: "load", timeout: 30_000 });
await tab.waitForNavigation({ waitUntil: "networkidle2", timeout: 30_000 });
await tab.find({ selector, timeout: 5_000 });
await tab.findAll({ selector });
await tab.waitForSelector({ selector, state: "attached", timeout: 30_000 });
await tab.waitForSelector({ selector, state: "ready", timeout: 30_000 });
await tab.waitForFunction({ expression, timeout: 30_000 });
await tab.race({ selectors: [".success"], jsFunctions: ["window.done === true"], visible: false, timeout: 30_000 });
await tab.evaluate({ expression: "document.title" });
// Read page-owned globals the isolated world can't see (the page's own window state,
// the live grecaptcha/turnstile objects). Opt-in; the isolated world is the default.
await tab.evaluate({ expression: "window.__APP_STATE__", world: "main" });
await tab.screenshot({ path: "shot.png" });
await tab.content();
await tab.sleep({ milliseconds: 2_000 });
// AI-ready content (no page JS; closed shadow DOM included)
await tab.markdown({ mainContent: true, query: "pricing", links: "inline", includeIframes: false });
await tab.metadata(); // OpenGraph, Twitter, canonical, JSON-LD
await tab.snapshot({ interactive: false }); // accessibility outline with [ref=eN]
await tab.byRef({ ref: "e4" }); // → Element (human-like click/type)
await tab.url();
await tab.title();
await browser.closeTab(tab); // closes the page target (tab.close() only disconnects)
// History navigation
await tab.back();
await tab.forward();
await tab.reload();
// Input helpers
await tab.pressKey({ key: "Enter" });
await tab.mouse.moveTo({ x: 200, y: 300 });
// Emulation / stealth
await tab.addInitScript({ source: "/* runs before page scripts */" });
await tab.setUserAgent({ userAgent: "..." });
await tab.setExtraHeaders({ headers: { "Accept-Language": "en-US" } });
await tab.setViewport({ width: 1280, height: 800 });
await tab.setGeolocation({ latitude: -23.55, longitude: -46.63 });
await tab.setTimezone({ timezoneId: "America/Sao_Paulo" });
await tab.setLocale({ locale: "pt-BR" });
// One call that keeps language + navigator.languages + Accept-Language + timezone
// consistent — match this to your proxy's region to avoid a geo mismatch.
await tab.emulateLocale({ locale: "pt-BR", timezone: "America/Sao_Paulo" });
await tab.bringToFront();
await tab.pdf({ path: "page.pdf" });
// Native dialogs (alert/confirm/prompt/beforeunload)
tab.onDialog(async (dialog) => {
await dialog.accept();
});
// Human-like ambient motion BEFORE a token-minting call (grecaptcha.execute) or a
// high-value submit — a "blind execute" with zero prior pointer/scroll activity
// tanks a reCAPTCHA v3 score.
await tab.ambientActivity({ durationMs: 1200 });
// Cloudflare Turnstile (INTERACTIVE checkbox): wait for the widget, human-click it,
// poll for the token. Built on the lib's own primitives (no third-party solver).
// For an INVISIBLE widget, read the token directly with turnstileToken(). Best
// behind a residential IP — the click can't rescue a datacenter-IP session.
const { solved, token } = await tab.solveTurnstile({ timeoutMs: 30_000 });
const t = await tab.turnstileToken(); // live cf-turnstile-response, or null
// Inject a warmed cookie bundle via CDP (encrypted with the live profile key —
// unlike the inert SQLite seeding). Each cookie needs a `domain` or `url`.
await tab.setCookies({ cookies: [{ name: "NID", value: "...", domain: ".google.com", secure: true }] });
// Surface a mid-run Chrome death as a typed, recoverable error for THIS run
// (in-flight CDP commands reject immediately instead of hanging to a 30s timeout).
tab.onDisconnect((error) => { /* abort this run gracefully */ });
waitUntil segue os mesmos nomes públicos usados pelo Puppeteer: load, domcontentloaded, networkidle0 e networkidle2.
Todos os timeouts e pausas são expressos em milissegundos.
waitForSelector({ state: "visible" }) espera apenas pela visibilidade.
waitForSelector({ state: "ready" }) espera pela visibilidade mais interatividade, incluindo verificações de habilitado/não ocupado para estados comuns de botões.
Rede / bloqueio de requisições
// Drop bandwidth-heavy resources (handy to cut metered-proxy traffic).
// Block by HOST to keep the page rendering normally — safest around anti-bot /
// captcha, which read a broken page as a bot signal:
await tab.blockResources({ urls: ["*adobeaemcloud.com*", "*googletagmanager.com*"] });
// Or block whole resource types (Image, Media, Font, Stylesheet). With no proxy
// auth, only the listed types pause at the CDP level — scripts/XHR/documents are
// never intercepted:
await tab.blockResources({ types: ["Image", "Media"] });
// Lower-level equivalents:
await tab.network.block({ patterns: ["*.jpg", "*ads*"], resourceTypes: ["Font"] });
tab.network.intercept({
pattern: "*",
resourceType: "Image",
handler: async (req) => {
if (req.url.includes("keep-me")) return req.continueRequest();
await req.abort({ reason: "BlockedByClient" });
},
});
// Observe traffic:
const onResponse = tab.network.on({ event: "response", handler: (res) => console.log(res.status, res.url) });
// Detach handlers. Drain them before teardown so none fires (and issues a CDP
// command the closing socket would reject) while the connection is going down:
tab.network.off({ event: "response", handler: onResponse });
tab.network.removeAllListeners();
O bloqueio de requisições compõe-se com um proxy autenticado: um único Fetch.enable
carrega tanto o tratamento de credenciais do proxy quanto as regras de bloqueio, então nenhum
substitui o outro.
Via rápida HTTP (browserless)
Para endpoints baratos / desprotegidos (APIs JSON, HTML estático), você nem sempre precisa de uma
aba — browser.createHttpClient() emite uma única requisição com a
persona do navegador (User-Agent resolvido + Accept-Language) e proxy, sem abrir o Chrome:
const http = browser.createHttpClient({
// Optional: a curl-impersonate binary gives a REAL Chrome TLS/JA3/JA4 + HTTP/2
// fingerprint through the proxy. WITHOUT it, the Node `fetch` fallback is used
// (coherent headers, but Node's TLS — fine for unprotected targets, a tell for
// hard ones) and a proxy is REFUSED rather than leaking the host TLS/IP.
curlImpersonate: "curl_chrome131",
});
// Reuse a warmed session by passing cookies pulled from a tab:
const cookies = Object.fromEntries((await tab.getCookies()).map((c) => [c.name, c.value]));
const res = await http.get("https://api.example.com/data", { cookies });
console.log(res.status, res.impersonated, res.body);
É um complemento, não um substituto furtivo: para qualquer coisa atrás de um anti-bot real, use uma aba.
Elemento
await element.click();
await element.click({ humanLike: false });
await element.click({ removeNewTabTarget: true });
await element.hover();
await element.type({ text: "hello", clear: true });
await element.pressKey({ key: "Enter" });
await element.text();
await element.innerHtml();
await element.getAttribute({ name: "href" });
await element.setAttribute({ name: "data-test", value: "1" });
await element.getProperty({ name: "value" });
await element.boundingBox();
await element.isVisible();
await element.isChecked();
await element.selectOption({ value: "b" });
await element.setInputFiles({ files: ["/path/to/file.png"] });
await element.screenshot({ path: "element.png" });
click() é semelhante ao humano por padrão: move o cursor até o elemento ao longo de um caminho Bézier antes de pressionar.
click({ humanLike: false }) despacha o pressionar/soltar diretamente na posição alvo resolvida.
click({ removeNewTabTarget: true }) remove target e formtarget antes de clicar para que a ação permaneça na aba atual quando possível.
type() e pressKey() despacham eventos reais de keyDown/keyUp (keyCode/code corretos), então teclas nomeadas como Enter, Tab e ArrowDown funcionam.
type({ typos: true }) (opt-in, desligado por padrão) injeta erros de digitação ocasionais em teclas adjacentes + autocorreção (Backspace), que os avaliadores comportamentais ponderam como humano. Deixe-o desligado para campos cujo valor você afirma no meio da digitação ou que validam a cada tecla pressionada.
Furtividade, em profundidade
Pesquisa de base: como reCAPTCHA v3/Enterprise e os fornecedores de anti-bot (Cloudflare, DataDome/Picasso, PerimeterX/HUMAN, Kasada, Akamai, Imperva, securiti.ai, Arkose, hCaptcha) realmente detectam bots — com a taxonomia de camadas de sinal e a justificativa de design por trás de cada escolha abaixo — está documentada em
docs/antibot-and-captcha-research.md.
A biblioteca é construída para minimizar os sinais que os fornecedores de anti-bot (Cloudflare, DataDome, Kasada) procuram:
- Sem
Runtime.enable. Todo JavaScript é executado por meio dePage.createIsolatedWorld+Runtime.evaluate, comandos simples que não habilitam o domínio Runtime. A invalidação de contexto é tratada viaPage.frameNavigatede novas tentativas de contexto obsoleto. (Calibração: a sonda mais conhecida — o getter deError.stackacionado durante a serialização do inspetor — foi amplamente desarmada pelos patches de maio de 2025 do V8, então isso não é mais o único sinal dominante que já foi; mas evitarRuntime.enableainda derrota os outros sinais dependentes do domínio Runtime, por exemplo,Runtime.consoleAPICalled, então permanece um invariante correto.) - User-Agent limpo + client hints. Em
--headless=new, o Chrome injetaHeadlessChromeno UA esec-ch-ua. A biblioteca o remove e aplicauserAgentMetadatacorrespondente para quenavigator.userAgentDatapermaneça consistente. - Nomes de mundo isolado por aba, aleatorizados (um token
u…aleatório, não um marcador fixo como uma constanteutilou o__playwright_utility_world__do Playwright) para que o nome do mundo não possa ser fixado como uma assinatura estática entre sessões se vazar (por exemplo, via um quadro de pilhaError). - Flags de lançamento evitam opções cuja mera presença é uma impressão digital (
--disable-popup-blocking,--disable-component-update,--disable-extensions,--enable-automation). - Entrada semelhante à humana. Movimentos de mouse seguem um perfil de velocidade min-jerk com tempo dimensionado pela distância, tremor em voo e overshoot balístico + correção; a digitação usa tempos de permanência/voo lognormais com sobreposição ocasional de teclas.
tab.mouse.idle()adiciona deriva de cursor ambiente não periódica (útil antes de umgrecaptcha.execute()). - Persona coerente. String de UA, Client-Hints (
platform,platformVersion,fullVersionListreal,architecture), identidade WebGL e geo (fuso horário/localidade/Accept-Language) são todos derivados de um único User-Agent resolvido para que as camadas não possam se contradizer — uma incompatibilidade entre camadas é o sinal de maior peso.
Um pequeno conjunto de patches JS condicionais e defensivos é injetado por padrão (alterne com evasions: false): navigator.webdriver é forçado a um getter false presente somente se a flag de lançamento não o fez; Notification.permission é reconciliado com navigator.permissions.query (o PermissionStatus real é retornado com apenas seu state mapeado, então default → o prompt válido); e window.screen/outerWidth são normalizados em uma geometria de janela maximizada coerente quando um screen/windowSize é definido. Cada patch mantém seu toString() nativo e não faz nada quando o valor já estava correto (corrigir demais um valor correto é em si um sinal). navigator.hardwareConcurrency é fixado via Emulation.setHardwareConcurrencyOverride; essa substituição de sessão de página não alcança alvos de worker, então a biblioteca o reemite na própria sessão CDP de cada worker (e retoma o worker, que de outra forma ficaria pausado sob auto-attach) — mantendo navigator.hardwareConcurrency coerente entre o thread principal e Web/Shared/Service Workers, já que uma incompatibilidade principal-vs-worker é um sinal mais forte do que o valor em si. deviceMemory é deliberadamente não falsificado (não tem substituição por alvo, então um patch apenas no mundo principal dessincronizaria dos workers). Adicione o seu próprio com tab.addInitScript({ source }).
Auditado contra bot-detector.rebrowser.net (sem runtimeEnableLeak, sem navigatorWebdriver, execução em mundo isolado), bot.sannysoft.com (0 falhas) e CreepJS (0% headless, 0% furtividade).
Meça novamente no seu binário de produção. Esses resultados foram obtidos em um Chrome de desktop. Em um Lambda sem GPU executando chrome-headless-shell (
@sparticuz/chromium), as superfícies diferem — ausência dewindow.chrome/navigator.plugins, WebGL SwiftShader, codecs — que é exatamente o quechannel:'headless-shell'contorna. Reexecute as suítes contra o binário real do sparticuz e trate isso como a fonte da verdade.
WebGL / GPU em hosts de nuvem
Na sua máquina, o WebGL relata sua GPU real. Em um servidor de nuvem sem GPU, o Chrome recai para SwiftShader/llvmpipe, e SwiftShader / llvmpipe no renderizador é um sinal headless conhecido. Mas a falsificação tem duas arestas afiadas que você deve respeitar:
- Nunca reivindique um SO ao qual o renderizador não pode pertencer. Uma string de renderizador
Direct3D11/D3D11existe somente no Windows. Emiti-la sob um UA Linux é uma combinação impossível que DataDome/Picasso bloqueia duramente — estritamente pior do que a string de software honesta.spoofWebGL: trueagora deriva um padrão coerente com o SO do UA resolvido (Linux→Mesa/ANGLE OpenGL, Windows→D3D11, macOS→Metal); nunca colocará uma GPU Windows em um UA Linux. Se você passar umwebglRendererexplícito, a biblioteca avisa quando seu backend contradiz o SO do UA (uma string D3D11 sob Linux/macOS, uma string Mesa/SwiftShader sob Windows/macOS, uma string Metal sob Windows/Linux). - Uma falsificação apenas de
getParameternão pode vencer um render forçado. A substituição altera as strings de fornecedor/renderizador, mas a lista de extensões, os limites de parâmetros e o hash de pixel realmente renderizado ainda vêm do SwiftShader. Anti-bots de primeira linha (DataDome Picasso, Cloudflare, Kasada) forçam um render e verificam cruzadamente, então uma string de GPU discreta sob renderização por software é detectável. A falsificação é apenas para alvos fáceis.
// Soft targets: an OS-coherent default derived from the UA (Linux => Mesa string)
const browser = new Browser({ headless: true, spoofWebGL: true });
// Or pick exact strings (must match your UA's OS)
const browser = new Browser({
webglVendor: "Google Inc. (Intel)",
webglRenderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics (TGL GT1) (0x00009A60), OpenGL 4.6 (Core Profile) Mesa 23.2.1)",
});
Guia de decisão para o alvo Lambda / Linux + --headless=new:
- Alvos difíceis (DataDome/Cloudflare/Kasada): uma falsificação de string não sobreviverá a um hash de render. Execute uma GPU real (EC2
g4dn/g5+ driver NVIDIA) e desligue a falsificação — deixe o WebGL relatar o hardware genuíno e internamente coerente. Esta é a única configuração que passa no Picasso. - Alvos fáceis:
spoofWebGL: true(string Linux coerente) é suficiente.
Em --headless=new, a biblioteca sempre adiciona --enable-unsafe-swiftshader: o Chrome 137+ removeu o fallback automático de WebGL por software, então sem ele getContext('webgl') retorna null em um host sem GPU — isso em si é um sinal, e torna qualquer falsificação inerte. Com uma GPU real presente, a GPU ainda é usada.
A substituição é executada no mundo principal da página e mantém getParameter.toString() com aparência nativa. Limitação conhecida: ela tem como alvo o thread principal, não Web Workers/OffscreenCanvas.
A stack de produção
proxy · VPS · solver — as três coisas que decidem se um scrape passa. Esta biblioteca lida com o navegador; estas lidam com tudo ao redor, e o próprio conselho da biblioteca aponta diretamente para elas. Descontos para leitores onde consegui obtê-los.
| Você precisa | Por quê — direto do conselho desta biblioteca | Recomendado (com desconto) |
|---|---|---|
| Proxy residencial / móvel | Sua reputação de IP é o teto — um ASN de datacenter perde antes mesmo do seu JavaScript ser executado. A maior alavanca única. | IPRoyal — pool mais barato que ainda passa · Bright Data — premium, para os alvos mais difíceis |
| Um VPS para executá-lo 24/7 | Scrapes agendados querem uma máquina Linux sempre ligada, não seu laptop. O caminho de WebGL por software sem GPU funciona bem aqui para alvos fáceis/médios. | Hostinger VPS — 20% de desconto através deste link |
| Um resolvedor de CAPTCHA (somente quando forçado) | A lib não é um resolvedor por design — entregue desafios de imagem/checkbox e conserte seu IP primeiro. | 2Captcha · Anti-Captcha |
Para alvos difíceis de hash de render (DataDome Picasso, Kasada), suba de um VPS barato para um EC2 com GPU real g4dn/g5 e desligue spoofWebGL — detalhes em Implantação em nuvem abaixo.
Links de proxy, VPS e captcha são links afiliados/pagos — posso ganhar uma comissão sem custo extra para você, e você obtém o desconto. São as ferramentas pelas quais roteio meu próprio tráfego.
Implantação em nuvem (Lambda vs EC2-GPU)
O sinal dominante é seu IP, não sua impressão digital. Um Chrome headless impecável em um IP de egresso da AWS ainda pontua baixo no reCAPTCHA v3 e é desafiado por Cloudflare/DataDome, porque ASNs de datacenter são penalizados antes do seu JavaScript ser executado. Roteie alvos protegidos através de um proxy residencial ou móvel — IPRoyal é o pool residencial mais barato que encontrei que ainda passa (link pago); Bright Data é a opção premium para os alvos mais difíceis. A biblioteca avisa sobre egresso direto (
warnOnDirectEgress) ebrowser.checkEgress()relata se seu IP de saída está sinalizado como hospedagem. Lambda — runtime ZIP +@sparticuz/chromium(o caminho suportado). Ogoogle-chrome-stablereal não roda no runtime ZIP do Lambda: ele morre sob o sandbox namespace/seccomp do Firecracker, mesmo em processo único (credentials.cc Operation not permitted, "Zygote could not fork"), e um Chrome completo não cabe no limite de ~250 MB do zip descompactado. Use@sparticuz/chromium(uma dependência opcional — instale e fixe a versão, e mantenha-oexternalno seu bundler para que ele resolva o binário por caminho relativo). Ele vem com chrome-headless-shell, que já é headless (--headless='shell'); a lib detecta isso viachannel:'headless-shell'e: nunca adiciona--headless=new, controla as flags de WebGL por software, garante queHOME/XDG_*/diretórios de user-data existam em/tmp, apontaDBUS_SESSION_BUS_ADDRESSpara/dev/null, e aplica automaticamente os shims de shell (window.chrome,navigator.connection) que um Chrome headful real tem nativamente.
import { Browser, resolveSparticuz } from "@eurafaeldecarvalho/browser-scraper";
const browser = new Browser({
chromium: await resolveSparticuz(), // executablePath + Lambda args; implies channel:'headless-shell'
proxy: "http://user:pass@residential-proxy:8000",
geoCountry: "BR", // timezone + locale + Accept-Language, matched to the proxy exit
onStderr: (chunk) => console.error(chunk), // optional: live Chrome stderr (OOM/seccomp cause)
});
Esta é uma troca marginal anti-bot (chrome-headless-shell + SwiftShader, sem codecs proprietários), aceita porque para reCAPTCHA v3 a reputação de IP domina. Uma build
--headless=newcom Chromium completo não vale o empacotamento: não cabe no zip, morre no Firecracker, e seus piores sinais (SwiftShader WebGL, sem GPU) vêm do host sem GPU, não do modo headless — então são idênticos ao shell. Dimensione o/tmpda função (armazenamento efêmero) além do padrão de 512 MB: o binário extrai ~130 MB lá, mais o perfil de user-data.
geoCountry deve corresponder ao país de saída do proxy (um relógio UTC ou locale pt-BR em um IP alemão é um sinal claro). Se você não souber antecipadamente, defina autoGeo: true em vez de geoCountry e o primeiro newTab() consultará o país de saída através do proxy e o definirá para você (uma requisição extra na inicialização; geoCountry/timezone/locale explícitos sempre vencem, e um país não mapeado gera um aviso em vez de adivinhar).
EC2 g4dn/g5 (GPU NVIDIA real): a postura para alvos difíceis. Verifique a GPU em chrome://gpu, deixe spoofWebGL desligado, e ainda use um proxy residencial/móvel.
Sem AWS? Um VPS simples também funciona. Para alvos suaves/médios e tarefas agendadas, um VPS barato sempre ligado é o host de menor esforço — Hostinger VPS (20% de desconto via esse link) roda o caminho de WebGL por software sem GPU perfeitamente. A mesma regra se aplica: combine com um proxy residencial/móvel, e só suba para uma máquina EC2 com GPU real para alvos difíceis de render-hash.
Identidade & persistência
Um perfil temporário novo a cada execução não acumula reputação, e cookies forjados do Google/_GRECAPTCHA não compram nada (o Google os emite e valida no servidor — o seeder não os fabrica mais). Para alvos protegidos por reCAPTCHA-v3, mantenha um perfil persistente por identidade fixado a um IP de proxy sticky e aqueça-o para que os servidores emitam cookies reais:
import { Browser, ProfileStore, warmProfile } from "@eurafaeldecarvalho/browser-scraper";
const store = new ProfileStore({ baseDir: "/mnt/efs/profiles" }); // EFS, or sync to S3 between runs
const browser = new Browser({
userDataDir: store.dirFor("identity-42"),
proxy: "http://user:pass@sticky-residential:8000",
geoCountry: "BR",
});
const tab = await browser.newTab();
await warmProfile(tab); // genuine navigations + ambient motion so cookies/history accrue
Um perfil aquecido atrás de um IP de datacenter rotativo não ganha nada — uma identidade, um IP sticky, uma impressão digital.
Injete cookies aquecidos via CDP, não pelo arquivo SQLite.
ProfileManager.seedCookies()(a escrita SQLite pré-lançamento) é inerte no Chrome ≥ 80: o Chrome lê oencrypted_valuecriptografado com OSCrypt e ignora ovalueem texto puro, então cookies semeados por arquivo são descartados no carregamento (o auto-seed agora só semeia histórico, que o caminho SQLite lida bem). Para injetar um pacote de cookies aquecido que realmente persista, defina-o em tempo de execução através do armazenamento (criptografado) do próprio Chrome:await tab.setCookies({ cookies })— criptografado com a chave do perfil ativo.ProfileManager.cookieSeeds()retorna o pacote padrão de baixa chave. Fixe cada pacote ao mesmo IP residencial sticky em que foi aquecido.
WebRTC
O WebRTC é deixado habilitado (desabilitá-lo completamente é em si uma anomalia), mas bloqueado para evitar vazamentos de IP: atrás de um proxy, ele força todo o UDP através do proxy (disable_non_proxied_udp); caso contrário, expõe apenas a interface pública e esconde IPs locais (default_public_interface_only).
Alternativas — como se compara
Se você está procurando uma alternativa não detectada ao Puppeteer / Playwright, aqui está onde esta biblioteca se posiciona. Ela tem o mesmo objetivo do Puppeteer padrão + puppeteer-extra-plugin-stealth ou puppeteer-real-browser, mas começa do CDP bruto em vez de corrigir um driver de alto nível depois.
| Capacidade | Puppeteer / Playwright | + puppeteer-extra-plugin-stealth | puppeteer-real-browser | browser-scraper |
|---|---|---|---|---|
| Transporte | API de alto nível (habilita Runtime) | mesmo + patches JS | Chrome real + patches | CDP bruto |
Vazamento de Runtime.enable | presente | corrigido | mitigado | nunca enviado (execução em mundo isolado) |
| Atravessa Shadow DOM fechado em uma chamada | ✗ | ✗ | ✗ | ✓ find() |
Iframe de origem cruzada no mesmo find() | ✗ (frames manuais) | ✗ | ✗ | ✓ |
| Persona coerente (UA + Client-Hints + WebGL + fuso horário de um único UA) | ✗ | parcial | parcial | ✓ |
| Entrada humana (mouse min-jerk, digitação lognormal) | ✗ | ✗ (adicione ghost-cursor) | parcial | ✓ integrado |
| Caminho AWS Lambda / chrome-headless-shell | manual | manual | ✗ | ✓ resolveSparticuz() |
Esta é uma ferramenta de precisão, não um bypass mágico de captcha: contra alvos difíceis (Cloudflare, DataDome, Kasada), sua reputação de IP ainda domina — combine com um proxy residencial/móvel (veja Implantação em nuvem).
Para agentes de IA
@playwright/mcp | Stagehand | Firecrawl / scrapers hospedados | browser-scraper | |
|---|---|---|---|---|
| Roda localmente, sem chave de API | ✓ | ✓ (modelo BYO) | ✗ (API paga) / auto-hospedado | ✓ |
Stealth (sem Runtime.enable, persona coerente, entrada humana) | ✗ | via nuvem Browserbase | no servidor | ✓ integrado |
| Resolução do Cloudflare Turnstile | ✗ | somente nuvem | ✓ (pago) | ✓ solveTurnstile() |
| Snapshot alcança raízes shadow fechadas | ✗ | ✗ | n/d | ✓ |
| HTML → Markdown, metadados | ✗ | parcial | ✓ | ✓ |
| Seletores gerados uma vez, depois sem LLM | ✗ | cache de ações | ✗ | ✓ generateExtractor() |
| Custo por página | grátis | LLM por ação | por crédito | grátis (LLM opcional) |
Use Stagehand ou browser-use se quiser um loop de agente autônomo. Use browser-scraper como a camada de navegador sob um agente (Claude, Cursor, o seu próprio), ou quando você precisar principalmente de conteúdo de páginas protegidas de forma barata.
FAQ
Como faço o Puppeteer ficar não detectado / passar na detecção de bot no Node.js?
Os sinais detectáveis são o vazamento CDP Runtime.enable, o User-Agent/client-hints HeadlessChrome, e incompatibilidades de impressão digital entre camadas. Esta biblioteca evita Runtime.enable completamente (todo JS roda via Page.createIsolatedWorld + Runtime.evaluate), remove o UA headless, e deriva uma persona coerente — veja Stealth, em profundidade. Verificado contra bot.sannysoft.com (0 falhas), CreepJS (0% headless), e bot-detector.rebrowser.net.
Como contorno o Cloudflare Turnstile no Node.js?
A caixa de seleção do Turnstile geralmente vive dentro de um iframe sandboxed e de origem cruzada. Um único find() a alcança (e qualquer shadow root fechado) para que você possa click() como um usuário real — ou chame tab.solveTurnstile() para o caminho de uma chamada (ele espera o widget, clica na caixa de seleção como humano, e faz polling pelo token). Veja Atravesse Shadow DOM fechado + iframes de origem cruzada e o exemplo examples/cloudflare-turnstile.ts. Atrás de um IP residencial limpo, isso passa no intersticial do Cloudflare; o IP, não o clique, é o fator dominante.
Como acesso ou consulto um Shadow DOM fechado?
Puppeteer e Playwright não conseguem atravessar raízes shadow fechadas. find() / findAll() pesquisam o documento principal, raízes shadow fechadas, e iframes de origem cruzada (OOP) em uma chamada — sem caminhada shadowRoot, sem encanamento de frames.
O que é o vazamento de detecção CDP Runtime.enable, e como é evitado?
Chamar Runtime.enable (que a maioria das pilhas de automação faz na inicialização) aciona sinais do domínio Runtime como Runtime.consoleAPICalled que fornecedores anti-bot identificam. Esta biblioteca nunca habilita o domínio Runtime — a execução acontece em um mundo isolado nomeado via Runtime.evaluate, que não o faz.
Como executo Chrome headless no AWS Lambda para scraping?
Use a build @sparticuz/chromium opcional e resolveSparticuz(); a biblioteca detecta chrome-headless-shell e aplica as flags, diretórios e shims corretos automaticamente — veja Implantação em nuvem (Lambda vs EC2-GPU).
Puppeteer vs Playwright para stealth scraping — qual devo usar?
Ambos são drivers de alto nível que habilitam o domínio Runtime e não conseguem atravessar raízes shadow fechadas, então para scraping sensível a detecção você acaba adicionando plugins de stealth. Esta biblioteca é a alternativa CDP-first construída para esse caso desde o início — veja Alternativas.
Como dou ao Claude / Cursor um navegador que não seja bloqueado?
Execute o servidor MCP incluído: claude mcp add browser-scraper -- npx -y @eurafaeldecarvalho/browser-scraper mcp. O agente obtém navigate / snapshot / click / type / markdown / solve_turnstile em um Chrome stealth. Veja Servidor MCP.
Como extraio um site para Markdown para um pipeline de LLM / RAG?
scrape({ url, formats: ["markdown"] }) retorna Markdown limpo do conteúdo principal, escalando de HTTP simples para o navegador stealth apenas quando necessário. Para campos estruturados, use extract() com um esquema Zod, ou generateExtractor() para pagar o LLM uma vez por layout de site em vez de uma vez por página.
Ele resolve CAPTCHAs ou reCAPTCHA v3?
Ele não resolve CAPTCHAs de imagem/token. O que faz é manter seu score v3 do reCAPTCHA alto — mouse/digitação semelhantes a humanos, atividade ambiente antes de uma chamada de emissão de token, e um perfil persistente aquecido em um IP residencial sticky — veja Identidade & persistência. Para desafios de imagem v2, combine com um serviço de resolução.
Notas & limitações
- A detecção de conteúdo principal do
markdown()é uma heurística (tags semânticas, papéis, dicas de classe, densidade de links), não Readability. UsemainContent: falsequando ele cortar demais. - O
snapshot()usa o domínio de Acessibilidade do CDP, a mesma fonte que o snapshot do Playwright usa. Seletores dogenerateExtractor()são executados comquerySelectorAllno mundo isolado, então eles não alcançam shadow roots fechados; para esses, useextract()(entrada Markdown/snapshot) ouobserve(). - Nenhum spoofing de impressão digital JavaScript é injetado por padrão em um Chrome desktop normal: o comportamento nativo é mais difícil de detectar do que APIs corrigidas. Os ganhos de stealth vêm de evitar
Runtime.enable, limpar o headless UA/client-hints, nomes de mundos genéricos, flags de inicialização cuidadosas e entrada semelhante à humana. Traga seus próprios patches viatab.addInitScriptse um alvo precisar deles. - Sob
channel:'headless-shell'a biblioteca aplica um pequeno conjunto de shims condicionais que fecham duas falhas específicas apenas de shell — ela não afirma tornar o binário de shell indistinguível de um Chrome headful real. Os shims, cada um um no-op quando o valor já é real e escrito para corresponder à forma de descritor nativa do Chrome (propriedade de dados própria enumerável parawindow.chrome; acessores de protótipo comtoStringde aparência nativa paranavigator.connection): umwindow.chrome/chrome.runtimemínimo (apenas quando ausente) e umnavigator.connection.rttnão zero (apenas quando lê0). Lacunas residuais de shell são deliberadamente deixadas intocadas (um adaptadornavigator.plugins/WebGPU fabricado é em si uma pista — veja o documento de pesquisa) e permanecem limitações de alvo suave; a alavanca dominante continua sendo o IP. A biblioteca também insere a marca GREASE real e viva (lida denavigator.userAgentData) nos Client-Hints em vez de um valor codificado que gira desatualizado, e verifica se um contexto WebGL existe antes de instalar o spoof de string WebGL (um spoof apenas degetParameteré inerte — e incoerente — sem contexto por trás; contra anti-bots de hash de renderização como DataDome Picasso é pior do que honesto, então mantenhaspoofWebGLapenas para alvos suaves). - O spoofing de string WebGL é apenas para alvos suaves — ele não pode vencer um hash de renderização forçado. Para alvos difíceis, use uma GPU real.
- O sinal dominante é a reputação do seu IP, não sua impressão digital. ASNs de datacenter perdem antes do JavaScript rodar; roteie alvos protegidos por um proxy residencial/móvel (IPRoyal com orçamento apertado, Bright Data para alvos difíceis — links pagos).
- Esta biblioteca não resolve captchas — por design. Sua postura é reputação de IP + comportamento semelhante ao humano, não forjamento de tokens (tokens
_GRECAPTCHAforjados não compram nada). Se um alvo apresentar um desafio interativo de imagem/caixa de seleção que você realmente precisa resolver, entregue-o a um serviço de solução — 2Captcha ou Anti-Captcha (links pagos). Corrija seu IP primeiro: um solucionador em um IP de datacenter sinalizado ainda perde.
Suporte
Esta biblioteca é construída e mantida em código aberto. Se ela poupou você do trabalho de combater vazamentos de Runtime.enable, travessia de shadow-DOM e coerência de impressão digital, uma pequena contribuição mantém o desenvolvimento em andamento:
- Patrocine no GitHub — única ou mensal
- Compre um café para mim no Ko-fi — dica rápida única
Dar uma estrela no repositório também ajuda — é assim que outros desenvolvedores encontram o projeto.
Alguns links de proxy e captcha neste README são links pagos — posso ganhar uma comissão sem custo extra para você. São ferramentas pelas quais eu rotearia meu próprio tráfego.
Licença
MIT. Versões até 0.7.0 foram publicadas sob PolyForm Noncommercial 1.0.0; a partir de 0.8.0 a biblioteca é MIT.