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>
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>
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>
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;
}
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;
}
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;
}
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;
}
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();
}
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;
}
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 };
}
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)