DEV Community

Cover image for Automatische Erkennung von Header-Zeilen: Die Heuristiken hinter dem Tabellen-Parsing
circobit
circobit

Posted on

Automatische Erkennung von Header-Zeilen: Die Heuristiken hinter dem Tabellen-Parsing

Die erste Zeile einer HTML-Tabelle ist die Header-Zeile.

Außer wenn nicht.

Wikipedia-Tabellen haben oft eine Titelzeile, die alle Spalten überspannt, bevor die eigentlichen Header kommen. Sportstatistik-Seiten haben gruppierte Header, bei denen „Spielzeit" mehrere Unterspalten wie „SP", „Starts", „Min" überspannt. Finanztabellen haben Einheitenzeilen („in Millionen EUR"), die wie Header aussehen, aber keine sind.

Wenn man annimmt, dass Zeile 0 immer der Header ist, werden die Exporte bei einem erheblichen Anteil realer Tabellen fehlerhaft sein.

So erkennt man die tatsächliche Header-Zeile programmatisch.

Das Problem: Drei Arten von „ersten Zeilen"

Diese häufigen Muster gibt es:

Muster 1: Titelzeile

<table>
  <tr>
    <th colspan="4">Weltbevölkerung nach Land</th>  <!-- Titel, kein Header -->
  </tr>
  <tr>
    <th>Rang</th>
    <th>Land</th>
    <th>Bevölkerung</th>
    <th>% der Welt</th>
  </tr>
  <tr>
    <td>1</td>
    <td>Indien</td>
    <td>1.428.627.663</td>
    <td>17,85 %</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Zeile 0 ist ein Titel. Zeile 1 ist der Header. Zeile 2+ sind Daten.

Muster 2: Gruppierte Header (zweistufig)

<table>
  <tr>
    <th></th>
    <th></th>
    <th colspan="3">Spielzeit</th>
    <th colspan="2">Leistung</th>
  </tr>
  <tr>
    <th>Spieler</th>
    <th>Nation</th>
    <th>SP</th>
    <th>Starts</th>
    <th>Min</th>
    <th>Tore</th>
    <th>Assists</th>
  </tr>
  <tr>
    <td>Max Müller</td>
    <td>GER</td>
    <td>34</td>
    <td>30</td>
    <td>2700</td>
    <td>12</td>
    <td>8</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Zeile 0 sind Gruppen-Header. Zeile 1 sind die tatsächlichen Spalten-Header. Zeile 2+ sind Daten.

Muster 3: Wikipedia-Navigationspräfix

<tr>
  <th colspan="3">v t e Welterbestätten</th>
</tr>
Enter fullscreen mode Exit fullscreen mode

Die „v t e"-Links (view/talk/edit) sind Wikipedias Template-Navigation. Sie müssen entfernt werden, und die Zeile könnte trotzdem ein Titel statt ein Header sein.

Heuristik 1: Titelzeilen erkennen

Eine Titelzeile hat typischerweise:

  • Eine einzelne Zelle (oder sehr wenige Zellen)
  • Großes colspan, das die meisten/alle Spalten überspannt
  • Textinhalt, der wie ein Titel aussieht, nicht wie Spaltennamen
function isTitleRow(row, totalColumns) {
  if (!row || row.length === 0) return false;

  // Nicht-leere Zellen zählen
  const nonEmptyCells = row.filter(cell => cell && cell.trim()).length;

  // Titelzeilen haben meist 1-2 nicht-leere Zellen
  if (nonEmptyCells > 2) return false;

  // Prüfen ob erste Zelle die meisten Spalten überspannt
  // In einer normalisierten Matrix zeigt sich das als wiederholte Werte
  const firstValue = row[0];
  const repeatedCount = row.filter(cell => cell === firstValue).length;

  // Wenn der erste Wert >50% der Spalten wiederholt, ist es wahrscheinlich ein Colspan-Titel
  if (repeatedCount > totalColumns * 0.5) {
    return true;
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

Heuristik 2: Was eine Zeile „wie Header aussehen" lässt

Header-Zeilen haben Eigenschaften, die sie von Datenzeilen unterscheiden:

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

  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)) {
      // Reine Zahl oder Prozent
      numericCells++;
    } else {
      textCells++;
    }
  }

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

  // Header sind überwiegend Text, nicht Zahlen
  // Wenn >70% der nicht-leeren Zellen numerisch sind, sind es wahrscheinlich Daten
  if (numericCells / totalNonEmpty > 0.7) {
    return false;
  }

  // Header sollten nicht überwiegend leer sein
  if (emptyCells / row.length > 0.7) {
    return false;
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Heuristik 3: Was eine Zeile „wie Daten aussehen" lässt

Die Gegenprüfung hilft zu bestätigen, dass man die richtige Grenze gefunden hat:

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

    // Auf numerische Muster prüfen
    if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
      numericCells++;
    }

    // Auf Datumsmuster prüfen
    if (/^\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4}$/.test(value)) {
      dateCells++;
    }
  }

  if (totalNonEmpty === 0) return false;

  // Datenzeilen haben typischerweise numerischen oder Datumsinhalt
  const dataLikeCells = numericCells + dateCells;
  return dataLikeCells / totalNonEmpty > 0.3;
}
Enter fullscreen mode Exit fullscreen mode

Heuristik 4: Gruppierte Spalten-Header erkennen

FBREF-artige Tabellen haben eine Gruppen-Header-Zeile gefolgt von einer Sub-Header-Zeile. Die Gruppenzeile hat:

  • Leere Zellen am Anfang (Spalten ohne Gruppen)
  • Wiederholte Werte durch Colspan-Expansion
  • Mehrere eindeutige nicht-leere Werte (nicht nur einen wie bei einem Titel)
function detectGroupHeaderRow(row, nextRow) {
  if (!row || !nextRow || row.length < 4) return false;

  // Gruppen-Header-Zeilen MÜSSEN leere Zellen am Anfang haben
  const firstCellEmpty = !(row[0] || "").trim();
  if (!firstCellEmpty) return false;

  // Eindeutige nicht-leere Werte zählen
  const uniqueValues = new Set(
    row.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
  );

  // Eine Titelzeile hat genau EINEN eindeutigen Wert
  // Eine Gruppen-Header-Zeile muss MEHRERE eindeutige Werte haben
  if (uniqueValues.size <= 1) return false;

  // Aufeinanderfolgende Wiederholungen zählen (zeigt Colspan-Expansion an)
  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);

  // Hohes Wiederholungsverhältnis (>30%) deutet auf Colspan-Expansion hin
  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

Heuristik 5: Wikipedia-Navigationspräfixe bereinigen

Wikipedia-Templates stellen Inhalten oft „v t e" voran (Links zu Anzeigen/Diskussion/Bearbeiten):

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

  // Muster 1: "v t e " am Anfang (leerzeichengetrennt)
  // Muster 2: "v | t | e " (pipe-getrennt)
  // Muster 3: "[v] [t] [e] " (klammergetrennt)

  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

Alles zusammensetzen: Der Erkennungsalgorithmus

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

    // Titelzeilen überspringen
    if (isTitleRow(currentRow, totalColumns)) {
      continue;
    }

    // Auf gruppierte Header prüfen (zweistufig)
    if (detectGroupHeaderRow(currentRow, nextRow)) {
      // Die Sub-Header-Zeile (i+1) ist der tatsächliche Header
      return i + 1;
    }

    // Prüfen ob diese Zeile wie Header aussieht und die nächste wie Daten
    if (rowLooksLikeHeaders(currentRow) && rowLooksLikeData(nextRow)) {
      return i;
    }
  }

  // Fallback: Zeile 0 als Header annehmen
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Praxistests

Diese Heuristiken wurden durch Tests gegen folgende Tabellen entwickelt:

  • Wikipedia Länder-/Bevölkerungstabellen (Titelzeilen + „v t e"-Präfixe)
  • FBREF-Spielerstatistiken (gruppierte Header)
  • Finanztabellen mit Einheitenzeilen
  • Behördentabellen mit mehreren Header-Ebenen

Keine Heuristik ist perfekt. Das Ziel ist, die gängigen Muster korrekt zu behandeln und bei ungewöhnlichen Tabellen sauber zu scheitern.

Wenn die Erkennung versagt

Für Tabellen, die nicht in gängige Muster passen, eine manuelle Überschreibung bereitstellen:

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

Nutzer, die ihre Daten kennen, können die Header-Zeile explizit angeben.

Zusammenfassung

Muster Erkennungsmethode
Titelzeile Einzelne Zelle mit großem colspan
Standard-Header Zeile mit überwiegend Text, gefolgt von Zeile mit Zahlen
Gruppierte Header Leere erste Zellen + wiederholte Werte + mehr eindeutige Werte in der nächsten Zeile
Wikipedia-Nav „v t e"-Präfixmuster

Die zentrale Erkenntnis: Header und Daten haben unterschiedliche Eigenschaften. Header sind textlastig mit beschreibenden Labels. Daten sind zahlenlastig mit tatsächlichen Werten. Die Grenze zwischen ihnen ist normalerweise erkennbar.

Für mehr zu Wikipedias spezifischen Tabellenherausforderungen siehe unseren Leitfaden zur besten Chrome-Erweiterung zum Kopieren von Tabellen nach Excel.


Automatische Header-Erkennung ohne Code? Erfahren Sie mehr auf gauchogrid.com/de/html-table-exporter oder probieren Sie es kostenlos im Chrome Web Store.

Top comments (0)