DEV Community

Cover image for Detectando Filas de Encabezado Automáticamente: Las Heurísticas Detrás del Parsing de Tablas
circobit
circobit

Posted on

Detectando Filas de Encabezado Automáticamente: Las Heurísticas Detrás del Parsing de Tablas

La primera fila de una tabla HTML es la fila de encabezado.

Excepto cuando no lo es.

Las tablas de Wikipedia suelen tener una fila de título que abarca todas las columnas antes de los encabezados reales. Los sitios de estadísticas deportivas tienen encabezados agrupados donde "Tiempo de Juego" abarca múltiples sub-columnas como "PJ", "Titular", "Min". Las tablas financieras tienen filas de unidad ("en millones USD") que parecen encabezados pero no lo son.

Si asumes que la fila 0 siempre es el encabezado, tus exportaciones van a estar rotas para una porción significativa de tablas del mundo real.

Aquí te muestro cómo detectar la fila de encabezado real programáticamente.

El Problema: Tres Tipos de "Primeras Filas"

Considerá estos patrones comunes:

Patrón 1: Fila de Título

<table>
  <tr>
    <th colspan="4">Población Mundial por País</th>  <!-- Título, no encabezado -->
  </tr>
  <tr>
    <th>Ranking</th>
    <th>País</th>
    <th>Población</th>
    <th>% del Mundo</th>
  </tr>
  <tr>
    <td>1</td>
    <td>India</td>
    <td>1.428.627.663</td>
    <td>17,85%</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

La fila 0 es un título. La fila 1 es el encabezado. La fila 2+ son datos.

Patrón 2: Encabezados Agrupados (Dos Niveles)

<table>
  <tr>
    <th></th>
    <th></th>
    <th colspan="3">Tiempo de Juego</th>
    <th colspan="2">Rendimiento</th>
  </tr>
  <tr>
    <th>Jugador</th>
    <th>Nación</th>
    <th>PJ</th>
    <th>Titular</th>
    <th>Min</th>
    <th>Goles</th>
    <th>Asist</th>
  </tr>
  <tr>
    <td>Juan Pérez</td>
    <td>ARG</td>
    <td>34</td>
    <td>30</td>
    <td>2700</td>
    <td>12</td>
    <td>8</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

La fila 0 son encabezados de grupo. La fila 1 son los encabezados de columna reales. La fila 2+ son datos.

Patrón 3: Prefijo de Navegación de Wikipedia

<tr>
  <th colspan="3">v t e Sitios del Patrimonio Mundial</th>
</tr>
Enter fullscreen mode Exit fullscreen mode

Los enlaces "v t e" (ver/discusión/editar) son la navegación de plantillas de Wikipedia. Necesitan ser eliminados, y la fila podría ser un título en lugar de un encabezado.

Heurística 1: Detectar Filas de Título

Una fila de título típicamente tiene:

  • Una sola celda (o muy pocas celdas)
  • colspan grande abarcando la mayoría/todas las columnas
  • Contenido de texto que parece un título, no nombres de columnas
function isTitleRow(row, totalColumns) {
  if (!row || row.length === 0) return false;

  // Contar celdas no vacías
  const nonEmptyCells = row.filter(cell => cell && cell.trim()).length;

  // Las filas de título suelen tener 1-2 celdas no vacías
  if (nonEmptyCells > 2) return false;

  // Verificar si la primera celda abarca la mayoría de columnas (indica colspan)
  // En una matriz normalizada, esto se muestra como valores repetidos
  const firstValue = row[0];
  const repeatedCount = row.filter(cell => cell === firstValue).length;

  // Si el primer valor se repite en >50% de las columnas, probablemente es un título con colspan
  if (repeatedCount > totalColumns * 0.5) {
    return true;
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 2: Qué Hace Que una Fila "Parezca Encabezados"

Las filas de encabezado tienen características que las distinguen de las filas de datos:

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

  // Si está dominada por números puros, no son encabezados
  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 o porcentaje
      numericCells++;
    } else {
      textCells++;
    }
  }

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

  // Los encabezados son mayormente texto, no números
  // Si >70% de las celdas no vacías son numéricas, probablemente son datos
  if (numericCells / totalNonEmpty > 0.7) {
    return false;
  }

  // Los encabezados no deberían estar mayormente vacíos
  if (emptyCells / row.length > 0.7) {
    return false;
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 3: Qué Hace Que una Fila "Parezca Datos"

La verificación inversa ayuda a confirmar que encontramos el límite correcto:

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 patrones numéricos
    if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
      numericCells++;
    }

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

  if (totalNonEmpty === 0) return false;

  // Las filas de datos típicamente tienen contenido numérico o de fecha
  const dataLikeCells = numericCells + dateCells;
  return dataLikeCells / totalNonEmpty > 0.3;
}
Enter fullscreen mode Exit fullscreen mode

Heurística 4: Detectar Encabezados de Columna Agrupados

Las tablas estilo FBREF tienen una fila de encabezado de grupo seguida por una fila de sub-encabezados. La fila de grupo tiene:

  • Celdas vacías al principio (columnas sin grupos)
  • Valores repetidos por expansión de colspan
  • Múltiples valores únicos no vacíos (no solo uno como un título)
function detectGroupHeaderRow(row, nextRow) {
  if (!row || !nextRow || row.length < 4) return false;

  // Las filas de encabezado de grupo DEBEN tener celdas vacías al inicio
  const firstCellEmpty = !(row[0] || "").trim();
  if (!firstCellEmpty) return false;

  // Contar valores únicos no vacíos
  const uniqueValues = new Set(
    row.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
  );

  // Una fila de título tiene exactamente UN valor único
  // Una fila de encabezado de grupo debe tener MÚLTIPLES valores únicos
  if (uniqueValues.size <= 1) return false;

  // Contar repeticiones consecutivas (indica expansión 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);

  // Ratio alto de repetición (>30%) sugiere expansión de colspan
  // La siguiente fila debería tener más valores únicos (los sub-encabezados reales)
  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: Limpiar Prefijos de Navegación de Wikipedia

Las plantillas de Wikipedia suelen tener prefijo con "v t e" (enlaces para ver/discusión/editar la plantilla):

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

  // Patrón 1: "v t e " al inicio (separados por espacios)
  // Patrón 2: "v | t | e " (separados por pipes)
  // Patrón 3: "[v] [t] [e] " (separados por corchetes)

  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

Uniendo Todo: El Algoritmo de Detección

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];

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

    // Verificar encabezados agrupados (dos niveles)
    if (detectGroupHeaderRow(currentRow, nextRow)) {
      // La fila de sub-encabezados (i+1) es el encabezado real
      return i + 1;
    }

    // Verificar si esta fila parece encabezados y la siguiente parece datos
    if (rowLooksLikeHeaders(currentRow) && rowLooksLikeData(nextRow)) {
      return i;
    }
  }

  // Fallback: asumir que la fila 0 es el encabezado
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Testing en el Mundo Real

Estas heurísticas se desarrollaron probando contra:

  • Tablas de países/población de Wikipedia (filas de título + prefijos "v t e")
  • Estadísticas de jugadores de FBREF (encabezados agrupados)
  • Tablas financieras con filas de unidad
  • Tablas de datos gubernamentales con múltiples niveles de encabezado

Ninguna heurística es perfecta. El objetivo es manejar los patrones comunes correctamente y fallar graciosamente en tablas inusuales.

Cuando la Detección Falla

Para tablas que no encajan en patrones comunes, proporciona una opción 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

Los usuarios que conocen sus datos pueden especificar la fila de encabezado explícitamente.

Resumen

Patrón Método de Detección
Fila de título Celda única con colspan grande
Encabezado estándar Fila con mayormente texto, seguida de fila con números
Encabezados agrupados Primeras celdas vacías + valores repetidos + más valores únicos en la siguiente fila
Navegación Wikipedia Patrón de prefijo "v t e"

La idea clave: los encabezados y los datos tienen características diferentes. Los encabezados son ricos en texto con etiquetas descriptivas. Los datos son ricos en números con valores reales. El límite entre ellos generalmente es detectable.

Para más sobre los desafíos específicos de las tablas de Wikipedia, consulta nuestra guía sobre exportar tablas de Wikipedia a Excel.


¿Necesitas detección automática de encabezados sin escribir código? Conoce más en gauchogrid.com/es/html-table-exporter o pruébalo gratis en la Chrome Web Store.

Top comments (0)