DEV Community

Cover image for Automatizando Exportação de Tabelas Web com JavaScript: Um Guia Prático
circobit
circobit

Posted on

Automatizando Exportação de Tabelas Web com JavaScript: Um Guia Prático

Você encontrou uma tabela num site. Precisa desses dados numa planilha. O caminho óbvio — copiar, colar, limpar no Excel — funciona uma vez. Mas e se você precisa desses dados toda semana? Ou de 50 páginas diferentes?

Este guia mostra como extrair tabelas HTML programaticamente com JavaScript, lidar com os casos extremos que quebram abordagens ingênuas, e exportar para formatos que suas ferramentas realmente aceitam.

A Abordagem Ingênua (E Por Que Ela Falha)

A extração mais simples é assim:

function extractTable(table) {
  return Array.from(table.rows).map(row => 
    Array.from(row.cells).map(cell => cell.textContent.trim())
  );
}
Enter fullscreen mode Exit fullscreen mode

Funciona para tabelas simples. Quebra imediatamente quando você encontra:

  • Rowspan/colspan — Células que abrangem múltiplas linhas ou colunas
  • Tabelas aninhadas — Tabelas dentro de células de tabela
  • Conteúdo oculto — Elementos <style>, <script> ou display:none
  • Caracteres especiais — Quebras de linha, tabulações e aspas no conteúdo das células

Vamos corrigir cada um.

Lidando com Rowspan e Colspan

Quando uma célula tem rowspan="2", ela ocupa espaço na linha atual E na próxima. Um extrator ingênuo vê menos células do que o esperado e desalinha as colunas.

A solução: construir um grid virtual que rastreia posições ocupadas.

function extractTableMatrix(table) {
  const rows = Array.from(table.rows);
  const grid = [];

  rows.forEach((rowEl, rowIndex) => {
    if (!grid[rowIndex]) grid[rowIndex] = [];

    let colIndex = 0;

    Array.from(rowEl.cells).forEach(cell => {
      // Pular colunas já ocupadas por rowspans anteriores
      while (grid[rowIndex][colIndex] !== undefined) {
        colIndex++;
      }

      const text = cell.textContent.trim();
      const rowSpan = parseInt(cell.rowSpan, 10) || 1;
      const colSpan = parseInt(cell.colSpan, 10) || 1;

      // Preencher o bloco retangular que esta célula ocupa
      for (let r = 0; r < rowSpan; r++) {
        const targetRow = rowIndex + r;
        if (!grid[targetRow]) grid[targetRow] = [];

        for (let c = 0; c < colSpan; c++) {
          const targetCol = colIndex + c;
          if (grid[targetRow][targetCol] === undefined) {
            grid[targetRow][targetCol] = text;
          }
        }
      }

      colIndex += colSpan;
    });
  });

  return grid;
}
Enter fullscreen mode Exit fullscreen mode

Agora uma tabela assim:

<table>
  <tr><td rowspan="2">A</td><td>B</td></tr>
  <tr><td>C</td></tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Se torna corretamente:

[
  ["A", "B"],
  ["A", "C"]  // "A" aparece em ambas as linhas
]
Enter fullscreen mode Exit fullscreen mode

Extraindo Texto Limpo

textContent pega tudo — incluindo regras CSS em tags <style> e JavaScript em tags <script> que algumas páginas injetam nas células da tabela.

Extração limpa requer filtragem:

function extractCellText(cell) {
  if (!cell) return "";

  // Clonar para evitar modificar o DOM
  const clone = cell.cloneNode(true);

  // Remover elementos invisíveis
  const invisibleSelectors = "style, script, noscript, template, link";
  clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());

  // Normalizar espaços em branco
  return (clone.textContent || "").replace(/\s+/g, " ").trim();
}
Enter fullscreen mode Exit fullscreen mode

Detectando Tabelas Aninhadas

Quando uma tabela contém outra tabela em uma célula, você normalmente quer os dados da tabela externa, não uma bagunça recursiva.

A detecção é simples:

function isNestedTable(table, allTables) {
  let parent = table.parentElement;

  while (parent) {
    if (parent.tagName === "TABLE") {
      return true;  // Esta tabela está dentro de outra tabela
    }
    parent = parent.parentElement;
  }

  return false;
}

// Filtrar ao escanear uma página
const allTables = document.querySelectorAll("table");
const topLevelTables = Array.from(allTables)
  .filter(t => !isNestedTable(t, allTables));
Enter fullscreen mode Exit fullscreen mode

Convertendo para CSV

CSV parece simples até você precisar lidar com:

  • Vírgulas dentro de valores
  • Aspas dentro de valores
  • Quebras de linha dentro de valores

A abordagem compatível com RFC 4180:

function toCSV(rows, delimiter = ",") {
  return rows.map(row =>
    row.map(cell => {
      if (cell == null) cell = "";
      const str = String(cell);

      // Colocar entre aspas se contém delimitador, aspas ou quebras de linha
      const needsQuotes = str.includes(delimiter) || /["\r\n]/.test(str);
      const escaped = str.replace(/"/g, '""');

      return needsQuotes ? `"${escaped}"` : escaped;
    }).join(delimiter)
  ).join("\r\n");
}
Enter fullscreen mode Exit fullscreen mode

Isso lida corretamente com o caso pesadelo:

toCSV([['Diga "Olá, Mundo"', "Normal"]])
// '"Diga ""Olá, Mundo""",Normal'
Enter fullscreen mode Exit fullscreen mode

Para um guia completo sobre exportações CSV, veja As 5 Melhores Extensões Chrome para Exportar Tabelas.

Convertendo para JSON

Para exportação JSON, a primeira linha se torna as chaves:

function toJSON(rows) {
  if (rows.length < 2) return "[]";

  const headers = rows[0].map((h, i) => sanitizeKey(h, i));
  const dataRows = rows.slice(1);

  const objects = dataRows.map(row => {
    const obj = {};
    headers.forEach((key, i) => {
      obj[key] = row[i] ?? "";
    });
    return obj;
  });

  return JSON.stringify(objects, null, 2);
}

function sanitizeKey(header, index) {
  let key = (header || "").toString().trim();

  if (!key) return `col_${index + 1}`;

  // Normalizar para snake_case minúsculo
  return key
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")  // Remover acentos
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");
}
Enter fullscreen mode Exit fullscreen mode

Entrada:

| Nome Produto | Preço (R$) |
|--------------|------------|
| Widget       | 29,99      |
Enter fullscreen mode Exit fullscreen mode

Saída:

[
  {
    "nome_produto": "Widget",
    "preco": "29.99"
  }
]
Enter fullscreen mode Exit fullscreen mode

Disparando o Download

No contexto do navegador, você pode disparar um download sem servidor:

function downloadFile(content, filename, mimeType) {
  const blob = new Blob([content], { type: mimeType });
  const url = URL.createObjectURL(blob);

  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.click();

  URL.revokeObjectURL(url);
}

// Uso
const csv = toCSV(extractTableMatrix(table));
downloadFile(csv, "dados.csv", "text/csv;charset=utf-8");
Enter fullscreen mode Exit fullscreen mode

Juntando Tudo

Aqui está um bookmarklet mínimo que exporta a primeira tabela de qualquer página:

javascript:(function(){
  const table = document.querySelector("table");
  if (!table) { alert("Nenhuma tabela encontrada"); return; }

  function extractTableMatrix(table) {
    const rows = Array.from(table.rows);
    const grid = [];
    rows.forEach((rowEl, ri) => {
      if (!grid[ri]) grid[ri] = [];
      let ci = 0;
      Array.from(rowEl.cells).forEach(cell => {
        while (grid[ri][ci] !== undefined) ci++;
        const text = cell.textContent.trim();
        const rs = parseInt(cell.rowSpan) || 1;
        const cs = parseInt(cell.colSpan) || 1;
        for (let r = 0; r < rs; r++) {
          if (!grid[ri+r]) grid[ri+r] = [];
          for (let c = 0; c < cs; c++) {
            if (grid[ri+r][ci+c] === undefined) grid[ri+r][ci+c] = text;
          }
        }
        ci += cs;
      });
    });
    return grid;
  }

  const data = extractTableMatrix(table);
  const csv = data.map(row => 
    row.map(c => c.includes(",") ? `"${c}"` : c).join(",")
  ).join("\n");

  const blob = new Blob([csv], {type: "text/csv"});
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = "tabela.csv";
  link.click();
})();
Enter fullscreen mode Exit fullscreen mode

Quando Usar uma Extensão de Navegador

Esse código funciona, mas mantê-lo em diferentes sites é tedioso. Se você extrai tabelas regularmente, uma extensão de navegador cuida de:

  • Múltiplas tabelas por página
  • Seleção de formato (CSV, JSON, Excel)
  • Limpeza de dados (normalização de números, tratamento de nulos)
  • Seleção e reordenação de colunas

Eu criei o HTML Table Exporter exatamente para esse fluxo de trabalho. Os algoritmos centrais são similares ao que foi mostrado aqui, empacotados numa interface usável.

Saiba mais em gauchogrid.com/pt-br/html-table-exporter ou experimente gratuitamente na Chrome Web Store.


Dúvidas sobre casos extremos de extração de tabelas? Deixe um comentário — provavelmente já encontrei o mesmo problema.

Top comments (0)