Detectando Linhas de Cabeçalho Automaticamente: As Heurísticas por Trás do Parsing de Tabelas

javascript dev.to

A primeira linha de uma tabela HTML é a linha de cabeçalho.

Exceto quando não é.

Tabelas da Wikipedia frequentemente têm uma linha de título que abrange todas as colunas antes dos cabeçalhos reais. Sites de estatísticas esportivas têm cabeçalhos agrupados onde "Tempo de Jogo" abrange múltiplas sub-colunas como "PJ", "Titular", "Min". Tabelas financeiras têm linhas de unidade ("em milhões USD") que parecem cabeçalhos mas não são.

Se você assume que a linha 0 é sempre o cabeçalho, suas exportações estarão quebradas para uma parte significativa das tabelas do mundo real.

Veja como detectar a linha de cabeçalho real programaticamente.

O Problema: Três Tipos de "Primeiras Linhas"

Considere estes padrões comuns:

Padrão 1: Linha de Título

<table>
  <tr>
    <th colspan="4">População Mundial por País</th>  <!-- Título, não cabeçalho -->
  </tr>
  <tr>
    <th>Posição</th>
    <th>País</th>
    <th>População</th>
    <th>% do Mundo</th>
  </tr>
  <tr>
    <td>1</td>
    <td>Índia</td>
    <td>1.428.627.663</td>
    <td>17,85%</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

A linha 0 é um título. A linha 1 é o cabeçalho. A linha 2+ são dados.

Padrão 2: Cabeçalhos Agrupados (Dois Níveis)

<table>
  <tr>
    <th></th>
    <th></th>
    <th colspan="3">Tempo de Jogo</th>
    <th colspan="2">Desempenho</th>
  </tr>
  <tr>
    <th>Jogador</th>
    <th>Nação</th>
    <th>PJ</th>
    <th>Titular</th>
    <th>Min</th>
    <th>Gols</th>
    <th>Ast</th>
  </tr>
  <tr>
    <td>João Silva</td>
    <td>BRA</td>
    <td>34</td>
    <td>30</td>
    <td>2700</td>
    <td>12</td>
    <td>8</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

A linha 0 são cabeçalhos de grupo. A linha 1 são os cabeçalhos de coluna reais. A linha 2+ são dados.

Padrão 3: Prefixo de Navegação da Wikipedia

<tr>
  <th colspan="3">v t e Patrimônios Mundiais da UNESCO</th>
</tr>
Enter fullscreen mode Exit fullscreen mode

Os links "v t e" (ver/discutir/editar) são a navegação de template da Wikipedia. Eles precisam ser removidos, e a linha ainda pode ser um título em vez de cabeçalho.

Heurística 1: Detectando Linhas de Título

Uma linha de título tipicamente tem:

  • Uma única célula (ou muito poucas)
  • Colspan grande abrangendo a maioria/todas as colunas
  • Conteúdo de texto que parece um título, não nomes de colunas
function isTitleRow(row, totalColumns) {
  if (!row || row.length === 0) return false;

  // Contar células não vazias
  const nonEmptyCells = row.filter(cell => cell && cell.trim()).length;

  // Linhas de título geralmente têm 1-2 células não vazias
  if (nonEmptyCells > 2) return false;

  // Verificar se primeira célula abrange a maioria das colunas (indica colspan)
  // Em uma matriz normalizada, isso aparece como valores repetidos
  const firstValue = row[0];
  const repeatedCount = row.filter(cell => cell === firstValue).length;

  // Se o primeiro valor se repete em >50% das colunas, provavelmente é um título colspan
  if (repeatedCount > totalColumns * 0.5) {
    return true;
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 2: O Que Faz Uma Linha "Parecer Cabeçalhos"

Linhas de cabeçalho têm características que as distinguem de linhas de dados:

function rowLooksLikeHeaders(row) {
  if (!row || row.length === 0) return false;

  // Se dominada por números puros, não é cabeçalho
  let numericCells = 0;
  let textCells = 0;
  let emptyCells = 0;

  for (const cell of row) {
    const value = (cell || "").trim();

    if (!value) {
      emptyCells++;
    } else if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
      // Número puro ou porcentagem
      numericCells++;
    } else {
      textCells++;
    }
  }

  const totalNonEmpty = numericCells + textCells;
  if (totalNonEmpty === 0) return false;

  // Cabeçalhos são principalmente texto, não números
  // Se >70% das células não vazias são numéricas, provavelmente são dados
  if (numericCells / totalNonEmpty > 0.7) {
    return false;
  }

  // Cabeçalhos não devem ser principalmente vazios
  if (emptyCells / row.length > 0.7) {
    return false;
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 3: O Que Faz Uma Linha "Parecer Dados"

A verificação inversa ajuda a confirmar que encontramos a fronteira correta:

function rowLooksLikeData(row) {
  if (!row || row.length === 0) return false;

  let numericCells = 0;
  let dateCells = 0;
  let totalNonEmpty = 0;

  for (const cell of row) {
    const value = (cell || "").trim();
    if (!value) continue;

    totalNonEmpty++;

    // Verificar padrões numéricos
    if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
      numericCells++;
    }

    // Verificar padrões de data
    if (/^\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4}$/.test(value)) {
      dateCells++;
    }
  }

  if (totalNonEmpty === 0) return false;

  // Linhas de dados tipicamente têm conteúdo numérico ou de data
  const dataLikeCells = numericCells + dateCells;
  return dataLikeCells / totalNonEmpty > 0.3;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 4: Detectando Cabeçalhos de Coluna Agrupados

Tabelas estilo FBREF têm uma linha de cabeçalho de grupo seguida por uma linha de sub-cabeçalho. A linha de grupo tem:

  • Células vazias no início (colunas sem grupos)
  • Valores repetidos da expansão de colspan
  • Múltiplos valores únicos não vazios (não apenas um como um título)
function detectGroupHeaderRow(row, nextRow) {
  if (!row || !nextRow || row.length < 4) return false;

  // Linhas de cabeçalho de grupo DEVEM ter células vazias no início
  // Isso distingue de tabelas duplicadas horizontalmente
  const firstCellEmpty = !(row[0] || "").trim();
  if (!firstCellEmpty) return false;

  // Contar valores únicos não vazios
  const uniqueValues = new Set(
    row.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
  );

  // Uma linha de título tem exatamente UM valor único
  // Uma linha de cabeçalho de grupo deve ter MÚLTIPLOS valores únicos
  if (uniqueValues.size <= 1) return false;

  // Contar repetições consecutivas (indica expansão de colspan)
  let consecutiveRepeats = 0;
  for (let i = 1; i < row.length; i++) {
    const curr = (row[i] || "").trim();
    const prev = (row[i - 1] || "").trim();
    if (curr === prev) consecutiveRepeats++;
  }

  const repeatRatio = consecutiveRepeats / (row.length - 1);

  // Alta taxa de repetição (>30%) sugere expansão de colspan
  // A próxima linha deve ter mais valores únicos (os sub-cabeçalhos reais)
  const nextUniqueValues = new Set(
    nextRow.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
  );

  return repeatRatio > 0.3 && nextUniqueValues.size > uniqueValues.size;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 5: Limpando Prefixos de Navegação da Wikipedia

Templates da Wikipedia frequentemente prefixam conteúdo com "v t e" (links para ver/discutir/editar o template):

function cleanWikipediaNavPrefix(text) {
  if (!text) return text;

  // Padrão 1: "v t e " no início (separado por espaço)
  // Padrão 2: "v | t | e " (separado por pipe)
  // Padrão 3: "[v] [t] [e] " (separado por colchetes)

  return text
    .replace(/^\s*v\s+t\s+e\s+/i, "")
    .replace(/^\s*v\s*\|\s*t\s*\|\s*e\s+/i, "")
    .replace(/^\s*\[v\]\s*\[t\]\s*\[e\]\s+/i, "")
    .trim();
}
Enter fullscreen mode Exit fullscreen mode

Juntando Tudo: O Algoritmo de Detecção

function detectHeaderRowIndex(matrix) {
  if (!matrix || matrix.length < 2) return 0;

  const totalColumns = matrix[0]?.length || 0;

  for (let i = 0; i < Math.min(matrix.length - 1, 5); i++) {
    const currentRow = matrix[i];
    const nextRow = matrix[i + 1];

    // Pular linhas de título
    if (isTitleRow(currentRow, totalColumns)) {
      continue;
    }

    // Verificar cabeçalhos agrupados (dois níveis)
    if (detectGroupHeaderRow(currentRow, nextRow)) {
      // A linha de sub-cabeçalho (i+1) é o cabeçalho real
      return i + 1;
    }

    // Verificar se esta linha parece cabeçalho e a próxima parece dados
    if (rowLooksLikeHeaders(currentRow) && rowLooksLikeData(nextRow)) {
      return i;
    }
  }

  // Fallback: assumir linha 0 como cabeçalho
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Testes no Mundo Real

Essas heurísticas foram desenvolvidas testando com:

  • Tabelas de países/população da Wikipedia (linhas de título + prefixos "v t e")
  • Estatísticas de jogadores do FBREF (cabeçalhos agrupados)
  • Tabelas financeiras com linhas de unidade
  • Tabelas de dados governamentais com múltiplos níveis de cabeçalho

Nenhuma heurística é perfeita. O objetivo é lidar corretamente com os padrões comuns e falhar graciosamente em tabelas incomuns.

Quando a Detecção Falha

Para tabelas que não se encaixam nos padrões comuns, forneça uma opção de override manual:

function extractTable(matrix, options = {}) {
  const headerRowIndex = options.headerRowIndex ?? detectHeaderRowIndex(matrix);

  const headerRow = matrix[headerRowIndex];
  const dataRows = matrix.slice(headerRowIndex + 1);

  return { headerRow, dataRows };
}
Enter fullscreen mode Exit fullscreen mode

Usuários que conhecem seus dados podem especificar a linha de cabeçalho explicitamente.

Resumo

Padrão Método de Detecção
Linha de título Célula única com colspan grande
Cabeçalho padrão Linha com mais texto, seguida por linha com números
Cabeçalhos agrupados Primeiras células vazias + valores repetidos + mais valores únicos na próxima linha
Navegação Wikipedia Padrão de prefixo "v t e"

O insight-chave: cabeçalhos e dados têm características diferentes. Cabeçalhos são pesados em texto com rótulos descritivos. Dados são pesados em números com valores reais. A fronteira entre eles geralmente é detectável.

Para saber mais sobre os desafios específicos das tabelas da Wikipedia, veja nosso guia sobre scraper de tabelas HTML para Chrome.


Precisa de detecção automática de cabeçalho sem escrever código? Saiba mais em gauchogrid.com/pt-br/html-table-exporter ou experimente gratuitamente na Chrome Web Store.

Source: dev.to

arrow_back Back to Tutorials