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())
);
}
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>oudisplay: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;
}
Agora uma tabela assim:
<table>
<tr><td rowspan="2">A</td><td>B</td></tr>
<tr><td>C</td></tr>
</table>
Se torna corretamente:
[
["A", "B"],
["A", "C"] // "A" aparece em ambas as linhas
]
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();
}
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));
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");
}
Isso lida corretamente com o caso pesadelo:
toCSV([['Diga "Olá, Mundo"', "Normal"]])
// '"Diga ""Olá, Mundo""",Normal'
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, "");
}
Entrada:
| Nome Produto | Preço (R$) |
|--------------|------------|
| Widget | 29,99 |
Saída:
[
{
"nome_produto": "Widget",
"preco": "29.99"
}
]
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");
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();
})();
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)