Volver al blog

Raspado web con TypeScript: extracción tipada con Cheerio y Node

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

21-Jul-2026

TL;DR:

  • Node 22 ejecuta TypeScript directamente con node --experimental-strip-types, por lo que un scraper no necesita un paso de compilación ni un empaquetador.
  • fetch está integrado en el entorno de ejecución, lo que deja a cheerio como la única dependencia para el análisis de HTML y selección de CSS.
  • Tipar el registro que extraes es lo que hace que un scraper sea mantenible: el compilador señala un campo renombrado en el momento en que se consume, en lugar de después de que los datos ya se hayan movido río abajo.
  • Los tipos describen la forma que esperas, no la página que recibiste: una página renderizada en el cliente aún devuelve una respuesta válida que se analiza en cero registros.
  • La API de Scraping Universal de Scrapeless renderiza la página primero, y los mismos selectores cheerio sin cambios luego devuelven todos los 10 registros.
  • Comienza con el plan gratuito de Scrapeless y apunta el ejemplo de pivote a tu propio objetivo.

TypeScript se gana su lugar en un scraper por una razón: los datos que extraes tienen una forma, y esa forma se desvía. Un sitio renombra un campo, un selector comienza a devolver una cadena vacía, y un scraper de JavaScript puro lleva el daño en silencio a lo que lo consume. Un registro tipado convierte eso en un error de compilación.

Lo que ha cambiado recientemente es el costo de configuración. Node 22 elimina los tipos de manera nativa, por lo que no hay paso de tsc, no hay empaquetador, y no hay ts-node en el árbol de dependencias.

Lo Que Necesitas

El scraping web con TypeScript en 2026 necesita Node 22 o posterior y una única dependencia. Las versiones a continuación son las que se usaron en estos ejemplos:

Componente Versión Trabajo
Node.js 22.22.3 Tiempo de ejecución, fetch nativo, eliminación de tipos nativa
cheerio 1.2.0 Análisis de HTML y selectores CSS

fetch está integrado en el entorno de ejecución, por lo que no necesita importación ni biblioteca HTTP. cheerio ofrece una API con la forma de jQuery sobre un documento analizado, que es lo más cercano a un estándar para consultas HTML del lado del servidor en el ecosistema de Node. Analiza según la especificación de análisis de HTML en lugar de tratar el marcado como texto para hacer coincidir patrones.

Instalar

Crea el proyecto y agrega la única dependencia:

bash Copy
mkdir ts-scraper && cd ts-scraper
npm init -y
npm pkg set type=module
npm install cheerio@1.2.0

La configuración type=module es importante: los ejemplos a continuación utilizan await a nivel superior, lo que requiere la sintaxis de módulos ES.

Extraer un Registro Tipado

Declara la forma primero, luego haz que la extracción la produzca. El compilador te mantendrá a ello:

typescript Copy
import * as cheerio from "cheerio";

interface Quote {
  text: string;
  author: string;
  tags: string[];
}

const res = await fetch("https://quotes.toscrape.com/");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const $ = cheerio.load(await res.text());

const quotes: Quote[] = $("div.quote")
  .map((_, el) => ({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
  }))
  .get();

console.log(`citas analizadas: ${quotes.length}`);
console.log(JSON.stringify(quotes[0], null, 2));

Ejecuta esto sin paso de compilación:

bash Copy
node --experimental-strip-types static.ts
text Copy
citas analizadas: 10
{
  "text": "“El mundo tal como lo hemos creado es un proceso de nuestro pensamiento. No se puede cambiar sin cambiar nuestro pensamiento.”",
  "author": "Albert Einstein",
  "tags": [
    "cambio",
    "profundas-reflexiones",
    "pensamiento",
    "mundo"
  ]
}

Tres cosas están haciendo el trabajo real allí.

La anotación Quote sobre quotes es lo que hace que el callback de .map() tenga un tipo verificado. Devolver un objeto que falta tags, o escribirlo como tag, hace que el error aparezca en esa línea en lugar de surfacing más tarde como undefined en lo que consuma el array.

res.ok es la verificación que la gente omite. fetch no lanza una excepción en un 404 o un 403: resuelve normalmente con ok establecido en false, y la página de error se analiza en cero coincidencias exactamente como un resultado vacío. Las clases de estado que se comportan de esta manera están definidas en la especificación de semántica HTTP.

El anidado .map(...).get() es el modismo de cheerio para convertir una selección en un array real. La llamada interna recoge las cadenas de etiquetas, por lo que tags llega como string[] en lugar de un objeto cheerio.

Donde los Tipos Dejan de Ayudar

Un tipo describe el registro que esperas, no la página que recibiste. Ni fetch ni cheerio ejecutan JavaScript, por lo que en una página que construye su contenido en el navegador, los selectores no coinciden con nada y los tipos se satisfacen con un array vacío.

El sitio anterior publica un gemelo renderizado por el cliente de los mismos datos en /js/. El mismo código de análisis, apuntado a él:

typescript Copy
import * as cheerio from "cheerio";

const res = await fetch("https://quotes.toscrape.com/js/");

si (!res.ok) throw new Error(HTTP ${res.status});
const html = await res.text();
const $ = cheerio.load(html);

console.log(bytes de html: ${html.length});
console.log(citas analizadas: ${$("div.quote").length});

Copy
```text
bytes de html: 5806
citas analizadas: 0

La solicitud fue exitosa, res.ok fue verdadero y se analizaron 5,806 caracteres de HTML válido sin quejas. Quote[] es un array vacío perfectamente tipado. Este es el modo de fallo que vale la pena diseñar, porque nada en el sistema de tipos o en la capa HTTP lo reporta: la marca de cita se escribe en el DOM después de que se ejecuta un script.

Renderizar Primero, Luego Analizar

La API Universal de Scraping Sin Scrapeless cierra esa brecha al renderizar la página en un navegador en la nube y devolver el HTML resultante, por lo que el lado de TypeScript se mantiene como una llamada fetch tipada.

Configura tu clave:

bash Copy
export SCRAPELESS_API_KEY="tu_clave_api_aqui"

Solo cambia la capa de fetch: la interfaz Quote y el código del selector son idénticos al primer ejemplo:

typescript Copy
import * as cheerio from "cheerio";

interface Quote {
  text: string;
  author: string;
  tags: string[];
}

const res = await fetch("https://api.scrapeless.com/api/v2/unlocker/request", {
  method: "POST",
  headers: {
    "x-api-token": process.env.SCRAPELESS_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    actor: "unlocker.webunlocker",
    input: {
      url: "https://quotes.toscrape.com/js/",
      js_render: true,
      headless: true,
    },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);

const envelope: { data: string } = await res.json();
const $ = cheerio.load(envelope.data);

const quotes: Quote[] = $("div.quote")
  .map((_, el) => ({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
  }))
  .get();

console.log(`bytes de html: ${envelope.data.length}`);
console.log(`citas analizadas: ${quotes.length}`);
console.log(`primer autor: ${quotes[0]?.author}`);
text Copy
bytes de html: 8940
citas analizadas: 10
primer autor: Albert Einstein

Misma página, mismos selectores, misma interfaz. El número de registros pasa de 0 a 10 y el payload crece de 5,806 a 8,940 caracteres, y la única diferencia es qué capa obtuvo el HTML.

Dos detalles de TypeScript en esa llamada valen la pena replicar. Anotar el sobre como { data: string } detiene await res.json() de esparcir any a través del resto del archivo, que es donde la seguridad de tipos suele filtrarse fuera de un scraper. Y quotes[0]?.author respeta el hecho de que un índice de array puede ser undefined: con noUncheckedIndexedAccess habilitado, el compilador lo requiere.

El campo data contiene el documento renderizado como una cadena, por eso va directamente a cheerio.load. Las opciones de renderizado se cubren en la documentación de Scrapeless, y el mismo comportamiento de js_render se explora más a fondo en la guía de renderizado de JS.

Solución de Problemas

ERR_UNKNOWN_FILE_EXTENSION en un archivo .ts. Falta el flag --experimental-strip-types, o Node es anterior a la versión 22. La eliminación de tipos quita anotaciones en el momento de carga; no hace verificación de tipos, así que ejecuta tsc --noEmit por separado cuando quieras la opinión del compilador.

No se puede usar la declaración de importación fuera de un módulo. Falta "type": "module" en el paquete. El await a nivel superior necesita módulos ES.

La eliminación de tipos rechaza un enum o una propiedad de parámetro. Esos constructos emiten código real en tiempo de ejecución en lugar de ser borrables, por lo que la eliminación no puede manejarlos. Usa una unión de literales de cadena en lugar de un enum, y asigna los campos del constructor explícitamente.

Los selectores coinciden en el navegador pero no en el script. Compara contra el ver-fuente en lugar del inspector. El inspector muestra el DOM después de que se han ejecutado los scripts, que no es lo que recibió fetch: imprima primero la longitud de la respuesta, como lo hace el ejemplo anterior.

Antes de apuntar esto a un objetivo en vivo, verifica los términos del sitio y sus directivas de /robots.txt, que siguen el estándar del Protocolo de Exclusión de Robots, y mantén la recolección a datos públicos a un volumen que el sitio pueda servir cómodamente.

Conclusión

TypeScript brinda a un scraper un contrato: declara el registro, y el compilador te dice cuando la extracción deja de satisfacerlo. Con Node 22 eliminando tipos de manera nativa y fetch integrado, ese contrato tiene un costo de una dependencia y ningún paso de compilación.
Lo que los tipos no pueden decirte es si la página que recuperaste contenía los datos en absoluto. Esa verificación tiene que ser explícita: un array vacío bien tipado es lo que devuelve una página renderizada por el cliente, y se ve exactamente como una página sin resultados. Medir la diferencia es un hábito que vale la pena mantener: 10 registros renderizados por el servidor, 0 en el gemelo de JavaScript, 10 nuevamente una vez que algo renderiza la página antes de que cheerio la vea.

Comienza con el plan gratuito de Scrapeless para ejecutar el paso de renderizado contra tus propios objetivos, y revisa el precio actual de Scrapeless cuando dimensionas un trabajo.

Preguntas frecuentes

P: ¿Necesito compilar TypeScript para raspar con él?

No. Node 22 y versiones posteriores ejecutan archivos .ts directamente con node --experimental-strip-types, que elimina las anotaciones de tipo en el momento de la carga. Eso significa que no hay paso de construcción ni empaquetador para un raspador. La eliminación de tipos no comprueba tipos, así que ejecuta tsc --noEmit en CI cuando quieras que el compilador los verifique realmente.

P: ¿Qué biblioteca de análisis HTML debería usar con TypeScript?

cheerio cubre la mayor parte del trabajo: analiza con un analizador compatible con las especificaciones HTML y expone una API de selector estilo jQuery con definiciones de TypeScript incluidas. Solo recurre a un navegador sin cabeza o una API de renderizado cuando el contenido se escribe en el DOM por scripts, que ningún analizador puede recuperar por sí solo.

P: ¿fetch lanza un error en un 404 en Node?

No, y esto sorprende a la gente. fetch se resuelve normalmente para cualquier respuesta HTTP y solo se rechaza en caso de un fallo a nivel de red, así que tienes que comprobar res.ok tú mismo. Sin esa verificación, una página de error se analiza en cero coincidencias y es indistinguible de una página que legítimamente no tenía resultados.

P: ¿Cómo mantengo los tipos honestos cuando la respuesta es JSON?

Anota en el límite. await res.json() devuelve any, así que asignarlo a una variable tipada como const envelope: { data: string } es lo que impide que any se propague por el resto del archivo. Para datos no confiables provenientes de upstream, valida en tiempo de ejecución con una biblioteca de esquemas en lugar de depender solo de la anotación.

P: ¿Puede TypeScript raspar una página que se renderiza en el navegador?

No por sí solo. El lenguaje no tiene influencia sobre si JavaScript se ejecuta: fetch devuelve los bytes que el servidor envió, y el ejemplo anterior muestra que esos bytes se analizan en cero registros en una página renderizada por el cliente. El renderizado tiene que ocurrir en otro lugar, ya sea en un navegador sin cabeza que operas o a través de una API que devuelve el DOM renderizado.

En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.

Artículos más populares

Catalogar