Saltar al contenido
Volver al blog

Cómo extraer datos de una tabla HTML con JavaScript y exportarlos a un archivo CSV limpio

Mihai MaximÚltima actualización el 15 min read
Cómo extraer datos de una tabla HTML con JavaScript y exportarlos a un archivo CSV limpio
En resumen: Primero, determina si las filas de la tabla están presentes en la respuesta HTTP, proceden de una solicitud de datos o requieren que un navegador las genere. A continuación, utiliza el script estático de Axios y Cheerio o la ruta específica de Puppeteer que se indica a continuación para extraer datos de una tabla HTML en JavaScript, validar los registros basados en encabezados, eliminar duplicados de las páginas y generar un archivo CSV de forma segura. Una tabla HTML organiza la información en filas y columnas, normalmente con <table>, <tr>, <th>, y <td> . Extraer una tabla HTML en JavaScript significa recuperar o renderizar ese marcado, convertir cada fila de datos en un objeto de JavaScript, validar el resultado y serializarlo a un formato como CSV.

El bucle sobre las filas rara vez es la parte difícil. La mayoría de los fallos se producen porque el rastreador lee la respuesta del servidor mientras el desarrollador inspecciona un DOM renderizado por el navegador, se centra en la tabla equivocada, asume posiciones fijas de las celdas o exporta valores sin escapar. Un flujo de trabajo fiable elige el método de extracción más ligero que permita ver realmente los datos.

Este tutorial de extracción de datos web con Node.js comienza con Axios y Cheerio para el marcado estático, y luego añade una ruta específica de extracción de tablas con Puppeteer para las filas creadas tras la carga de la página. El analizador deriva las claves a partir de los encabezados de las tablas, en lugar de dar por sentado un único conjunto de datos. Además, señala las filas con formato incorrecto, conserva los valores como cadenas hasta que se realice una conversión intencionada y comprueba que el resultado sea utilizable. El resultado es una forma práctica de extraer datos de tablas HTML en JavaScript sin tener que pretender que rowspan, colspan, las tablas anidadas o todas las cuadrículas personalizadas puedan aplanarse automáticamente.

Elige la ruta de extracción adecuada antes de escribir el código

Antes de extraer datos de una tabla HTML en JavaScript, identifica dónde se encuentran los datos de las filas. En el marcado estándar, <tr> representa una fila, <th> suele etiquetar una columna, y <td> contiene un valor normal. La referencia del elemento de tabla HTML de MDN es una herramienta semántica útil cuando no se conoce bien la estructura.

Sigue este orden de decisión:

  1. Filas en el HTML de la respuesta: solicita la página con Axios y analízala con Cheerio.
  2. Filas devueltas por Fetch o XHR: utiliza el punto final de datos permitido cuando sea estable y adecuado.
  3. Filas creadas únicamente tras la ejecución de scripts o la interacción del usuario: utiliza la automatización del navegador.

Este orden hace que un extractor de tablas en JavaScript resulte más sencillo y económico de manejar que tener que iniciar un navegador para cada página.

Comprueba si las filas están presentes en el código HTML de la respuesta

Abre «Ver código fuente», no solo el panel «Elementos», y busca un valor de celda característico. También puedes guardar response.data y buscarlo directamente. Si el valor y un selector de fila específico están presentes, la automatización del navegador es innecesaria, incluso cuando la página muestra controles de paginación.

Prueba los selectores en DevTools antes de enviar solicitudes repetidas y, a continuación, vuelve a probarlos con el código HTML obtenido. Una hoja de referencia de selectores CSS resulta útil cuando table tr también se capturan elementos de navegación, tablas anidadas o filas de pie de página.

Comprueba si hay filas obtenidas de la red o renderizadas por el navegador

Si el valor no aparece en el código fuente, filtra el panel «Red» por «Fetch/XHR», recarga la página, cambia una página o un filtro e inspecciona las respuestas. Un punto final de datos autorizado y estable suele ser más fácil de validar que las celdas renderizadas, pero confirma que su uso esté permitido.

Elige un navegador sin interfaz gráfica cuando sean esenciales el JavaScript, el estado de inicio de sesión, los clics o el desplazamiento. Una comparación entre Cheerio y Puppeteer puede ayudar a documentar esa disyuntiva. Evita las expresiones regulares como analizador HTML general; utilízalas solo para patrones específicos dentro de una celda ya seleccionada.

Crea un scraper reutilizable con Axios y Cheerio para extraer datos de tablas HTML en JavaScript

En el caso de una página estática, el proceso es sencillo: Axios recupera la respuesta, Cheerio carga el marcado y un analizador de ámbito limitado convierte las filas en objetos. La decisión de diseño más importante es la configuración. Acepta la URL, el selector de tabla y la ruta de salida como entradas y, a continuación, deriva las claves de los registros a partir de los encabezados de la tabla seleccionada.

Este enfoque de scraping web con Axios y Cheerio evita una correspondencia frágil del tipo «la celda cero es el nombre, la celda uno es el precio». Funciona con tablas convencionales cuyos encabezados y anchos de fila son consistentes, al tiempo que sigue detectando excepciones estructurales.

Configura el proyecto de Node.js y solicita la página de forma segura

Crea un proyecto e instala los tres paquetes utilizados en el ejemplo estático:

mkdir table-scraper
cd table-scraper
npm init -y
npm install axios cheerio objects-to-csv

Crear scrape-table.js. El script completo utiliza CommonJS de forma coherente, aplica un tiempo de espera para las solicitudes, solo acepta estados HTTP exitosos y agrupa las tareas de solicitud, análisis, validación y exportación en un único bloque de errores. No reintenta ciegamente los errores del cliente, ya que las solicitudes repetidas no solucionarán la falta de un selector, un acceso denegado o una URL no válida.

Las API de los paquetes pueden cambiar. Fija las versiones tras probar el script en tu entorno y revisa la documentación oficial del paquete antes de utilizar el ejemplo como dependencia de producción.

Selecciona la tabla y deriva nombres de columnas estables

Limita el selector a una sola tabla siempre que sea posible, por ejemplo #results en lugar de table. El analizador toma deliberadamente la primera coincidencia y, a continuación, busca la última fila en <thead>. Si no hay ninguna <thead>, trata la primera fila que contenga <th> celdas como el encabezado. Si las etiquetas siguen sin estar disponibles, genera column_1, column_2, y así sucesivamente.

El texto de los encabezados se normaliza a minúsculas con el formato «snake case». A un encabezado en blanco se le asigna un valor de reserva posicional, mientras que los duplicados se convierten en claves como price y price_2. Esto evita que las celdas posteriores sobrescriban las propiedades anteriores. Para encabezados multilingües o un contrato de datos público, sustituye la normalización automática por un mapa de encabezados explícito.

Encabezados de varias filas, rowspany colspan requieren una lógica específica para la tabla, ya que es posible que la cuadrícula visual no coincida con el recuento de celdas del DOM. No hagas suposiciones sin más. Examina el marcado, define las columnas previstas y añade un paso de expansión de la cuadrícula si esa semántica es relevante.

Asigna cada fila a un objeto sin nombres de campos codificados de forma fija

Lee las celdas hijas directas de cada fila en lugar de todas las celdas descendientes. Esa distinción evita que una tabla anidada añada valores inesperados a su fila padre. El script construye primero una matriz de cadenas de celda depuradas, omite las filas que no contienen texto significativo y calcula la fila más ancha observada.

A continuación, crea una clave para cada columna y empareja las claves con los valores por índice. Los valores que faltan se convierten en cadenas vacías. Las celdas adicionales reciben encabezados generados cuando el escaneo inicial las detecta. El analizador también registra una advertencia cada vez que el recuento de celdas de una fila difiere del ancho calculado, de modo que las columnas desplazadas no pasan desapercibidas.

Este es el núcleo reutilizable a la hora de extraer datos de una tabla HTML en JavaScript: la matriz de destino se encuentra fuera de la iteración de filas, y cada fila aceptada aporta un objeto completo. Sigue siendo importante revisar las advertencias. Una discrepancia en el ancho puede indicar una celda que abarca varias columnas, una columna de control oculta, un marcado mal formado o un selector que se ha cruzado a otra sección de la tabla.

Normaliza y valida los valores extraídos

Normaliza el ruido de presentación, no el significado. El ejemplo convierte los espacios no separables en espacios normales, recorta los extremos y elimina los espacios en blanco repetidos. Deja las fechas, las divisas, los porcentajes, los identificadores y los ceros iniciales como cadenas, ya que valores como 01/02/03 o 1,234 son ambiguos sin las reglas de configuración regional y de esquema.

La validación debería fallar de forma evidente cuando falte el selector de la tabla o no queden registros no vacíos. Comprueba también las columnas obligatorias, inspecciona un objeto de muestra, revisa las advertencias de discrepancia y confirma que el archivo final existe antes de que un trabajo posterior lo utilice. Si necesitas valores tipados, aplica convertidores explícitos por columna tras la extracción y conserva el valor sin procesar para la depuración. Una guía de análisis de datos resulta muy útil cuando esas conversiones se convierten en una etapa independiente del proceso.

Serialice los registros a CSV de forma segura

No generes archivos CSV con row.join(","). Las comas, las comillas dobles, los retornos de carro y los saltos de línea dentro de una celda deben escaparse y ponerse entre comillas. Utiliza un serializador que se mantenga activamente y cuyo comportamiento hayas probado. El ejemplo utiliza objects-to-csv, asigna a cada objeto sus propiedades en el mismo orden de encabezado y escribe la matriz de JavaScript resultante en un archivo CSV.

Prueba la versión elegida con datos de prueba que contengan comas, comillas, saltos de línea incrustados, valores en blanco y texto no ASCII. Confirma su comportamiento con respecto a los encabezados y al codificado UTF-8 en las aplicaciones que leerán el archivo. Una serialización CSV correcta no puede reparar una tabla de origen irregular; resuelve las celdas que abarcan varias columnas, las semánticas duplicadas y las filas que no coinciden antes de la exportación.

Ejecuta el scraper de tabla estática completo

Guarda lo siguiente como scrape-table.js. Abarca la solicitud, la selección, la normalización de encabezados, la asignación de filas, las comprobaciones y la exportación a CSV. Edita requiredColumns cuando los datos posteriores necesiten campos garantizados.

const axios = require("axios");
const cheerio = require("cheerio");
const ObjectsToCsv = require("objects-to-csv");

const [url, tableSelector = "table", outputPath = "table.csv"] =
  process.argv.slice(2);

const requiredColumns = [];

function clean(value) {
  return value
    .replace(/\u00a0/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function uniqueHeaders(labels, width) {
  const seen = new Map();

  return Array.from({ length: width }, (_, index) => {
    const normalized = clean(labels[index] || "")
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "_")
      .replace(/^_+|_+$/g, "");

    const base = normalized || `column_${index + 1}`;
    const count = (seen.get(base) || 0) + 1;
    seen.set(base, count);

    return count === 1 ? base : `${base}_${count}`;
  });
}

function parseTable(html) {
  const $ = cheerio.load(html);
  const table = $(tableSelector).first();

  if (!table.length) {
    throw new Error(`No table matched selector: ${tableSelector}`);
  }

  let headerRow = table.find("thead tr").last();

  if (!headerRow.length) {
    const firstRow = table.find("tr").first();
    if (firstRow.children("th").length) {
      headerRow = firstRow;
    }
  }

  const headerLabels = headerRow
    .children("th, td")
    .map((_, cell) => clean($(cell).text()))
    .get();

  let dataRows = table.children("tbody").children("tr");
  if (!dataRows.length) {
    dataRows = table.children("tr");
  }

  const headerNode = headerRow.get(0);
  const matrix = dataRows
    .toArray()
    .filter((row) => row !== headerNode)
    .map((row) =>
      $(row)
        .children("th, td")
        .map((_, cell) => clean($(cell).text()))
        .get()
    )
    .filter((cells) => cells.some(Boolean));

  const width = Math.max(
    0,
    headerLabels.length,
    ...matrix.map((cells) => cells.length)
  );

  if (!width) {
    throw new Error("The selected table has no readable cells.");
  }

  const headers = uniqueHeaders(headerLabels, width);
  const warnings = [];

  if (headerLabels.length && headerLabels.length !== width) {
    warnings.push(
      `Header has ${headerLabels.length} cells; widest row has ${width}.`
    );
  }

  const records = matrix.map((cells, rowIndex) => {
    if (cells.length !== width) {
      warnings.push(
        `Row ${rowIndex + 1} has ${cells.length} cells; expected ${width}.`
      );
    }

    return Object.fromEntries(
      headers.map((header, cellIndex) => [
        header,
        cells[cellIndex] ?? "",
      ])
    );
  });

  return { headers, records, warnings };
}

async function main() {
  if (!url) {
    throw new Error(
      'Usage: node scrape-table.js "<url>" "<table-selector>" "<output.csv>"'
    );
  }

  const response = await axios.get(url, {
    timeout: 15000,
    responseType: "text",
    maxRedirects: 5,
    validateStatus: (status) => status >= 200 && status < 300,
    headers: {
      "User-Agent": "TableScraper/1.0 (+contact@example.com)",
    },
  });

  const { headers, records, warnings } = parseTable(response.data);

  if (!records.length) {
    throw new Error("The table matched, but no nonempty rows were parsed.");
  }

  for (const column of requiredColumns) {
    if (!headers.includes(column)) {
      throw new Error(`Required column is missing: ${column}`);
    }
  }

  if (warnings.length) {
    console.warn(warnings.slice(0, 10).join("\n"));
  }

  await new ObjectsToCsv(records).toDisk(outputPath);

  console.log(`Wrote ${records.length} rows to ${outputPath}`);
  console.log(`Columns: ${headers.join(", ")}`);
  console.log("Sample record:", records[0]);
}

main().catch((error) => {
  console.error(`Scrape failed: ${error.message}`);
  process.exitCode = 1;
});

Ejecútalo con la URL de destino, un selector de tabla entre comillas y una ruta de salida:

node scrape-table.js "https://example.com/page" "#results" "results.csv"

Revisa las columnas mostradas y el registro de muestra antes de dar por válido el CSV. Considera cada advertencia de ancho como un problema de calidad de los datos que debes investigar.

Gestiona las tablas renderizadas con JavaScript y paginadas

Para extraer una tabla HTML en JavaScript cuando faltan filas en la respuesta, utiliza la automatización del navegador tras descartar un punto final de datos adecuado. Un navegador sin interfaz gráfica ejecuta los scripts de la página y puede hacer clic en los controles, pero consume más memoria y presenta más estados de fallo que una solicitud HTTP.

La ruta que se describe a continuación utiliza Puppeteer y un control Next configurable. Instálalo en el mismo proyecto:

npm install puppeteer

Esta ruta es adecuada para una tabla renderizada convencional. No intenta resolver cuadrículas virtualizadas que reciclan nodos DOM, retos de autenticación ni marcos de componentes arbitrarios sin cambios en los selectores.

Espera a que aparezcan las filas y recopila cada página o lote de desplazamiento

Espera a que aparezca un selector de fila en lugar de permanecer inactivo durante un tiempo arbitrario. En cada página, extrae una matriz de celdas, añade sus registros, haz clic en «Siguiente» y espera hasta que la matriz cambie. La documentación oficial de Puppeteer waitForSelector describe la espera del selector utilizada para el primer lote.

Guárdalo como scrape-rendered.js. El código reutiliza la estrategia de encabezado, falla ante columnas desplazadas y se niega a escribir un archivo cuando la paginación parece atascada.

const puppeteer = require("puppeteer");
const ObjectsToCsv = require("objects-to-csv");

const [
  url,
  tableSelector = "#results",
  nextSelector = 'button[aria-label="Next"]',
  outputPath = "rendered.csv",
] = process.argv.slice(2);

function clean(value) {
  return value.replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
}

function uniqueHeaders(labels, width) {
  const seen = new Map();

  return Array.from({ length: width }, (_, index) => {
    const normalized = clean(labels[index] || "")
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "_")
      .replace(/^_+|_+$/g, "");

    const base = normalized || `column_${index + 1}`;
    const count = (seen.get(base) || 0) + 1;
    seen.set(base, count);
    return count === 1 ? base : `${base}_${count}`;
  });
}

async function main() {
  if (!url) {
    throw new Error(
      'Usage: node scrape-rendered.js "<url>" "<table>" "<next>" "<output.csv>"'
    );
  }

  const rowSelector = `${tableSelector} tbody tr`;
  const headerSelector =
    `${tableSelector} thead tr:last-child th, ` +
    `${tableSelector} thead tr:last-child td`;

  const browser = await puppeteer.launch({ headless: true });

  try {
    const page = await browser.newPage();
    await page.goto(url, {
      waitUntil: "domcontentloaded",
      timeout: 30000,
    });
    await page.waitForSelector(rowSelector, { timeout: 15000 });

    const labels = await page.$$eval(headerSelector, (cells) =>
      cells.map((cell) => cell.textContent.replace(/\s+/g, " ").trim())
    );

    const recordsByValue = new Map();
    let headers;
    let pageNumber = 0;

    while (true) {
      pageNumber += 1;

      if (pageNumber > 1000) {
        throw new Error("Safety limit reached before pagination completed.");
      }

      const matrix = await page.$$eval(rowSelector, (rows) =>
        rows.map((row) =>
          Array.from(row.children)
            .filter((cell) => cell.matches("th, td"))
            .map((cell) => cell.textContent.replace(/\s+/g, " ").trim())
        )
      );

      if (!matrix.length) {
        throw new Error(`No rows found on rendered page ${pageNumber}.`);
      }

      if (!headers) {
        const width = Math.max(
          labels.length,
          ...matrix.map((cells) => cells.length)
        );
        headers = uniqueHeaders(labels, width);
      }

      for (const cells of matrix) {
        if (cells.length !== headers.length) {
          throw new Error(
            `Column mismatch on page ${pageNumber}: ` +
              `${cells.length} cells, expected ${headers.length}.`
          );
        }

        const record = Object.fromEntries(
          headers.map((header, index) => [header, cells[index]])
        );

        recordsByValue.set(JSON.stringify(record), record);
      }

      const next = await page.$(nextSelector);
      if (!next) break;

      const disabled = await next.evaluate(
        (element) =>
          element.disabled ||
          element.getAttribute("aria-disabled") === "true" ||
          element.classList.contains("disabled")
      );

      if (disabled) break;

      const previous = JSON.stringify(matrix);
      await next.click();

      try {
        await page.waitForFunction(
          ({ rowSelector, previous }) => {
            const current = Array.from(
              document.querySelectorAll(rowSelector)
            ).map((row) =>
              Array.from(row.children)
                .filter((cell) => cell.matches("th, td"))
                .map((cell) =>
                  cell.textContent.replace(/\s+/g, " ").trim()
                )
            );

            return current.length > 0 && JSON.stringify(current) !== previous;
          },
          { timeout: 10000 },
          { rowSelector, previous }
        );
      } catch {
        throw new Error(
          `Pagination did not advance after page ${pageNumber}; ` +
            "refusing to save a possibly partial file."
        );
      }
    }

    const records = [...recordsByValue.values()];

    if (!records.length) {
      throw new Error("No rendered records were collected.");
    }

    await new ObjectsToCsv(records).toDisk(outputPath);
    console.log(`Wrote ${records.length} unique rows to ${outputPath}`);
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(`Rendered scrape failed: ${error.message}`);
  process.exitCode = 1;
});

Para el desplazamiento infinito, sustituye el clic en «Siguiente» por una acción de desplazamiento y compara el recuento de filas o la firma de filas después de cada lote. Detente solo cuando el recuento y la altura de desplazamiento permanezcan sin cambios tras varias comprobaciones, y mantén un límite de seguridad de «máximo de pasadas».

Detente de forma segura y elimina los registros duplicados

Detente cuando el botón «Siguiente» no esté presente o esté desactivado, y mantén un límite de páginas. Si el control permanece activado pero la firma de las filas no cambia, informa de una paginación parcial en lugar de guardar el resultado de forma silenciosa.

El ejemplo elimina los duplicados exactos con un Map. Es preferible utilizar un ID estable o una URL canónica cuando estén disponibles, ya que las claves de registro completo pueden fusionar filas repetidas legítimas. Una guía sobre el scraping web con JavaScript y Node.js puede ayudar a separar la navegación, la extracción y el almacenamiento.

Solucionar errores comunes en el scraping de tablas

Al extraer datos de una tabla HTML en JavaScript, clasifica los errores según la recuperación, la selección, la paginación o la exportación.

Síntoma

Solución práctica

Axios analiza cero filas

Busca en la respuesta guardada un valor conocido; si no aparece, comprueba las solicitudes de red o utiliza un navegador

No hay ninguna tabla que coincida

Define un ID o contenedor estable e incluye el selector que ha fallado en el error

Desplazamiento de columnas

Inspecciona las celdas directamente y añade un tratamiento personalizado para estructuras que abarcan varias celdas o anidadas

Solo se guarda la primera página

Espera a que cambie la firma de una fila, registra el progreso de la página y genera un error si un control se bloquea

El texto de la celda está en blanco

Lee el enlace, el campo de entrada o el atributo de datos correspondiente, en lugar de solo el texto

Las solicitudes devuelven un código de error 403 o 429

Detén la operación, revisa los permisos de acceso, reduce la concurrencia y utiliza un retroceso limitado cuando sea apropiado

Las columnas del CSV se rompen

Utiliza un serializador probado y fixtures que contengan comillas, comas, saltos de línea y texto UTF-8

Añade medidas de seguridad responsables para la producción

Si extraes datos de una tabla HTML en JavaScript más allá de una prueba puntual, utiliza una lista de comprobación concisa para producción:

  • Revisa los términos, las normas de acceso y las directrices para robots antes de la recopilación. Obtén la revisión adecuada para casos de uso sensibles o regulados.
  • Mantén un nivel de concurrencia conservador, almacena en caché las páginas que no hayan cambiado, respeta Retry-After cuando proceda, y limita los reintentos con retrasos progresivos. Ningún retraso es seguro en todos los casos.
  • Recopila solo los campos necesarios, restringe el acceso, protege los datos conservados y elimínalos cuando finalice el periodo de conservación.
  • Registra el estado, el selector, el número de página, el recuento de filas y las advertencias, excluyendo las credenciales y los valores sensibles.

Las consecuencias legales pueden variar según el sitio web, los datos, el método y la jurisdicción. Considera esto como una guía técnica general, no como asesoramiento jurídico. Una guía práctica para el web scraping sin ser bloqueado puede incluir controles operativos una vez que se hayan establecido el permiso y el alcance.

Puntos clave

  • La forma más rápida de extraer datos de una tabla HTML en JavaScript es inspeccionar primero la respuesta y, a continuación, utilizar un navegador solo cuando los scripts o la interacción sean realmente necesarios.
  • Deriva claves únicas a partir de los encabezados, genera alternativas explícitas y señala las discrepancias en el recuento de celdas, en lugar de codificar de forma rígida las columnas de un sitio web concreto.
  • Conserva las cadenas sin procesar hasta que se conozcan las reglas de conversión; a continuación, valida las columnas obligatorias, la salida no vacía, las advertencias y los casos extremos de CSV.
  • En el caso de la paginación renderizada, espera a que se produzca un cambio medible en las filas, detén el proceso ante señales explícitas, limita el bucle y elimina duplicados con una clave significativa y estable.

Preguntas frecuentes

¿Por qué DevTools puede mostrar filas de tabla que Axios y Cheerio no encuentran?

DevTools muestra el DOM en tiempo real después de que el navegador haya ejecutado los scripts y normalizado el marcado, mientras que Axios recibe los bytes de la respuesta del servidor. El código del lado del cliente puede recuperar datos, insertar filas o sustituir un marcador de posición tras la carga. Los navegadores también pueden insertar elementos estructurales como <tbody>, así que compara un valor representativo y ten en cuenta la normalización del DOM al probar los selectores.

¿Necesito Puppeteer cuando una tabla muestra controles de paginación?

No. Los controles de paginación pueden ocultar filas que ya están presentes en el DOM, cambiar segmentos de un array en memoria o solicitar otra página desde un punto final de datos. Utiliza un navegador solo cuando cambiar de página requiera interacción con JavaScript y no haya ningún punto final adecuado disponible. Inspecciona el código fuente, el recuento de filas del DOM y las respuestas de red antes de añadir automatización del navegador.

¿Cómo puedo extraer enlaces o atributos de datos de las celdas de una tabla en lugar del texto visible?

Selecciona el elemento dentro de la celda y lee la propiedad correspondiente. Con Cheerio, utiliza .attr("href") para los enlaces, .attr("data-id") para los atributos de datos y .val() para los controles de formulario. Resuelve los enlaces relativos con new URL(relativeHref, pageUrl).href. Almacena el texto visible y el atributo en campos separados cuando ambos contengan información útil.

¿Puedo guardar los registros analizados como JSON o enviarlos a una base de datos en lugar de como CSV?

Sí. Genera JSON con JSON.stringify(records, null, 2), envía los registros a una API o insértalos en una base de datos. Valida primero el mismo esquema. Para las bases de datos, utiliza operaciones parametrizadas, lotes o transacciones cuando sea apropiado, y una clave única estable para que las ejecuciones posteriores puedan actualizar o insertar registros en lugar de crear duplicados accidentales.

Conclusión

Una forma fiable de extraer datos de una tabla HTML en JavaScript comienza con el diagnóstico, no con la elección de una biblioteca. Confirma si las filas existen en la respuesta, llegan a través de una solicitud de red o requieren un navegador que las represente. Para el marcado estático, Axios y Cheerio ofrecen una solución ligera, mientras que un analizador basado en encabezados hace que el resultado sea reutilizable en tablas convencionales, en lugar de limitarse a un diseño de columnas fijo.

Antes de la exportación, normaliza únicamente los espacios en blanco de presentación, conserva los valores ambiguos como cadenas, verifica los campos obligatorios, revisa las advertencias de discrepancias y prueba el uso de comillas en el CSV con datos de prueba hostiles. Para las tablas dinámicas, espera a que se actualicen los selectores y las filas, utiliza condiciones de parada explícitas, registra el progreso de la paginación y elimina duplicados con una clave que refleje los datos. Estas comprobaciones son las que diferencian una demostración de un rastreador que se pueda depurar.

Empieza con el script estático y añade complejidad solo cuando la página demuestre que es necesario. Si los bloqueos en la capa de peticiones, los retos CAPTCHA o la rotación de proxies se convierten en un cuello de botella, la API de rastreo de WebScrapingAPI puede devolver HTML sin procesar mientras gestiona esos problemas, de modo que puedas mantener el mismo código de análisis y validación de Cheerio.

Acerca del autor

Mihai Maxim, Desarrollador Full Stack @ WebScrapingAPI

Mihai Maxim

Desarrollador Full Stack

Mihai Maxim es desarrollador full stack en WebScrapingAPI, donde colabora en todas las áreas del producto y ayuda a crear herramientas y funciones fiables para la plataforma.

Extracción de datos web con AWS Lambda: guía para Python y Java 2026
Guías

Extracción de datos web con AWS Lambda: guía para Python y Java 2026

En resumen: el web scraping con AWS Lambda funciona mejor cuando cada invocación es breve, está delimitada y se puede reintentar de forma independiente. Empieza con HTTP directo, AWS SAM y S3, y añade SQS, contenedores, renderización en navegador, proxies o una capa de recuperación gestionada solo cuando la carga de trabajo demuestre que los necesita.

Suciu Dan33 min read
Leer artículo
Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos
Guías

Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos

En resumen: GoSpider es un rastreador de línea de comandos diseñado para descubrir URL, no un extractor completo de datos estructurados. Esta guía sobre cómo utilizar GoSpider muestra cómo realizar un rastreo limitado, gestionar correctamente los resultados, pasar los datos de Colly a CSV y seguir una ruta de diagnóstico para respuestas 403 o páginas que requieren renderización con JavaScript.

Suciu Dan24 min read
Leer artículo
Cómo raspar Redfin: Guía Python de Datos Inmobiliarios
Guías

Cómo raspar Redfin: Guía Python de Datos Inmobiliarios

TL;DR: Redfin expone puntos finales de API ocultos que devuelven JSON estructurado para los listados de propiedades, lo que permite omitir por completo el frágil análisis HTML. Esta guía te guía a través de la construcción de un raspador de Python que extrae datos de alquiler y venta, busca por ubicación, supervisa los nuevos listados a través de mapas de sitio XML y exporta resultados limpios a CSV o JSON.

Suciu Dan14 min read
Leer artículo

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.