Tabelas com células mescladas estão em todo lugar. A Wikipedia usa. Sites de estatísticas esportivas usam. Relatórios financeiros usam.
E elas quebram quase todo scraper de tabelas que eu já vi.
O problema: quando você itera pelos elementos <tr> e <td>, você obtém o que está no HTML — não o que é renderizado visualmente. Uma célula com rowspan="3" aparece uma vez no HTML, mas ocupa três linhas na saída.
A abordagem ingênua falha silenciosamente. Você obtém colunas desalinhadas, dados faltantes e exportações que não se parecem em nada com a tabela original.
Veja como resolver.
O Problema: HTML Não Corresponde à Grade
Considere esta tabela simples com uma célula mesclada:
<table>
<tr>
<td rowspan="2">A</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
</tr>
</table>
Representação visual:
| A | B |
| | C |
Mas se você extrair células ingenuamente iterando pelas linhas:
// Abordagem errada
table.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('td');
console.log(cells.map(c => c.textContent));
});
// Saída: ["A", "B"], ["C"]
// Esperado: ["A", "B"], ["A", "C"]
A linha 2 está sem o "A" que visualmente se estende até ela.
A Solução: Construir uma Matriz
A abordagem correta é pré-alocar uma grade 2D e preenchê-la de acordo com os atributos de span.
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
const gridRow = grid[rowIndex];
let colIndex = 0;
// Pular colunas já ocupadas por rowspans de linhas anteriores
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
// Avançar além de células ocupadas
while (gridRow[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 que esta célula ocupa
for (let r = 0; r < rowSpan; r++) {
const targetRowIndex = rowIndex + r;
if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
for (let c = 0; c < colSpan; c++) {
const targetColIndex = colIndex + c;
if (grid[targetRowIndex][targetColIndex] === undefined) {
grid[targetRowIndex][targetColIndex] = text;
}
}
}
colIndex += colSpan;
});
});
return grid;
}
Agora a saída corresponde à tabela visual:
const matrix = extractTableMatrix(table);
// [["A", "B"], ["A", "C"]]
Por Que Isso Importa: Exemplos Reais
Tabelas de Países na Wikipedia
A Wikipedia usa células mescladas extensivamente. Uma tabela listando países por região pode ter:
<tr>
<td rowspan="5">Europa</td>
<td>França</td>
</tr>
<tr>
<td>Alemanha</td>
</tr>
<!-- ... -->
Sem tratamento adequado, você perde a associação "Europa" para as linhas 2-5.
Sites de Estatísticas Esportivas
Sites como FBREF usam tanto rowspan QUANTO colspan. Tabelas de estatísticas de jogadores têm:
- Cabeçalhos agrupados com colspan (Tempo de Jogo → MP | Titulares | Min)
- Linhas de resumo com rowspan abrangendo detalhes dos jogadores
Um scraper ingênuo exporta lixo. Uma abordagem baseada em matriz preserva a estrutura.
Relatórios Financeiros
Relatórios trimestrais frequentemente têm:
<td colspan="4">2024</td>
<!-- Depois: Q1, Q2, Q3, Q4 -->
O cabeçalho do ano precisa se propagar para todas as quatro colunas de trimestre.
Casos Extremos a Tratar
Células Vazias no Meio
Às vezes as células estão genuinamente vazias. Não confunda isso com "ainda não preenchida por um span".
// Normalizar para o mesmo número de colunas
const maxCols = grid.reduce((max, row) =>
Math.max(max, row ? row.length : 0), 0
);
const normalized = grid.map(row => {
const r = row || [];
const out = new Array(maxCols);
for (let i = 0; i < maxCols; i++) {
out[i] = r[i] != null ? r[i] : "";
}
return out;
});
Tabelas Aninhadas
Tabelas dentro de células criam ruído. Na maioria das vezes, você quer o conteúdo de texto da tabela aninhada, não uma extração recursiva.
function extractCellText(cell) {
const clone = cell.cloneNode(true);
// Remover tabelas aninhadas, scripts, estilos
clone.querySelectorAll("table, script, style, noscript")
.forEach(el => el.remove());
return clone.textContent.replace(/\s+/g, " ").trim();
}
Colspan + Rowspan Combinados
O algoritmo lida com isso naturalmente — você está apenas preenchendo um retângulo maior na grade.
<td rowspan="2" colspan="3">Célula Grande</td>
Preenche um bloco 2×3 com o mesmo valor.
Quando Usar uma Ferramenta
Escrever isso do zero é educativo, mas demorado. Para uso em produção, considere:
- HTML Table Exporter — Extensão de navegador que lida com esses casos automaticamente
- Pandas read_html() — Funciona, mas tem dificuldades com tabelas aninhadas complexas
- Beautiful Soup — Requer tratamento manual de spans
O Algoritmo Completo
Aqui está a versão de produção com todos os casos extremos tratados:
function extractCellText(cell) {
if (!cell) return "";
const clone = cell.cloneNode(true);
// Remover elementos com conteúdo não visível
const invisibleSelectors = "style, script, noscript, template, link";
clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());
const text = clone.textContent || "";
return text.replace(/\s+/g, " ").trim();
}
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
const gridRow = grid[rowIndex];
let colIndex = 0;
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const text = extractCellText(cell);
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
for (let r = 0; r < rowSpan; r++) {
const targetRowIndex = rowIndex + r;
if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
const targetRow = grid[targetRowIndex];
for (let c = 0; c < colSpan; c++) {
const targetColIndex = colIndex + c;
if (targetRow[targetColIndex] === undefined) {
targetRow[targetColIndex] = text;
}
}
}
colIndex += colSpan;
});
});
// Normalizar todas as linhas para o mesmo comprimento
const maxCols = grid.reduce((max, row) =>
Math.max(max, row ? row.length : 0), 0
);
return grid.map(row => {
const r = row || [];
const out = new Array(maxCols);
for (let i = 0; i < maxCols; i++) {
out[i] = r[i] != null ? r[i] : "";
}
return out;
});
}
Resumo
| Abordagem | Lida com Spans | Pronto para Produção |
|---|---|---|
| Iteração ingênua | ❌ | ❌ |
| Matriz com spans | ✅ | ✅ |
| Pandas read_html | Parcial | Depende |
| Extensão de navegador | ✅ | ✅ |
O insight chave: pense em tabelas como grades, não como elementos HTML aninhados. Construa a grade primeiro, depois exporte.
Para mais sobre exportação das tabelas particularmente complicadas da Wikipedia, veja nosso guia sobre a melhor extensão Chrome para copiar tabelas para Excel.
Precisa extrair tabelas sem escrever código? Saiba mais em gauchogrid.com/pt-br/html-table-exporter ou experimente gratuitamente na Chrome Web Store.
Top comments (0)