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.

npm version CI npm downloads node license: MIT

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() e snapshot() 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, e runOrHeal() 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 fingerprintFornecedor 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-scraper nã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

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ívelO que executaUsado quando
httpUma requisição, sem Chrome (fetch do Node, ou curl-impersonate para um JA3/JA4 Chrome genuíno)Páginas estáticas, APIs
browserO Chrome stealthA resposta HTTP foi bloqueada (403/429/503), um desafio, ou um shell de app JS vazio
stealthA mesma aba mais um aquecimento comportamental e uma resolução interativa de TurnstileO 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:

FerramentaO que faz
navigateAbre uma URL e retorna o snapshot
snapshotEstrutura de acessibilidade com [ref=…]
clickClica em um elemento por referência, com entrada semelhante à humana
typeDigita em um elemento por referência, com entrada semelhante à humana
press_keyPressiona uma tecla
go_backVolta no histórico
waitEspera um número de segundos
markdownConteúdo principal como Markdown, com um query opcional
metadataJSON-LD, OpenGraph e o restante dos metadados da página
screenshotCaptura de tela PNG
solve_turnstileResolve um Cloudflare Turnstile interativo
scrapeFetch de chamada única com escalonamento de nível
check_egressMostra se seu IP de saída parece de datacenter

Proteções:

  • --allowed-domains a.com,b.com recusa navegação para outros lugares e reverte cliques que saiam da lista.
  • --secrets .env permite 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-port vinculado a 127.0.0.1. Uma sondagem na página de mesma origem de localhost nã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 de Page.createIsolatedWorld + Runtime.evaluate, comandos simples que não habilitam o domínio Runtime. A invalidação de contexto é tratada via Page.frameNavigated e novas tentativas de contexto obsoleto. (Calibração: a sonda mais conhecida — o getter de Error.stack acionado 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 evitar Runtime.enable ainda 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 injeta HeadlessChrome no UA e sec-ch-ua. A biblioteca o remove e aplica userAgentMetadata correspondente para que navigator.userAgentData permaneça consistente.
  • Nomes de mundo isolado por aba, aleatorizados (um token u… aleatório, não um marcador fixo como uma constante util ou 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 pilha Error).
  • 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 um grecaptcha.execute()).
  • Persona coerente. String de UA, Client-Hints (platform, platformVersion, fullVersionList real, 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 de window.chrome/navigator.plugins, WebGL SwiftShader, codecs — que é exatamente o que channel:'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:

  1. Nunca reivindique um SO ao qual o renderizador não pode pertencer. Uma string de renderizador Direct3D11/D3D11 existe 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: true agora 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 um webglRenderer explí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).
  2. Uma falsificação apenas de getParameter nã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ê precisaPor quê — direto do conselho desta bibliotecaRecomendado (com desconto)
Proxy residencial / móvelSua 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/7Scrapes 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) e browser.checkEgress() relata se seu IP de saída está sinalizado como hospedagem. Lambda — runtime ZIP + @sparticuz/chromium (o caminho suportado). O google-chrome-stable real 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-o external no 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 via channel:'headless-shell' e: nunca adiciona --headless=new, controla as flags de WebGL por software, garante que HOME/XDG_*/diretórios de user-data existam em /tmp, aponta DBUS_SESSION_BUS_ADDRESS para /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=new com 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 /tmp da 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ê o encrypted_value criptografado com OSCrypt e ignora o value em 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.

CapacidadePuppeteer / Playwright+ puppeteer-extra-plugin-stealthpuppeteer-real-browserbrowser-scraper
TransporteAPI de alto nível (habilita Runtime)mesmo + patches JSChrome real + patchesCDP bruto
Vazamento de Runtime.enablepresentecorrigidomitigadonunca 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)✗parcialparcial✓
Entrada humana (mouse min-jerk, digitação lognormal)✗✗ (adicione ghost-cursor)parcial✓ integrado
Caminho AWS Lambda / chrome-headless-shellmanualmanual✗✓ 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/mcpStagehandFirecrawl / scrapers hospedadosbrowser-scraper
Roda localmente, sem chave de API✓✓ (modelo BYO)✗ (API paga) / auto-hospedado✓
Stealth (sem Runtime.enable, persona coerente, entrada humana)✗via nuvem Browserbaseno 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áginagrátisLLM por açãopor créditográ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. Use mainContent: false quando ele cortar demais.
  • O snapshot() usa o domínio de Acessibilidade do CDP, a mesma fonte que o snapshot do Playwright usa. Seletores do generateExtractor() são executados com querySelectorAll no mundo isolado, então eles não alcançam shadow roots fechados; para esses, use extract() (entrada Markdown/snapshot) ou observe().
  • 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 via tab.addInitScript se 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 para window.chrome; acessores de protótipo com toString de aparência nativa para navigator.connection): um window.chrome/chrome.runtime mínimo (apenas quando ausente) e um navigator.connection.rtt não zero (apenas quando lê 0). Lacunas residuais de shell são deliberadamente deixadas intocadas (um adaptador navigator.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 de navigator.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 de getParameter é 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 mantenha spoofWebGL apenas 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 _GRECAPTCHA forjados 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:

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.