web scraping with javascript

Esta guía explica cómo hacer web scraping con JavaScript usando Node.js, el entorno de ejecución que la mayoría de los ingenieros eligen cuando quieren recopilar datos públicos a escala. Aprender a hacer web scraping con JavaScript significa elegir la herramienta correcta para la página: un cliente HTTP ligero para HTML estático, o un navegador headless para contenido que se carga mediante scripts.

A continuación encontrarás código funcional de Node.js, listo para ejecutar, para ambos casos, además de la configuración del proyecto, exportaciones estructuradas a JSON y CSV, paginación, reintentos con backoff, limitación de velocidad, autenticación de proxy, interceptación de XHR, y los problemas de ética y bloqueo que determinan si un scraper sigue funcionando con el tiempo.

DataImpulse es un proveedor de proxies éticos que ofrece más de 90 millones de direcciones IP residenciales, móviles y de datacenter en 195 países. Utiliza un modelo de pago por uso desde 1 dólar por GB con tráfico que no caduca, y se usa para web scraping, verificación de anuncios, monitoreo de precios, investigación de mercado y gestión de múltiples cuentas.

Datos Clave

  • Dos conjuntos de herramientas: Las páginas estáticas solo necesitan un cliente HTTP más un parser como fetch y cheerio, mientras que las páginas dinámicas que renderizan contenido en el navegador necesitan un motor headless como Puppeteer o Playwright.
  • Mejor tipo de proxy: proxies residenciales rotativos, que usan IPs reales de consumidores y superan la detección.
  • Precio: desde 1 dólar por GB, pago por uso, con tráfico que no caduca y sin suscripción.
  • Cobertura: más de 90M de IPs obtenidas éticamente en 195 países.
  • Fiabilidad: tasa de éxito del 99.51%, calificado con 4.8 de 5 en G2.
  • Protocolos y segmentación: HTTP, HTTPS y SOCKS5, con segmentación por país incluida.
Estático vs dinámico: elige tu ruta de scraping con JavaScript

¿Qué es el web scraping con JavaScript?

El web scraping con JavaScript es el proceso de solicitar páginas web de forma programática y extraer datos estructurados de ellas usando JavaScript, normalmente ejecutado en el entorno Node.js en lugar de dentro de una pestaña del navegador.

JavaScript es una opción natural porque el lenguaje ya modela el Document Object Model, por lo que seleccionar elementos resulta familiar para los desarrolladores front-end. En la práctica, un scraper hace tres cosas: obtiene una página por HTTP, analiza el markup devuelto en un árbol consultable, y extrae los campos que te interesan, como títulos, precios o enlaces. La complejidad viene de cómo la página objetivo entrega sus datos, por lo que elegir la herramienta correcta importa más que el propio análisis. Las páginas estáticas envían sus datos en la primera respuesta HTML, mientras que las páginas dinámicas los ensamblan en el navegador después de la carga, y ambos casos requieren conjuntos de herramientas diferentes.

¿Cómo configuras un proyecto de scraping en Node.js?

Crea una carpeta de proyecto, inicialízala con npm e instala las librerías que necesitas antes de escribir cualquier código de scraping.

Node.js 18 y versiones posteriores incluyen un fetch global incorporado, por lo que no necesitas una librería HTTP para solicitudes GET simples, aunque muchos equipos siguen prefiriendo axios por sus interceptores y su manejo de proxies. Los siguientes comandos crean la estructura de un proyecto e instalan cheerio para el análisis, axios como cliente alternativo, p-limit para el control de concurrencia, y Puppeteer y Playwright para páginas dinámicas:

mkdir js-scraper && cd js-scraper
npm init -y
npm install cheerio axios p-limit
npm install puppeteer playwright

# enable modern import syntax
npm pkg set type=module

Configurar type=module te permite usar sentencias import. Si prefieres CommonJS, deja el valor por defecto y usa require en su lugar. Los ejemplos de esta guía usan sintaxis import, pero cada uno se corresponde directamente con una llamada require. Una vez termine la instalación, tendrás todo lo necesario para scraping estático y dinámico en un único proyecto.

¿Cómo haces scraping de una página estática con fetch y cheerio?

Para páginas estáticas, descarga el HTML con la función fetch incorporada y analízalo con cheerio, que te da una API estilo jQuery en el servidor.

Las páginas estáticas devuelven su contenido directamente en la respuesta HTML inicial, por lo que no se necesita renderizado en navegador. Este enfoque es rápido y económico porque solo descargas markup, no imágenes, fuentes ni scripts. El siguiente ejemplo obtiene una página, la carga en cheerio y lee cada tarjeta de producto:

import * as cheerio from 'cheerio';

async function scrape(url) {
  const res = await fetch(url, {
    headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' }
  });
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  const html = await res.text();
  const $ = cheerio.load(html);

  const items = [];
  $('.product').each((i, el) => {
    items.push({
      title: $(el).find('h2').text().trim(),
      price: $(el).find('.price').text().trim(),
      link: $(el).find('a').attr('href')
    });
  });
  return items;
}

console.log(await scrape('https://example.com/catalog'));

Si prefieres axios, la parte de análisis es idéntica y solo cambia la solicitud. Axios lanza un error automáticamente en códigos de estado que no sean 2xx y simplifica la configuración de proxy, por lo que muchos scrapers en producción lo usan:

import axios from 'axios';
import * as cheerio from 'cheerio';

const { data: html } = await axios.get('https://example.com/catalog', {
  headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' },
  timeout: 15000
});
const $ = cheerio.load(html);
const titles = $('.product h2').map((i, el) => $(el).text().trim()).get();
console.log(titles);

Antes de apuntar un scraper a cualquier sitio, vale la pena confirmar que el objetivo lo permite. Nuestra guía sobre cómo verificar si un sitio web permite el scraping explica el robots.txt y los términos de servicio.

¿Cómo extraes datos estructurados a JSON y CSV?

Recopila cada registro en un array de objetos planos, y luego escribe ese array en un archivo JSON, o aplánalo en filas separadas por comas para un archivo CSV.

El texto extraído en bruto solo es útil una vez que se almacena en una forma predecible. JSON es el objetivo más simple porque un array de JavaScript se serializa directamente. El siguiente fragmento escribe los elementos extraídos en disco con el módulo de sistema de archivos incorporado:

import { writeFile } from 'node:fs/promises';

const items = await scrape('https://example.com/catalog');
await writeFile('products.json', JSON.stringify(items, null, 2), 'utf-8');
console.log(`Saved ${items.length} records to products.json`);

CSV es mejor cuando los datos alimentan una hoja de cálculo o un almacén de datos. Puedes construirlo sin ninguna librería escapando comillas y uniendo campos, aunque el escape importa porque los precios y títulos pueden contener comas:

import { writeFile } from 'node:fs/promises';

function toCsv(rows) {
  const headers = Object.keys(rows[0]);
  const escape = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`;
  const lines = [headers.join(',')];
  for (const row of rows) {
    lines.push(headers.map((h) => escape(row[h])).join(','));
  }
  return lines.join('\n');
}

const items = await scrape('https://example.com/catalog');
await writeFile('products.csv', toCsv(items), 'utf-8');

Mantén tus nombres de campo estables entre ejecuciones para que las herramientas posteriores no se rompan cuando el diseño de una página cambie ligeramente. Si también necesitas extraer archivos binarios, consulta nuestra guía sobre cómo extraer imágenes de un sitio web.

¿Cómo haces scraping de varias páginas con paginación?

Recorre las URLs de las páginas del sitio o sigue el enlace de página siguiente en cada página, extrayendo cada una y deteniéndote cuando ya no haya más resultados.

La mayoría de los catálogos dividen los resultados en páginas numeradas usando un parámetro de consulta como ?page=2, o exponen un enlace siguiente. El patrón más seguro lee cada página, recopila sus registros, luego busca la página siguiente y se detiene cuando falta. El siguiente ejemplo recorre páginas numeradas hasta que una página devuelve cero elementos:

import * as cheerio from 'cheerio';

async function scrapeAll(base) {
  const all = [];
  for (let page = 1; page <= 50; page++) {
    const res = await fetch(`${base}?page=${page}`, {
      headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' }
    });
    if (!res.ok) break;
    const $ = cheerio.load(await res.text());
    const items = $('.product').map((i, el) => ({
      title: $(el).find('h2').text().trim(),
      price: $(el).find('.price').text().trim()
    })).get();

    if (items.length === 0) break;
    all.push(...items);
    await new Promise((r) => setTimeout(r, 1000));
  }
  return all;
}

console.log((await scrapeAll('https://example.com/catalog')).length);

Observa la pausa de un segundo entre páginas y el límite estricto de 50 iteraciones, que evita un bucle descontrolado si el sitio nunca devuelve una página vacía. Cuando el contenido se carga mediante scroll en lugar de páginas numeradas, necesitas un navegador headless, lo que cubre la sección de Puppeteer más abajo.

¿Cómo manejas reintentos, backoff y limitación de velocidad?

Envuelve cada solicitud en un ayudante de reintento que espera más tiempo después de cada fallo, y limita cuántas solicitudes se ejecutan a la vez para mantenerte dentro de la tolerancia de un sitio.

Las redes no son fiables y los servidores devuelven errores transitorios, por lo que un scraper que se rinde en el primer fallo pierde datos innecesariamente. Un ayudante de reintento con backoff exponencial espera progresivamente más entre intentos, lo que además le da tiempo a un servidor limitado en velocidad para recuperarse:

async function fetchWithRetry(fn, retries = 4, delay = 1000) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === retries) throw err;
      const wait = delay * 2 ** (attempt - 1);
      console.warn(`Attempt ${attempt} failed, retrying in ${wait}ms`);
      await new Promise((r) => setTimeout(r, wait));
    }
  }
}

const html = await fetchWithRetry(async () => {
  const res = await fetch('https://example.com/catalog');
  if (res.status === 429) throw new Error('Rate limited');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.text();
});

El backoff maneja solicitudes individuales, pero también necesitas controlar cuántas se ejecutan en paralelo. Lanzar cientos de solicitudes concurrentes degrada el servidor objetivo y hace que tu tráfico sea marcado. La librería p-limit limita la concurrencia para que un número fijo de workers procese una cola de URLs:

import pLimit from 'p-limit';

const limit = pLimit(5); // at most 5 requests in flight
const urls = Array.from({ length: 100 }, (_, i) =>
  `https://example.com/item/${i + 1}`
);

const results = await Promise.all(
  urls.map((url) =>
    limit(() => fetchWithRetry(async () => {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.text();
    }))
  )
);

console.log(`Fetched ${results.length} pages with a cap of 5 in parallel`);

Juntos, el backoff y un límite de concurrencia mantienen tu huella razonable y tu tasa de éxito alta. Para una lista de verificación más amplia, consulta nuestra guía de buenas prácticas de web scraping.

¿Cómo haces scraping de páginas dinámicas con Puppeteer?

Para páginas dinámicas que construyen contenido con JavaScript después de la carga, controla Chrome headless con Puppeteer, espera al selector objetivo, y lee el DOM completamente renderizado.

Muchos sitios modernos devuelven un shell HTML casi vacío y luego obtienen datos mediante solicitudes en segundo plano. Un cliente HTTP no ve nada útil, por lo que necesitas un motor que ejecute el propio JavaScript de la página. Puppeteer controla Chrome headless y puede esperar elementos, hacer clic y desplazarse. El siguiente ejemplo espera un selector, luego se desplaza repetidamente para activar la carga de scroll infinito antes de extraer los datos dentro del contexto del navegador:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (compatible; MyScraper/1.0)');
await page.goto('https://example.com/feed', { waitUntil: 'networkidle2' });

// wait until the first batch of cards is present
await page.waitForSelector('.product');

// infinite scroll: scroll to the bottom until height stops growing
let previousHeight = 0;
for (let i = 0; i < 20; i++) {
  const height = await page.evaluate('document.body.scrollHeight');
  if (height === previousHeight) break;
  previousHeight = height;
  await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
  await new Promise((r) => setTimeout(r, 1500));
}

const items = await page.evaluate(() =>
  Array.from(document.querySelectorAll('.product')).map((el) => ({
    title: el.querySelector('h2')?.innerText.trim(),
    price: el.querySelector('.price')?.innerText.trim()
  }))
);

console.log(`Extracted ${items.length} items`);
await browser.close();

El bucle se detiene tan pronto como la altura de la página deja de aumentar, lo que indica que no se está cargando contenido nuevo, y el límite estricto de 20 desplazamientos evita un bucle infinito. Para más sobre esta clase de objetivo, consulta nuestra guía sobre cómo extraer páginas web dinámicas.

¿Cómo usas Playwright como alternativa a Puppeteer?

Playwright funciona casi de forma idéntica a Puppeteer, pero añade soporte de primera clase para Chromium, Firefox y WebKit, además de localizadores con espera automática que reducen los selectores inestables.

Playwright fue creado por algunos de los ingenieros originales de Puppeteer y comparte la mayor parte de su API, por lo que migrar suele ser cuestión de renombrar unas pocas llamadas. Sus localizadores esperan automáticamente a que los elementos sean accionables, lo que elimina muchas llamadas manuales a waitForSelector. El scraper equivalente se ve así:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  userAgent: 'Mozilla/5.0 (compatible; MyScraper/1.0)'
});
const page = await context.newPage();
await page.goto('https://example.com/feed', { waitUntil: 'networkidle' });

await page.locator('.product').first().waitFor();

const items = await page.locator('.product').evaluateAll((els) =>
  els.map((el) => ({
    title: el.querySelector('h2')?.innerText.trim(),
    price: el.querySelector('.price')?.innerText.trim()
  }))
);

console.log(`Extracted ${items.length} items`);
await browser.close();

Elige Playwright cuando necesites probar el mismo scraper en varios motores de navegador o quieras su simulación de red incorporada. Elige Puppeteer cuando solo apuntes a Chrome y quieras la dependencia más pequeña. Ambos consumen mucha más memoria que fetch y cheerio, así que resérvalos para páginas que realmente requieran renderizado.

¿Qué herramienta de scraping en JavaScript deberías elegir?

Usa fetch o axios con cheerio para HTML estático, y recurre a Puppeteer o Playwright solo cuando una página renderiza sus datos en el navegador.

La herramienta correcta depende de cómo la página entrega los datos y de cuánto valores la velocidad frente a la fidelidad del renderizado. Los clientes HTTP son rápidos y ligeros pero no pueden ejecutar JavaScript; los navegadores headless ejecutan todo lo que ve un usuario real, pero cuestan más memoria y tiempo. La siguiente tabla resume las ventajas y desventajas:

Herramienta Ejecuta JS Velocidad Mejor para
fetch + cheerio No La más rápida HTML estático, alto volumen
axios + cheerio No Rápida HTML estático, configuración de proxy sencilla
Puppeteer Lenta Páginas dinámicas solo de Chrome
Playwright Lenta Dinámico, pruebas multi-navegador

Un patrón de producción común combina ambos: usa un navegador headless solo para llegar a las páginas o endpoints que necesitan renderizado, y luego recurre a un cliente HTTP simple para el grueso de las solicitudes. JavaScript es una de varias pilas viables para este trabajo; si estás comparando ecosistemas, nuestra guía de web scraping con Rust cubre una alternativa compilada.

¿Cómo enrutas scrapers de JavaScript a través de un proxy?

Pasa la configuración de proxy a tu cliente HTTP o a las opciones de lanzamiento del navegador para que las solicitudes salgan desde una dirección IP diferente, lo que distribuye la carga y reduce la probabilidad de que una sola dirección quede limitada por velocidad.

Hacer scraping de muchas páginas desde una sola IP es la forma más rápida de que te bloqueen. Rotar las solicitudes entre un conjunto de direcciones hace que el tráfico parezca provenir de muchos visitantes separados. Con axios, proporcionas el host, puerto y credenciales del proxy en la configuración de la solicitud. El siguiente ejemplo enruta a través de un gateway de DataImpulse:

import axios from 'axios';

const { data } = await axios.get('https://example.com', {
  proxy: {
    protocol: 'http',
    host: 'gw.dataimpulse.com',
    port: 823,
    auth: { username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD' }
  },
  headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' }
});
console.log(data.length, 'bytes received through the proxy');

Puppeteer toma el proxy como un argumento de lanzamiento y luego se autentica en la página, porque Chrome no acepta credenciales dentro de la URL del proxy. El patrón es el mismo en Playwright, que acepta un objeto de proxy con un nombre de usuario y contraseña directamente:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--proxy-server=gw.dataimpulse.com:823']
});
const page = await browser.newPage();
await page.authenticate({
  username: 'YOUR_USERNAME',
  password: 'YOUR_PASSWORD'
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
console.log(await page.title());
await browser.close();

DataImpulse ofrece proxies residenciales, proxies de datacenter y proxies móviles sobre HTTP, HTTPS y SOCKS5, con sesiones rotativas y fijas. La segmentación por país está incluida, mientras que la segmentación por estado, ciudad, código postal y ASN son complementos de pago. Si una solicitud de proxy devuelve un error de autenticación, nuestra nota sobre el error HTTP 407 explica la causa más común, y nuestra guía de autenticación de proxy cubre los formatos de credenciales con más detalle.

¿Cómo interceptas XHR y endpoints JSON?

En lugar de extraer el HTML renderizado, observa el tráfico de red en un navegador headless y lee las respuestas JSON que la página obtiene en segundo plano, lo que suele ser más limpio y rápido.

Las páginas dinámicas normalmente obtienen sus datos de una API oculta que devuelve JSON. Leer esa respuesta directamente evita por completo el análisis del DOM y te da registros bien estructurados. Tanto Puppeteer como Playwright te permiten escuchar respuestas y capturar las que quieras. El siguiente ejemplo de Puppeteer captura cualquier respuesta JSON de una ruta de API:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();

const captured = [];
page.on('response', async (response) => {
  const url = response.url();
  if (url.includes('/api/') && response.headers()['content-type']?.includes('application/json')) {
    try {
      captured.push(await response.json());
    } catch (err) {
      // non-JSON body, ignore
    }
  }
});

await page.goto('https://example.com/feed', { waitUntil: 'networkidle2' });
console.log(`Captured ${captured.length} JSON payloads`);
await browser.close();

Una vez que conoces la URL del endpoint y sus parámetros, a menudo puedes prescindir por completo del navegador y llamar a la API JSON con fetch simple, pasando las mismas cabeceras que envió la página. Eso te da la velocidad de un scraper estático con la completitud de uno renderizado, y es la mayor ganancia de rendimiento disponible para sitios dinámicos.

¿Cuáles son los límites éticos y legales del scraping?

Extrae solo datos públicos, respeta el robots.txt y los términos de servicio del sitio, evita datos personales, y nunca sobrecargues un servidor. Estas prácticas mantienen tu proyecto defendible y sostenible.

Revisa el archivo robots.txt en la raíz del dominio para ver qué rutas un sitio pide a los rastreadores que eviten, y toma esas solicitudes en serio. Identifica tu scraper con un User-Agent honesto, limita tus solicitudes, y almacena en caché las respuestas para no volver a obtener la misma página innecesariamente. En el lado de la infraestructura, el origen de tus direcciones IP importa. DataImpulse opera proxies éticos obtenidos de usuarios que se inscriben voluntariamente y son compensados, alineados con el RGPD, con un acuerdo de procesamiento de datos disponible. La ética no es solo una cuestión de cumplimiento; los scrapers educados y transparentes también son los que siguen funcionando. Para conocer los principios detrás de una recopilación sostenible, consulta nuestra guía de web scraping ético.

Herramientas de scraping en JavaScript y cuándo usar cada una

Preguntas frecuentes

¿Necesito Node.js para hacer scraping con JavaScript?

Para cualquier cosa más allá de una prueba rápida de una sola página, sí. Node.js te permite ejecutar scrapers en un servidor, recorrer muchas URLs, manejar reintentos, exportar datos y usar proxies, nada de lo cual la consola del navegador hace bien.

¿Cuándo debería usar Puppeteer en lugar de fetch y cheerio?

Usa fetch o axios con cheerio cuando los datos están presentes en la respuesta HTML inicial. Cambia a Puppeteer o Playwright solo cuando el contenido se renderiza mediante JavaScript después de que la página carga, porque un navegador headless es mucho más pesado.

¿Cómo exporto datos extraídos a CSV en Node.js?

Recopila los registros como un array de objetos, luego construye filas separadas por comas uniendo los valores de cada objeto y escapando comillas y comas. Escribe el resultado en un archivo con el módulo fs incorporado, o usa una librería como csv-stringify.

¿Por qué me bloquean al hacer scraping con JavaScript?

Las causas comunes son demasiadas solicitudes desde una sola IP, un User-Agent ausente o sospechoso, y ninguna pausa entre solicitudes. Los proxies rotativos, las cabeceras realistas, los reintentos con backoff y la limitación de velocidad reducen los bloqueos.

¿Qué tipo de proxy es mejor para el scraping con JavaScript?

Los proxies de datacenter son rápidos y económicos para objetivos tolerantes, mientras que los proxies residenciales y móviles se parecen más a usuarios reales en sitios estrictos. Las sesiones rotativas ayudan a distribuir las solicitudes entre muchas direcciones.

¿Cuándo no es DataImpulse la opción adecuada?

Si necesitas proxies ISP estáticos, una API de scraping totalmente gestionada, o acceso a sitios bancarios y gubernamentales, DataImpulse no es la herramienta adecuada. Se centra en proxies residenciales, móviles y de datacenter rotativos para recopilar datos públicos y acceder a contenido.

Empieza a hacer scraping con proxies fiables

¿Listo para mantener tus scrapers de JavaScript funcionando sin bloqueos constantes? DataImpulse te da IPs residenciales, móviles y de datacenter obtenidas éticamente desde 1 dólar por GB con tráfico que no caduca y sin suscripción. Crea una cuenta y enruta tu primer scraper a través de un proxy hoy mismo.


Share article: