Browser Scraper
Servidor MCP local que controla Chrome real mediante CDP sin Runtime.enable. Instantánea de accesibilidad con referencias clicables, Markdown que incluye shadow roots cerrados e iframes de origen cruzado, herramienta Cloudflare Turnstile. Gratis, MIT, sin clave API.
Documentación
Browser Scraper: el navegador sigiloso para web scraping y agentes de IA
Chrome no detectado para Node.js que lee y opera en los sitios que bloquean bots. Úsalo como librería, CLI o servidor MCP para Claude, Cursor y Codex. Convierte cualquier página, incluidos Shadow DOM cerrados e iframes de origen cruzado, en Markdown limpio, datos estructurados o una instantánea de accesibilidad por la que un agente puede navegar haciendo clic. Local, gratuito, MIT, trae tu propio 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 impulsa Chrome real a través del protocolo DevTools sin procesar (CDP). Nunca envía Runtime.enable, una de las señales de automatización más ampliamente detectadas, y cada capa de la identidad del navegador cuenta la misma historia. Sobre ese núcleo sigiloso añade las piezas que las cargas de trabajo de IA necesitan: extracción de Markdown y metadatos, una instantánea de accesibilidad con referencias clicables, un scrape() de una sola llamada que escala de HTTP simple a un navegador sigiloso solo cuando es necesario, un servidor MCP y extracción LLM opcional con cualquier modelo.
Por qué este:
- Atraviesa donde los navegadores headless son bloqueados. No hay fuga de
Runtime.enable, la persona (UA, Client Hints, WebGL, zona horaria, locale) es coherente, y la entrada de mouse y teclado es similar a la humana.solveTurnstile()maneja la casilla de Cloudflare. - Ve todo.
find(),markdown()ysnapshot()alcanzan raíces shadow cerradas e iframes de origen cruzado, que la instantánea de Playwright y los extractores estilo Readability pasan por alto. - Construido para agentes. El servidor MCP y la CLI funcionan sin instalación adicional. Las credenciales se escriben a través de marcadores de posición
{{SECRET}}para que el modelo nunca las vea, una lista de permitidos restringe dominios, y el contenido de la página se delimita como datos no confiables. - LLM solo donde vale la pena.
generateExtractor()hace que el modelo escriba selectores CSS una vez. Después,run()extrae sin LLM, yrunOrHeal()regenera los selectores cuando el sitio cambia. - Local y gratuito. Sin nube, sin clave API, sin facturación por página. Tú traes el proxy y, opcionalmente, el modelo (OpenAI, Anthropic, Google, Ollama… a través del Vercel AI SDK, o tu propia función).
Pasa los detectores de bots que la gente realmente usa
Con la configuración adecuada — una IP residencial/móvil limpia, más una GPU real (EC2 g4dn/g5) para objetivos de hash de renderizado — browser-scraper está construido para superar los conjuntos de huellas digitales y las pilas anti-bot que bloquean la automatización headless:
| Detección / conjunto de huellas | Proveedor anti-bot / WAF | ||
|---|---|---|---|
bot.sannysoft.com — 0 fallos | ✅ | Cloudflare (Turnstile) | ✅ |
| CreepJS — 0% headless, 0% sigiloso | ✅ | 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 | ✅ |
Un ✅ significa que la capa de huella + comportamiento pasa — no es un bypass garantizado. Contra objetivos difíciles, tu reputación de IP es el techo: un ASN de centro de datos pierde antes de que el JavaScript se ejecute, así que enruta a través de un proxy residencial/móvil (ver La pila de producción).
browser-scraperno es un solucionador de CAPTCHA — para desafíos de imagen reCAPTCHA v2 / hCaptcha / Arkose (FunCaptcha), combínalo con un servicio solucionador. Sí mantiene las puntuaciones de reCAPTCHA v3 altas mediante entrada similar a la humana y perfiles calentados.
Los números se tomaron en un Chrome de escritorio. En un Lambda sin GPU ejecutando chrome-headless-shell (
@sparticuz/chromium) las superficies difieren — vuelve a ejecutar los conjuntos contra tu binario de producción real y trátalo como la fuente de verdad (ver Implementación en la nube).
Esta es una herramienta de precisión, no un bypass mágico — es honesta sobre sus límites (ver Notas y limitaciones), y esa honestidad es exactamente lo que hace que las marcas verdes de arriba valgan algo.
Contenido
- Instalación
- Inicio rápido
- Para agentes de IA y LLMs — Markdown,
scrape(), instantánea + referencias, servidor MCP, CLI, extracción LLM - El recorrido — qué hace, con ejemplos
- Referencia de API
- Sigilo, en profundidad
- WebGL / GPU en hosts en la nube
- La pila de producción — proxy · VPS · solucionador
- Implementación en la nube (Lambda vs EC2-GPU)
- Identidad y persistencia
- WebRTC
- Alternativas — cómo se compara
- Preguntas frecuentes
- Notas y limitaciones
- Soporte
Instalación
npm install @eurafaeldecarvalho/browser-scraper
o:
pnpm add @eurafaeldecarvalho/browser-scraper
Requiere Node.js ≥ 20 y Chrome/Chromium. El sembrado del historial de perfil usa el node:sqlite integrado de Node (Node ≥ 22.5) o el better-sqlite3 opcional; si ninguno está disponible, simplemente se omite. Los ayudantes de LLM necesitan ai (y zod si usas esquemas Zod), y solo si importas /ai.
Requiere Node.js ≥ 20 y un Chrome/Chromium local (se encuentra automáticamente, o apunta a uno con chromePath). Para AWS Lambda, instala también el @sparticuz/chromium opcional — ver Implementación en la nube.
Inicio 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();
}
Esa es la forma completa de la librería: un Browser, un Tab, y Elements que encuentras y sobre los que actúas. Todo lo demás es detalle sobre estos tres.
Para agentes de IA y LLMs
Markdown limpio, metadatos y enlaces
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() lee el árbol DOM propio de Chrome a través de CDP (DOM.getDocument con pierce). No se inyecta ningún script en la página, y el contenido de las raíces shadow cerradas se incluye en orden de lectura, con slots compuestos. Para HTML que ya tienes, htmlToMarkdown(html) y extractMetadata(html) hacen lo mismo sin navegador.
scrape(): una llamada, primero el nivel más 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 }, …]
| Nivel | Qué se ejecuta | Cuándo se usa |
|---|---|---|
http | Una solicitud, sin Chrome (fetch de Node, o curl-impersonate para un JA3/JA4 genuino de Chrome) | Páginas estáticas, APIs |
browser | El Chrome sigiloso | La respuesta HTTP fue bloqueada (403/429/503), un desafío, o un shell de aplicación JS vacío |
stealth | La misma pestaña más un calentamiento conductual y una resolución interactiva de Turnstile | El navegador aún ve una página de desafío |
Una página de desafío nunca se devuelve como contenido: si cada nivel está bloqueado, scrape() lanza un ScrapeError que lleva el attempts.
Instantánea de accesibilidad + referencias (en lo que los agentes hacen clic)
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
El árbol de accesibilidad se calcula sobre el árbol compuesto, por lo que los elementos dentro de raíces shadow cerradas aparecen y se pueden hacer clic. Pasa { interactive: true } para obtener solo la lista plana de elementos clicables o escribibles.
Servidor MCP (Claude Code, Claude Desktop, Cursor, Codex…)
claude mcp add browser-scraper -- npx -y @eurafaeldecarvalho/browser-scraper mcp
O en cualquier configuración 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"]
}
}
}
Herramientas:
| Herramienta | Qué hace |
|---|---|
navigate | Abre una URL y devuelve la instantánea |
snapshot | Esquema de accesibilidad con [ref=…] |
click | Hace clic en un elemento por referencia, con entrada similar a la humana |
type | Escribe en un elemento por referencia, con entrada similar a la humana |
press_key | Presiona una tecla |
go_back | Retrocede en el historial |
wait | Espera un número de segundos |
markdown | Contenido principal como Markdown, con un query opcional |
metadata | JSON-LD, OpenGraph y el resto de metadatos de la página |
screenshot | Captura de pantalla PNG |
solve_turnstile | Resuelve un Cloudflare Turnstile interactivo |
scrape | Fetch de una sola vez con escalada de niveles |
check_egress | Muestra si tu IP de salida parece un centro de datos |
Barandillas de seguridad:
--allowed-domains a.com,b.comrechaza la navegación a otros lugares y revierte los clics que salen de la lista.--secrets .envpermite que el agente escriba{{PASSWORD}}. El valor se rellena en el momento de la pulsación de teclas y se redacta de todo lo devuelto.- El contenido de la página siempre regresa dentro de marcadores
<untrusted-page-content>. - Otras opciones:
--proxy,--headful,--user-data-dir,--geo BR. El servidor también está listado para el Registro MCP (server.json), y se incluye una Habilidad de Agente.
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
Extracción LLM: @eurafaeldecarvalho/browser-scraper/ai
Opcional, y no se instala nada adicional a menos que lo uses. Pasa un modelo de Vercel AI SDK (npm i ai @ai-sdk/anthropic, o openai, google, ollama…) o tu propia función llm. Los esquemas pueden ser Zod o JSON Schema simple.
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();
¿No tienes un modelo a mano? llm: async ({ system, prompt, schema, images }) => myClient.json(...) funciona con cualquier proveedor o un modelo local.
El recorrido
Una mirada guiada a las piezas que usarás más, cada una mostrada de la manera en que realmente la usarías.
Atraviesa Shadow DOM cerrado e iframes de origen cruzado con un solo find()
find() es el titular. Dale un selector CSS y busca en el documento principal, raíces shadow cerradas e iframes de origen cruzado — en una sola llamada. La casilla de captcha que vive tres capas dentro de un iframe sandboxed se encuentra exactamente de la misma manera que un <h1> de nivel 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" }));
Espera el momento adecuado
Las páginas reales se asientan por etapas. Elige la condición que coincida con lo que realmente estás esperando en lugar de dormir y esperar.
// 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,
});
Interactúa como un humano
Las entradas están humanizadas por defecto — el realismo está en la librería, no en tu 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() y pressKey() envían eventos genuinos de keyDown/keyUp con keyCode/code correctos, por lo que las teclas nombradas (Enter, Tab, ArrowDown) se comportan como un teclado real en lugar de un toque sintético de value.
Controla la red
Reduce el ancho de banda del proxy medido, mata rastreadores o reescribe solicitudes — sin romper la página de una manera que en sí misma parezca un 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) });
El bloqueo se compone limpiamente con un proxy autenticado: un solo Fetch.enable lleva tanto las credenciales del proxy como tus reglas de bloqueo, para que ninguno pise al otro.
Huella de navegador coherente (User-Agent, Client Hints, WebGL, zona horaria)
La defensa de huella más fuerte no es ninguna suplantación individual — es que nada se contradiga entre sí. Establece la persona una vez y cada capa la sigue.
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" });
¿No sabes el país de salida del proxy de antemano? Establece autoGeo: true y el primer newTab() lo busca a través del proxy y configura la geolocalización por ti.
Perfiles persistentes y calentamiento
reCAPTCHA v3 puntúa la reputación, y un perfil desechable no tiene ninguna. Mantén una identidad, en una IP fija, y caliéntala para que los servidores acuñen cookies reales.
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
Una identidad, una IP fija, una huella — un perfil calentado detrás de una IP de centro de datos rotatoria no gana nada.
Referencia de 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();
Brecha de endurecimiento conocida (no abordada): la biblioteca expone CDP a través de un
--remote-debugging-portTCP vinculado a127.0.0.1. Una sonda en la página del mismo origen delocalhostno puede bloquearse por completo con el puerto aleatorio. Cerrarlo requiere cambiar todo el transporte a--remote-debugging-pipe(fd 3/4), lo que elimina los endpoints HTTP/json/*y colapsa el modelo WebSocket por pestaña en una única conexión multiplexada por sesión — una reescritura de transporte profunda y aislada. Se deja deliberadamente como un cambio separado en lugar de enviarse a medias; es un vector de baja gravedad (puerto aleatorio + enlace localhost ya mitigan el escaneo remoto).
Pestaña
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 sigue los mismos nombres públicos que usa Puppeteer: load, domcontentloaded, networkidle0 y networkidle2.
Todos los tiempos de espera y pausas se expresan en milisegundos.
waitForSelector({ state: "visible" }) espera solo la visibilidad.
waitForSelector({ state: "ready" }) espera visibilidad más interactividad, incluyendo comprobaciones de habilitado/no ocupado para estados comunes de botones.
Red / bloqueo de solicitudes
// 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();
El bloqueo de solicitudes se compone con un proxy autenticado: un único Fetch.enable
transporta tanto el manejo de credenciales del proxy como las reglas de bloqueo, por lo que ninguno
pisa al otro.
Carril rápido HTTP (browserless)
Para endpoints baratos / sin protección (APIs JSON, HTML estático) no siempre necesitas una
pestaña — browser.createHttpClient() emite una única solicitud con la
persona del navegador (User-Agent resuelto + Accept-Language) y proxy, sin abrir 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);
Es un complemento, no un reemplazo sigiloso: para cualquier cosa detrás de un anti-bot real, conduce una pestaña.
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() es humanoide por defecto: mueve el cursor al elemento a lo largo de una ruta Bézier antes de presionar.
click({ humanLike: false }) envía la pulsación/soltado directamente en la posición objetivo resuelta.
click({ removeNewTabTarget: true }) elimina target y formtarget antes de hacer clic para que la acción permanezca en la pestaña actual cuando sea posible.
type() y pressKey() envían eventos reales de keyDown/keyUp (correctos keyCode/code), por lo que teclas con nombre como Enter, Tab y ArrowDown funcionan.
type({ typos: true }) (opt-in, desactivado por defecto) inyecta errores ocasionales de teclas adyacentes + autocorrección (Backspace), que los evaluadores de comportamiento ponderan como humano. Déjalo desactivado para campos cuyo valor afirmas a mitad de escritura o que validan en cada pulsación.
Sigilo, en profundidad
Investigación de fondo: cómo reCAPTCHA v3/Enterprise y los proveedores anti-bot (Cloudflare, DataDome/Picasso, PerimeterX/HUMAN, Kasada, Akamai, Imperva, securiti.ai, Arkose, hCaptcha) detectan realmente los bots — con la taxonomía de capas de señales y la justificación de diseño detrás de cada elección a continuación — está documentada en
docs/antibot-and-captcha-research.md.
La biblioteca está construida para minimizar las señales que los proveedores anti-bot (Cloudflare, DataDome, Kasada) buscan:
- Sin
Runtime.enable. Todo JavaScript se ejecuta a través dePage.createIsolatedWorld+Runtime.evaluate, comandos simples que no habilitan el dominio Runtime. La invalidación de contexto se maneja mediantePage.frameNavigatedy reintentos de contexto obsoleto. (Calibración: la sonda más conocida — el getter deError.stackactivado durante la serialización del inspector — fue en gran medida desactivada por los parches de V8 de mayo de 2025, por lo que ya no es la única señal dominante que solía ser; pero evitarRuntime.enableaún derrota las otras señales dependientes del dominio Runtime, p. ej.Runtime.consoleAPICalled, por lo que sigue siendo un invariante correcto.) - User-Agent limpio + client hints. En
--headless=new, Chrome inyectaHeadlessChromeen el UA ysec-ch-ua. La biblioteca lo elimina y aplicauserAgentMetadatacoincidentes para quenavigator.userAgentDatase mantenga consistente. - Nombres de mundo aislado aleatorizados por pestaña (un token
u…aleatorio, no un marcador fijo como una constanteutilo el__playwright_utility_world__de Playwright) para que el nombre del mundo no pueda fijarse como una firma estática entre sesiones si alguna vez se filtra (p. ej. a través de un marco de pilaError). - Flags de lanzamiento evitan opciones cuya mera presencia es una huella digital (
--disable-popup-blocking,--disable-component-update,--disable-extensions,--enable-automation). - Entrada humana. Los movimientos del ratón siguen un perfil de velocidad min-jerk con tiempo escalado por distancia, temblor en vuelo y sobreimpulso balístico + corrección; la escritura usa tiempos lognormales de permanencia/vuelo con solapamiento ocasional de teclas.
tab.mouse.idle()añade deriva de cursor ambiental no periódica (útil antes de ungrecaptcha.execute()). - Persona coherente. La cadena UA, Client-Hints (
platform,platformVersion,fullVersionListreal,architecture), identidad WebGL y geo (zona horaria/idioma/Accept-Language) se derivan de un único User-Agent resuelto para que las capas no puedan contradecirse entre sí — un desajuste entre capas es la señal de mayor peso.
Un pequeño conjunto de parches JS condicionales y defensivos se inyecta por defecto (alternar con evasions: false): navigator.webdriver se fuerza a un getter false presente solo si el flag de lanzamiento no lo hizo ya; Notification.permission se reconcilia con navigator.permissions.query (el PermissionStatus real se devuelve con solo su state mapeado, por lo que default → el prompt válido); y window.screen/outerWidth se normalizan en una geometría de ventana maximizada coherente cuando se establece un screen/windowSize. Cada parche mantiene su toString() nativo y no hace nada cuando el valor ya era correcto (parchear en exceso un valor correcto es en sí mismo una señal). navigator.hardwareConcurrency se fija mediante Emulation.setHardwareConcurrencyOverride; esa anulación de sesión de página no alcanza los objetivos de worker, por lo que la biblioteca lo reemite en la sesión CDP de cada worker (y reanuda el worker, que de otro modo se colgaría en pausa bajo auto-attach) — manteniendo navigator.hardwareConcurrency coherente entre el hilo principal y los Web/Shared/Service Workers, ya que un desajuste principal-vs-worker es una señal más fuerte que el valor en sí. deviceMemory se no falsifica deliberadamente (no tiene anulación por objetivo, por lo que un parche solo en el mundo principal se desincronizaría de los workers). Añade el tuyo con tab.addInitScript({ source }).
Auditado contra bot-detector.rebrowser.net (sin runtimeEnableLeak, sin navigatorWebdriver, ejecución en mundo aislado), bot.sannysoft.com (0 fallos) y CreepJS (0% headless, 0% sigilo).
Vuelve a medir en tu binario de producción. Esos resultados se tomaron en un Chrome de escritorio. En un Lambda sin GPU que ejecuta chrome-headless-shell (
@sparticuz/chromium) las superficies difieren — ausencia dewindow.chrome/navigator.plugins, WebGL SwiftShader, códecs — que es exactamente lo quechannel:'headless-shell'cubre. Vuelve a ejecutar las suites contra el binario real de sparticuz y trátalo como la fuente de verdad.
WebGL / GPU en hosts en la nube
En tu máquina, WebGL informa tu GPU real. En un servidor en la nube sin GPU, Chrome cae a SwiftShader/llvmpipe, y SwiftShader / llvmpipe en el renderizador es una señal headless conocida. Pero la falsificación tiene dos bordes afilados que debes respetar:
- Nunca afirmes un SO al que el renderizador no pueda pertenecer. Una cadena de renderizador
Direct3D11/D3D11existe solo en Windows. Emitirla bajo un UA de Linux es una combinación imposible que DataDome/Picasso bloquea duramente — estrictamente peor que la cadena de software honesta.spoofWebGL: trueahora deriva un valor por defecto coherente con el SO del UA resuelto (Linux→Mesa/ANGLE OpenGL, Windows→D3D11, macOS→Metal); nunca pondrá una GPU de Windows en un UA de Linux. Si pasas unwebglRendererexplícito, la biblioteca advierte cuando su backend contradice el SO del UA (una cadena D3D11 bajo Linux/macOS, una cadena Mesa/SwiftShader bajo Windows/macOS, una cadena Metal bajo Windows/Linux). - Una falsificación solo de
getParameterno puede vencer un render forzado. La anulación cambia las cadenas de vendor/renderizador, pero la lista de extensiones, los límites de parámetros y el hash de píxeles renderizado real aún provienen de SwiftShader. Los anti-bots de primer nivel (DataDome Picasso, Cloudflare, Kasada) fuerzan un render y verifican de forma cruzada, por lo que una cadena de GPU discreta bajo render por software es detectable. La falsificación es solo para objetivos blandos.
// 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)",
});
Guía de decisión para el objetivo Lambda / Linux + --headless=new:
- Objetivos duros (DataDome/Cloudflare/Kasada): una falsificación de cadena no sobrevivirá a un hash de render. Ejecuta una GPU real (EC2
g4dn/g5+ driver NVIDIA) y desactiva la falsificación — deja que WebGL informe el hardware genuino e internamente coherente. Esta es la única configuración que pasa Picasso. - Objetivos blandos:
spoofWebGL: true(cadena Linux coherente) es suficiente.
En --headless=new, la biblioteca siempre añade --enable-unsafe-swiftshader: Chrome 137+ eliminó la caída automática a WebGL por software, por lo que sin él getContext('webgl') devuelve null en un host sin GPU — en sí mismo una señal, y hace que cualquier falsificación sea inerte. Con una GPU real presente, la GPU aún se usa.
La anulación se ejecuta en el mundo principal de la página y mantiene getParameter.toString() con aspecto nativo. Limitación conocida: apunta al hilo principal, no a Web Workers/OffscreenCanvas.
La pila de producción
proxy · VPS · solver — las tres cosas que deciden si un scrape pasa. Esta biblioteca maneja el navegador; estas manejan todo lo demás, y el propio consejo de la biblioteca apunta directamente a ellas. Descuentos para lectores donde pude conseguirlos.
| Necesitas | Por qué — directamente del propio consejo de esta biblioteca | Recomendado (con descuento) |
|---|---|---|
| Proxy residencial / móvil | Tu reputación de IP es el techo — un ASN de centro de datos pierde antes de que tu JavaScript siquiera se ejecute. La palanca más grande. | IPRoyal — el pool más barato que aún pasa · Bright Data — premium, para los objetivos más duros |
| Un VPS para ejecutarlo 24/7 | Los scrapes programados quieren una caja Linux siempre encendida, no tu portátil. La ruta de WebGL por software sin GPU funciona bien aquí para objetivos blandos/medios. | Hostinger VPS — 20% de descuento a través de este enlace |
| Un solucionador de CAPTCHA (solo cuando sea forzado) | La biblioteca no es un solucionador por diseño — entrega los desafíos de imagen/casilla y arregla tu IP primero. | 2Captcha · Anti-Captcha |
Para objetivos duros con hash de render (DataDome Picasso, Kasada) sube de un VPS barato a un EC2 con GPU real g4dn/g5 y desactiva spoofWebGL — detalles en Implementación en la nube a continuación.
Los enlaces de proxy, VPS y captcha son enlaces de afiliado/de pago — puedo ganar una comisión sin costo extra para ti, y tú obtienes el descuento. Son las herramientas por las que enruto mi propio tráfico.
Implementación en la nube (Lambda vs EC2-GPU)
La señal dominante es tu IP, no tu huella digital. Un Chrome headless impecable en una IP de salida de AWS aún puntúa bajo en reCAPTCHA v3 y recibe desafíos de Cloudflare/DataDome, porque los ASN de centros de datos se penalizan antes de que tu JavaScript se ejecute. Enruta los objetivos protegidos a través de un proxy residencial o móvil — IPRoyal es el pool residencial más barato que he encontrado que aún pasa (enlace de pago); Bright Data es la opción premium para los objetivos más duros. La biblioteca advierte sobre la salida directa (
warnOnDirectEgress) ybrowser.checkEgress()informa si tu IP de salida está marcada como hosting. Lambda — ZIP runtime +@sparticuz/chromium(la ruta compatible). Elgoogle-chrome-stablereal no se ejecuta en el runtime ZIP de Lambda: muere bajo el sandbox namespace/seccomp de Firecracker incluso en un solo proceso (credentials.cc Operation not permitted, "Zygote could not fork"), y un Chrome completo no cabe en el límite de ~250 MB del zip descomprimido. Usa@sparticuz/chromium(una dependencia opcional — instálala y fíjala, y mantenlaexternalen tu bundler para que resuelva su binario por ruta relativa). Incluye chrome-headless-shell, que ya es headless (--headless='shell'); la librería lo detecta víachannel:'headless-shell'y: nunca añade--headless=new, gestiona los flags de software-WebGL, asegura que los directoriosHOME/XDG_*/user-data existan bajo/tmp, apuntaDBUS_SESSION_BUS_ADDRESSa/dev/null, y aplica automáticamente los shims de shell (window.chrome,navigator.connection) que un Chrome headful real tiene de forma nativa.
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)
});
Esto es un trade-off marginal anti-bot (chrome-headless-shell + SwiftShader, sin códecs propietarios), aceptado porque para reCAPTCHA v3 la reputación de IP domina. Una build
--headless=newde Chromium completo no merece la pena empaquetarla: no cabe en el zip, muere en Firecracker, y sus peores señales (SwiftShader WebGL, sin GPU) provienen del host sin GPU, no del modo headless — así que son idénticas a las del shell. Dimensiona el/tmpde la función (almacenamiento efímero) más allá del default de 512 MB: el binario extrae ~130 MB allí, más el perfil user-data.
geoCountry debe coincidir con el país de salida del proxy (un reloj UTC o una locale pt-BR en una IP alemana es una señal clara). Si no lo sabes de antemano, establece autoGeo: true en lugar de geoCountry y el primer newTab() buscará el país de salida a través del proxy y lo establecerá por ti (una petición extra al inicio; geoCountry/timezone/locale explícitos siempre ganan, y un país no mapeado avisa en lugar de adivinar).
EC2 g4dn/g5 (GPU NVIDIA real): la postura para objetivos difíciles. Verifica la GPU en chrome://gpu, deja spoofWebGL desactivado, y sigue usando un proxy residencial/móvil.
¿Sin AWS? Un VPS normal también sirve. Para objetivos suaves/medios y trabajos programados, un VPS barato siempre encendido es el host de menor esfuerzo — Hostinger VPS (20% de descuento vía ese enlace) ejecuta la ruta software-WebGL sin GPU perfectamente. Misma regla aplica: combínalo con un proxy residencial/móvil, y solo sube a una caja EC2 con GPU real para objetivos difíciles por hash de render.
Identidad y persistencia
Un perfil temporal nuevo en cada ejecución no acumula reputación, y las cookies forjadas de Google/_GRECAPTCHA no compran nada (Google las acuña y valida en el servidor — el seeder ya no las fabrica). Para objetivos protegidos por reCAPTCHA-v3, mantén un perfil persistente por identidad fijado a una IP de proxy sticky y caliéntalo para que los servidores acuñen cookies reales:
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
Un perfil calentado detrás de una IP de datacenter rotatoria no gana nada: una identidad, una IP sticky, una huella.
Inyecta cookies calentadas vía CDP, no el archivo SQLite.
ProfileManager.seedCookies()(la escritura SQLite previa al lanzamiento) es inerte en Chrome ≥ 80: Chrome lee elencrypted_valuecifrado con OSCrypt e ignora elvalueen texto plano, así que las cookies sembradas por archivo se descartan al cargar (el auto-seed ahora solo siembra historial, que la ruta SQLite maneja bien). Para inyectar un lote de cookies calentadas que realmente persista, establécelo en tiempo de ejecución a través del almacén cifrado de Chrome:await tab.setCookies({ cookies })— cifrado con la clave del perfil vivo.ProfileManager.cookieSeeds()devuelve el lote de baja clave por defecto. Fija cada lote a la misma IP residencial sticky en la que se calentó.
WebRTC
WebRTC se deja habilitado (deshabilitarlo por completo es en sí una anomalía) pero bloqueado para prevenir fugas de IP: detrás de un proxy fuerza todo el UDP a través del proxy (disable_non_proxied_udp); de lo contrario, expone solo la interfaz pública y oculta las IPs locales (default_public_interface_only).
Alternativas — cómo se compara
Si buscas una alternativa indetectable a Puppeteer / Playwright, aquí es donde se sitúa esta librería. Apunta al mismo trabajo que Puppeteer estándar + puppeteer-extra-plugin-stealth o puppeteer-real-browser, pero parte de CDP crudo en lugar de parchear un driver de alto nivel después del hecho.
| Capacidad | Puppeteer / Playwright | + puppeteer-extra-plugin-stealth | puppeteer-real-browser | browser-scraper |
|---|---|---|---|---|
| Transporte | API de alto nivel (habilita Runtime) | mismo + parches JS | Chrome real + parches | CDP crudo |
Fuga de Runtime.enable | presente | parcheada | mitigada | nunca enviada (ejecución en mundo aislado) |
| Atravesar Shadow DOM cerrado en una llamada | ✗ | ✗ | ✗ | ✓ find() |
Iframe cross-origin en el mismo find() | ✗ (frames manuales) | ✗ | ✗ | ✓ |
| Persona coherente (UA + Client-Hints + WebGL + timezone de un solo UA) | ✗ | parcial | parcial | ✓ |
| Entrada humana (mouse min-jerk, tecleo lognormal) | ✗ | ✗ (añade ghost-cursor) | parcial | ✓ integrado |
| Ruta AWS Lambda / chrome-headless-shell | manual | manual | ✗ | ✓ resolveSparticuz() |
Esta es una herramienta de precisión, no un bypass mágico de captchas: contra objetivos difíciles (Cloudflare, DataDome, Kasada) tu reputación de IP sigue dominando — combínala con un proxy residencial/móvil (ver Cloud deployment).
Para agentes de IA
@playwright/mcp | Stagehand | Firecrawl / scrapers alojados | browser-scraper | |
|---|---|---|---|---|
| Se ejecuta localmente, sin API key | ✓ | ✓ (modelo BYO) | ✗ (API de pago) / auto-alojado | ✓ |
Stealth (sin Runtime.enable, persona coherente, entrada humana) | ✗ | vía nube Browserbase | del lado del servidor | ✓ integrado |
| Resolución de Cloudflare Turnstile | ✗ | solo nube | ✓ (de pago) | ✓ solveTurnstile() |
| Snapshot alcanza raíces shadow cerradas | ✗ | ✗ | n/a | ✓ |
| HTML → Markdown, metadatos | ✗ | parcial | ✓ | ✓ |
| Selectores generados una vez, luego sin LLM | ✗ | caché de acciones | ✗ | ✓ generateExtractor() |
| Coste por página | gratis | LLM por acción | por crédito | gratis (LLM opcional) |
Usa Stagehand o browser-use si quieres un bucle de agente autónomo. Usa browser-scraper como la capa de navegador bajo un agente (Claude, Cursor, el tuyo propio), o cuando necesites principalmente contenido de páginas protegidas de forma económica.
FAQ
¿Cómo hago que Puppeteer sea indetectable / pase la detección de bots en Node.js?
Las señales detectables son la fuga CDP de Runtime.enable, el User-Agent/client-hints de HeadlessChrome, y los desajustes de huella entre capas. Esta librería evita Runtime.enable por completo (todo el JS se ejecuta vía Page.createIsolatedWorld + Runtime.evaluate), elimina el UA headless, y deriva una persona coherente — ver Stealth, in depth. Verificado contra bot.sannysoft.com (0 fallos), CreepJS (0% headless), y bot-detector.rebrowser.net.
¿Cómo evito Cloudflare Turnstile en Node.js?
La casilla de Turnstile suele vivir dentro de un iframe sandboxed y cross-origin. Un solo find() la alcanza (y cualquier shadow root cerrado) para que puedas click() como un usuario real — o llama a tab.solveTurnstile() para la ruta de una sola llamada (espera el widget, hace clic humano en la casilla, y sondea el token). Ver Pierce closed Shadow DOM + cross-origin iframes y el ejemplo de examples/cloudflare-turnstile.ts. Detrás de una IP residencial limpia, esto pasa el intersticial de Cloudflare; la IP, no el clic, es el factor dominante.
¿Cómo accedo o consulto un Shadow DOM cerrado?
Puppeteer y Playwright no pueden atravesar raíces shadow cerradas. find() / findAll() buscan en el documento principal, raíces shadow cerradas, e iframes cross-origin (OOP) en una sola llamada — sin recorrido de shadowRoot, sin plomería de frames.
¿Qué es la fuga de detección CDP de Runtime.enable y cómo se evita?
Llamar a Runtime.enable (que la mayoría de los stacks de automatización hacen al inicio) dispara señales del dominio Runtime como Runtime.consoleAPICalled que los proveedores anti-bot identifican. Esta librería nunca habilita el dominio Runtime — la ejecución ocurre en un mundo aislado nombrado vía Runtime.evaluate, que no lo hace.
¿Cómo ejecuto Chrome headless en AWS Lambda para scraping?
Usa la build opcional de @sparticuz/chromium y resolveSparticuz(); la librería detecta chrome-headless-shell y aplica los flags, directorios y shims correctos automáticamente — ver Cloud deployment (Lambda vs EC2-GPU).
Puppeteer vs Playwright para scraping sigiloso — ¿cuál debería usar?
Ambos son drivers de alto nivel que habilitan el dominio Runtime y no pueden atravesar raíces shadow cerradas, así que para scraping sensible a detección terminas añadiendo plugins de stealth. Esta librería es la alternativa CDP-first construida para ese caso desde el inicio — ver Alternatives.
¿Cómo le doy a Claude / Cursor un navegador que no esté bloqueado?
Ejecuta el servidor MCP incluido: claude mcp add browser-scraper -- npx -y @eurafaeldecarvalho/browser-scraper mcp. El agente obtiene navigate / snapshot / click / type / markdown / solve_turnstile en un Chrome sigiloso. Ver MCP server.
¿Cómo convierto un sitio web a Markdown para un pipeline LLM / RAG?
scrape({ url, formats: ["markdown"] }) devuelve el Markdown limpio del contenido principal, escalando de HTTP plano al navegador sigiloso solo cuando es necesario. Para campos estructurados, usa extract() con un esquema Zod, o generateExtractor() para pagar al LLM una vez por diseño de sitio en lugar de una vez por página.
¿Resuelve CAPTCHAs o reCAPTCHA v3?
No resuelve CAPTCHAs de imagen/token. Lo que hace es mantener tu puntuación v3 de reCAPTCHA alta — mouse/tecleo similares a humanos, actividad ambiental antes de una llamada de acuñación de token, y un perfil persistente calentado en una IP residencial sticky — ver Identity & persistence. Para desafíos de imagen v2, combínalo con un servicio de resolución.
Notas y limitaciones
- La detección de contenido principal de
markdown()es heurística (etiquetas semánticas, roles, pistas de clases, densidad de enlaces), no Readability. UsamainContent: falsecuando recorte demasiado. snapshot()usa el dominio de Accesibilidad de CDP, la misma fuente que usa la instantánea de Playwright. Los selectores degenerateExtractor()se ejecutan conquerySelectorAllen el mundo aislado, por lo que no llegan a shadow roots cerrados; para esos, usaextract()(entrada Markdown/instantánea) oobserve().- No se inyecta suplantación de huella de JavaScript por defecto en un Chrome de escritorio normal: el comportamiento nativo es más difícil de detectar que las API parcheadas. Las ganancias de sigilo provienen de evitar
Runtime.enable, limpiar el UA/client-hints sin cabeza, nombres de mundo genéricos, banderas de lanzamiento cuidadosas y entrada similar a la humana. Trae tus propios parches mediantetab.addInitScriptsi un objetivo los necesita. - Bajo
channel:'headless-shell'la librería aplica un pequeño conjunto de shims condicionales que cierran dos pistas específicas solo de shell — no afirma hacer que el binario de shell sea indistinguible de un Chrome real con cabeza. Los shims, cada uno un no-op cuando el valor ya es real y escrito para coincidir con la forma de descriptor nativa de Chrome (propiedad de datos propia enumerable parawindow.chrome; accesores de prototipo contoStringde aspecto nativo paranavigator.connection): unwindow.chrome/chrome.runtimemínimo (solo cuando falta) y unnavigator.connection.rttdistinto de cero (solo cuando lee0). Las brechas residuales de shell se dejan deliberadamente intactas (un adaptadornavigator.plugins/WebGPU fabricado es en sí mismo una pista — consulta el documento de investigación) y siguen siendo limitaciones de objetivo blando; la palanca dominante sigue siendo la IP. La librería también enhebra la marca GREASE real y viva (leída denavigator.userAgentData) en los Client-Hints en lugar de un valor codificado que rota obsoleto, y verifica que exista un contexto WebGL antes de instalar la suplantación de cadena WebGL (una suplantación solo degetParameteres inerte — e incoherente — sin contexto detrás; contra anti-bots de hash de renderizado como DataDome Picasso es peor que honesto, así que manténspoofWebGLsolo para objetivos blandos). - La suplantación de cadenas WebGL es solo para objetivos blandos — no puede vencer un hash de renderizado forzado. Para objetivos difíciles, ejecuta una GPU real.
- La señal dominante es tu reputación de IP, no tu huella. Los ASN de centros de datos pierden antes de que se ejecute el JavaScript; enruta objetivos protegidos a través de un proxy residencial/móvil (IPRoyal con presupuesto ajustado, Bright Data para objetivos difíciles — enlaces de pago).
- Esta librería no resuelve captchas — por diseño. Su postura es reputación de IP + comportamiento similar al humano, no forja de tokens (los tokens
_GRECAPTCHAforjados no compran nada). Si un objetivo lanza un desafío interactivo de imagen/casilla que debes superar genuinamente, entrégalo a un servicio de resolución — 2Captcha o Anti-Captcha (enlaces de pago). Arregla tu IP primero: un solucionador en una IP de centro de datos marcada aún pierde.
Soporte
Esta librería se construye y mantiene en abierto. Si te ahorró el trabajo de combatir fugas de Runtime.enable, recorrido de shadow-DOM y coherencia de huellas por tu cuenta, una pequeña contribución mantiene el desarrollo en marcha:
- Patrocina en GitHub — única o mensual
- Invítame a un café en Ko-fi — propina rápida única
Dar estrella al repositorio también ayuda — es cómo otros desarrolladores encuentran el proyecto.
Algunos enlaces de proxy y captcha en este README son enlaces de pago — puedo ganar una comisión sin costo adicional para ti. Son herramientas por las que enrutaría mi propio tráfico.
Licencia
MIT. Las versiones hasta 0.7.0 se publicaron bajo PolyForm Noncommercial 1.0.0; desde 0.8.0 la librería es MIT.