Puppeteer es la librería de Node.js de Google para controlar Chrome (y Firefox) mediante el Chrome DevTools Protocol o WebDriver BiDi. Cuando una página solo entrega datos después de ejecutar JavaScript, un simple fetch devuelve una cáscara vacía: necesitas un navegador real. Esta guía es la ruta práctica en 2026: instalar, hacer scraping de un sandbox con JS, bloquear recursos pesados, interceptar JSON, ser honesto sobre la detección y saber cuándo dejar de ejecutar Chromium por tu cuenta.
Si eres nuevo en el scraping, empieza con web scraping con Python para el nivel de HTML estático. Para la opción multi-navegador por defecto en Python, consulta web scraping con Playwright. Este artículo es para equipos de Node.js que ya quieren usar Puppeteer.
¿Por qué Puppeteer para hacer scraping?
| Situación | Herramienta adecuada |
|---|---|
| Los datos están en el HTML inicial | fetch + un parser (cheerio) — omite el navegador |
| Los datos llegan desde una API JSON que la página invoca | Llama a esa API directamente después de encontrarla en DevTools |
| Los datos se renderizan solo después de JavaScript | Puppeteer (o Playwright) |
| UI de varios pasos (clic, escritura, scroll) | Puppeteer (o Playwright) |
| Capturas de pantalla / PDFs | Puppeteer |
Puppeteer sigue siendo la opción natural cuando tu stack ya es Node, solo necesitas Chromium y quieres un wrapper ligero sobre CDP. Playwright gana en cobertura multi-navegador y en la ergonomía de las esperas automáticas para proyectos nuevos; más adelante en este artículo verás la comparación.
Instalación y tu primer scraping
npm i puppeteer
# Si tu gestor de paquetes bloquea los scripts de instalación:
npx puppeteer browsers install
puppeteer descarga una versión de Chrome compatible. Usa puppeteer-core cuando apuntes a un Chrome del sistema o a un servicio de navegador remoto.
Aquí tienes un scraper completo contra quotes.toscrape.com/js, un sandbox que renderiza citas con JavaScript (el HTTP plano devuelve cero citas):
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto("https://quotes.toscrape.com/js/", {
waitUntil: "domcontentloaded",
});
await page.waitForSelector(".quote");
const quotes = await page.$$eval(".quote", (cards) =>
cards.map((card) => ({
text: card.querySelector(".text")?.textContent?.trim() ?? "",
author: card.querySelector(".author")?.textContent?.trim() ?? "",
tags: [...card.querySelectorAll(".tag")].map((t) => t.textContent.trim()),
})),
);
console.log(`scraped ${quotes.length} quotes`);
console.log(quotes[0]);
await browser.close();
Notas que ahorran horas más adelante:
waitUntil: "domcontentloaded"normalmente es suficiente;networkidle0se queda colgado en páginas con analítica o websockets.waitForSelector(opage.locator(...).wait()) sustituye las conjeturas consetTimeout.- Un viewport explícito evita el tamaño diminuto por defecto del modo headless, que activa diseños móviles y hace que falten selectores de escritorio.
Extraer datos de forma limpia
Tres patrones cubren la mayoría de los scrapers:
// 1) Un solo campo
const title = await page.$eval("h1", (el) => el.textContent.trim());
// 2) Muchos registros ($$eval se ejecuta en el contexto de la página — rápido)
const rows = await page.$$eval("article.product", (nodes) =>
nodes.map((n) => ({
name: n.querySelector("h2")?.textContent?.trim(),
price: n.querySelector(".price")?.textContent?.trim(),
})),
);
// 3) API de Locator (Puppeteer 22+) — esperas con reintento automático
await page.locator("button#load-more").click();
Pagina haciendo clic en «Siguiente» dentro de un bucle, o recorriendo URLs ?page=N cuando el sitio las expone. Limita la concurrencia; cada página abierta es un proceso de navegador real.
Funciones avanzadas para scrapers
Bloquear imágenes, fuentes y medios
Un scraper no necesita píxeles. Aborta los tipos de recursos pesados antes de navegar:
await page.setRequestInterception(true);
page.on("request", (req) => {
const type = req.resourceType();
if (type === "image" || type === "font" || type === "media") {
req.abort();
} else {
req.continue();
}
});
await page.goto("https://quotes.toscrape.com/js/");
Espera un ahorro considerable de ancho de banda y CPU en catálogos con muchas imágenes. Dos advertencias: bloquear las hojas de estilo puede romper selectores que dependen de la visibilidad, y eliminar la analítica en algunos sitios anti-bot puede hacerte más sospechoso.
Interceptar JSON en lugar de hacer scraping del DOM
Si la pestaña Network de DevTools muestra un XHR que ya devuelve datos estructurados, captúralo:
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
/** @type {any[]} */
const payloads = [];
page.on("response", async (res) => {
const url = res.url();
if (!url.includes("/api/") || res.request().resourceType() !== "xhr") return;
try {
payloads.push(await res.json());
} catch {
// no es JSON
}
});
await page.goto("https://example.com/catalog", { waitUntil: "networkidle2" });
console.log(payloads[0]);
await browser.close();
Cuando funciona, esto supera a los selectores CSS: campos tipados, menos roturas por rediseños y, a menudo, campos extra que la interfaz nunca muestra. Revisa la pestaña Network antes de escribir un solo locator.
Páginas en paralelo sin N navegadores
Lanza un solo navegador, abre muchas páginas (o contextos de navegador) y limita la velocidad con un pool sencillo:
import puppeteer from "puppeteer";
async function mapPool(items, limit, worker) {
const ret = [];
let i = 0;
await Promise.all(
Array.from({ length: limit }, async () => {
while (i < items.length) {
const idx = i++;
ret[idx] = await worker(items[idx]);
}
}),
);
return ret;
}
const browser = await puppeteer.launch({ headless: true });
const urls = [
"https://quotes.toscrape.com/js/page/1/",
"https://quotes.toscrape.com/js/page/2/",
];
const results = await mapPool(urls, 3, async (url) => {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: "domcontentloaded" });
await page.waitForSelector(".quote");
return page.$$eval(".quote .text", (els) => els.map((e) => e.textContent));
} finally {
await page.close();
}
});
await browser.close();
console.log(results.flat().length);
De tres a cinco páginas concurrentes por máquina es un punto de partida realista. La concurrencia sin límite satura la memoria del host (OOM) y hace que el objetivo te limite la tasa de peticiones.
Fallos comunes y cómo solucionarlos
| Fallo | Causa probable | Solución |
|---|---|---|
| Timeout esperando un selector | CSS incorrecto, contenido dentro de un iframe, o el JS nunca se ejecutó | Ejecuta en modo visible (headless: false); usa page.frames() / locators de frame; espera una respuesta de red |
| Scraping vacío, HTML con 200 | Cáscara de SPA; los datos se cargan después vía XHR | waitForSelector sobre el nodo ya hidratado, o intercepta el JSON |
| Funciona en modo visible, falla en headless | Diferencias de fingerprint/viewport; carga diferida (lazy load) fuera de pantalla | Configura el viewport + un user agent realista; haz scroll con page.mouse.wheel |
net::ERR_FAILED durante la interceptación | Olvidaste req.continue() en algunas peticiones | Continúa o aborta siempre explícitamente en el manejador de la petición |
| El objetivo devuelve CAPTCHA / 403 «suave» | IP de datacenter + señales de automatización | Los proxies residenciales ayudan; los plugins de stealth ayudan un poco; a volumen, usa una API gestionada |
¿Se puede detectar Puppeteer? Sí
Recién instalado, un Chrome headless controlado por Puppeteer es fácil de detectar. Señales comunes:
navigator.webdriver === true- User agent de headless / plugins ausentes / cadenas extrañas en el renderer de WebGL
- Artefactos del runtime de CDP que los scripts de detección buscan activamente
- Reputación de IP de datacenter sumada al fingerprint
La solución DIY habitual es puppeteer-extra con puppeteer-extra-plugin-stealth:
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto("https://example.com");
await browser.close();
Stealth todavía ayuda contra comprobaciones ingenuas. Contra Cloudflare, DataDome, PerimeterX y similares en 2026, es una carrera armamentista en la que estás en el bando perdedor: puntuación de comportamiento, fingerprinting de TLS y pruebas de laboratorio continuas contra los stacks de automatización conocidos. Combina cualquier navegador con proxies residenciales y aun así espera CAPTCHAs y datos basura silenciosos en objetivos difíciles. La escalera de escalamiento está en sitios que bloquean bots.
Puppeteer vs Playwright vs Selenium
| Puppeteer | Playwright | Selenium 4 | |
|---|---|---|---|
| Lenguaje de origen | Node.js (oficial) | Python, Node, Java, .NET | Muchos (WebDriver) |
| Navegadores | Chrome/Firefox (CDP/BiDi) | Chromium, Firefox, WebKit | Todos los principales vía drivers |
| Espera automática | Locators mejorando; todavía a menudo manual | Locators de primera clase | WebDriverWait explícito |
| Control de red | Interceptación de peticiones + CDP | page.route incorporado | Históricamente necesita complementos |
| Ecosistema de stealth | Maduro (puppeteer-extra) | Portado / patchright | undetected-chromedriver, etc. |
| Mejor encaje | Scrapers de Node + Chromium | Scrapers nuevos multi-navegador | Grids heredados / QA multi-lenguaje |
Elige Puppeteer cuando ya trabajas con Chrome como prioridad en Node y ya tienes inversión hecha. Elige Playwright para un scraper nuevo que pueda necesitar WebKit/Firefox o Python. Elige Selenium cuando Grid y las suites de WebDriver existentes dominan tu stack. Ninguno de los tres está libre de detección; todos son costosos a escala de flota.
Cuándo dejar de ejecutar navegadores
Una página de navegador necesita una porción real de CPU y cientos de MB de RAM. Un host que puede enviar miles de peticiones HTTP planas por minuto tal vez solo sostenga unas pocas decenas de páginas de Puppeteer concurrentes. Suma proxies residenciales y resolución de CAPTCHA, y la economía unitaria se derrumba.
Para páginas públicas donde solo necesitas HTML o Markdown limpio, la API de web scraping de Crawlora (POST /api/v1/web/scrape) ejecuta la petición (y escala a un navegador headless cuando render: "auto" lo requiere) detrás de una sola clave:
curl -X POST "https://api.crawlora.net/api/v1/web/scrape" \
-H "x-api-key: $CRAWLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "formats": ["markdown", "html"], "render": "auto"}'
const res = await fetch("https://api.crawlora.net/api/v1/web/scrape", {
method: "POST",
headers: {
"x-api-key": process.env.CRAWLORA_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com",
formats: ["markdown", "html"],
render: "auto",
}),
});
const { data } = await res.json();
console.log(data.markdown?.slice(0, 400));
console.log("fetch method:", data.scrape?.method); // "http" | "browser" | ...
Para las plataformas que Crawlora ya normaliza (Amazon, Reddit, SERP de Google, …), prefiere el endpoint de plataforma en lugar de un navegador genérico: obtienes JSON tipado en vez de mantener selectores. El plan gratuito ofrece 2,000 créditos/mes, sin tarjeta; la facturación es pay-on-success (pagas solo si funciona). Consulta los precios, prueba cualquier URL en el Free Web Scraper, o abre el playground.
- HTML estático — usa fetch + cheerio (o Python requests + BeautifulSoup), no Puppeteer.
- Renderizado con JS, con protección ligera — Puppeteer con bloqueo de recursos e interceptación de respuestas.
- Los datos ya llegan como JSON vía XHR — interceptalos; omite el DOM.
- WAFs difíciles a volumen — un /web/scrape gestionado o una API de plataforma superan a una granja de stealth.
- Cargas de trabajo mixtas — conserva Puppeteer para los flujos de UI de cola larga; usa la API para objetivos de alto volumen.
Evita la granja de Chromium para páginas públicas protegidas
Endpoints documentados, renderizado y proxies gestionados, facturación pay-on-success. 2,000 créditos gratis al mes, sin tarjeta.
Lecturas relacionadas
- Web scraping con Playwright — la contraparte multi-navegador en Python
- Web scraping con Selenium — la ruta de WebDriver y cuándo todavía tiene sentido
- Scraping de sitios que bloquean bots — la escalera de escalamiento de detección
- Proxies para web scraping explicados — economía de residenciales vs. datacenter
- Mejores APIs de web scraping en 2026 — cuándo comprar la capa de acceso
Preguntas frecuentes
Is Puppeteer good for web scraping in 2026?
Yes for Node.js teams that need Chromium to render JavaScript, click through UI flows, or capture screenshots. It is a poor default for static HTML (use fetch + cheerio) and expensive at fleet scale against modern anti-bot vendors.
Is Puppeteer better than Playwright for scraping?
For Chrome-only Node stacks, Puppeteer is a thin, mature CDP wrapper with a strong stealth-plugin ecosystem. Playwright is usually better for greenfield scrapers that need multi-browser coverage, first-class auto-waiting locators, and first-class Python support.
Can websites detect Puppeteer?
Yes. Default headless Chrome exposes navigator.webdriver, headless fingerprints, and CDP artifacts. puppeteer-extra-plugin-stealth patches obvious leaks, but Cloudflare, DataDome, and similar vendors also score TLS fingerprints, IP reputation, and behavior — stealth alone is not enough at volume.
How do I scrape a page that loads data with JavaScript?
Launch Puppeteer, navigate, wait for the selector that appears after hydration (or intercept the XHR that returns JSON), then extract with $$eval or the locator API. Prefer intercepting the site's own JSON when DevTools shows it — it is more stable than DOM scraping.
Why does networkidle hang in Puppeteer?
Pages with analytics, websockets, or long-polling never go fully idle, so networkidle0/2 can burn the full timeout. Prefer waitUntil domcontentloaded plus waitForSelector (or a specific response) for the content you need.
When should I use a scraping API instead of Puppeteer?
When targets are behind hard WAFs, you need high concurrency, or browser RAM and proxy ops cost more than the data. A managed POST /web/scrape or a platform-specific JSON endpoint removes the fleet, stealth patches, and CAPTCHA treadmill from your stack.
Is Puppeteer free?
Yes — open source under Apache-2.0. Your real costs are compute (hundreds of MB RAM per page), proxies on protected sites, and engineering time to keep selectors and stealth patches working.
