De eerste rij van een HTML-tabel is de headerrij.
Behalve wanneer dat niet zo is.
Wikipedia-tabellen hebben vaak een titelrij die alle kolommen overspant vóór de eigenlijke headers. Sportstatistiekensites hebben gegroepeerde headers waar "Speeltijd" meerdere subkolommen overspant zoals "WS", "Basis", "Min". Financiële tabellen hebben eenheidrijen ("in miljoenen EUR") die op headers lijken maar dat niet zijn.
Als je aanneemt dat rij 0 altijd de header is, zullen je exports kapot zijn voor een aanzienlijk deel van de real-world tabellen.
Zo detecteer je de eigenlijke headerrij programmatisch.
Het Probleem: Drie Soorten "Eerste Rijen"
Bekijk deze veelvoorkomende patronen:
Patroon 1: Titelrij
<table>
<tr>
<th colspan="4">Wereldbevolking per Land</th> <!-- Titel, geen header -->
</tr>
<tr>
<th>Rang</th>
<th>Land</th>
<th>Bevolking</th>
<th>% van Wereld</th>
</tr>
<tr>
<td>1</td>
<td>India</td>
<td>1.428.627.663</td>
<td>17,85%</td>
</tr>
</table>
Rij 0 is een titel. Rij 1 is de header. Rij 2+ is data.
Patroon 2: Gegroepeerde Headers (Twee Niveaus)
<table>
<tr>
<th></th>
<th></th>
<th colspan="3">Speeltijd</th>
<th colspan="2">Prestatie</th>
</tr>
<tr>
<th>Speler</th>
<th>Land</th>
<th>WS</th>
<th>Basis</th>
<th>Min</th>
<th>Dls</th>
<th>Ass</th>
</tr>
<tr>
<td>Jan de Vries</td>
<td>NED</td>
<td>34</td>
<td>30</td>
<td>2700</td>
<td>12</td>
<td>8</td>
</tr>
</table>
Rij 0 is groepheaders. Rij 1 is de eigenlijke kolomheaders. Rij 2+ is data.
Patroon 3: Wikipedia-Navigatieprefix
<tr>
<th colspan="3">v t e Werelderfgoedlijst</th>
</tr>
De "v t e" (bekijk/overleg/bewerk) links zijn Wikipedia's sjabloonnavigatie. Ze moeten verwijderd worden, en de rij kan nog steeds een titel zijn in plaats van een header.
Heuristiek 1: Titelrijen Detecteren
Een titelrij heeft doorgaans:
- Een enkele cel (of heel weinig cellen)
- Grote colspan die de meeste/alle kolommen overspant
- Tekstinhoud die op een titel lijkt, niet op kolomnamen
function isTitleRow(row, totalColumns) {
if (!row || row.length === 0) return false;
// Tel niet-lege cellen
const nonEmptyCells = row.filter(cell => cell && cell.trim()).length;
// Titelrijen hebben meestal 1-2 niet-lege cellen
if (nonEmptyCells > 2) return false;
// Controleer of de eerste cel de meeste kolommen overspant (duidt op colspan)
// In een genormaliseerde matrix toont dit zich als herhaalde waarden
const firstValue = row[0];
const repeatedCount = row.filter(cell => cell === firstValue).length;
// Als de eerste waarde >50% van de kolommen herhaalt, is het waarschijnlijk een colspan-titel
if (repeatedCount > totalColumns * 0.5) {
return true;
}
return false;
}
Heuristiek 2: Wat Maakt dat een Rij "Op Headers Lijkt"
Headerrijen hebben kenmerken die ze onderscheiden van datarijen:
function rowLooksLikeHeaders(row) {
if (!row || row.length === 0) return false;
// Als gedomineerd door pure getallen, zijn het geen headers
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)) {
// Puur getal of percentage
numericCells++;
} else {
textCells++;
}
}
const totalNonEmpty = numericCells + textCells;
if (totalNonEmpty === 0) return false;
// Headers zijn voornamelijk tekst, geen getallen
// Als >70% van niet-lege cellen numeriek is, is het waarschijnlijk data
if (numericCells / totalNonEmpty > 0.7) {
return false;
}
// Headers mogen niet grotendeels leeg zijn
if (emptyCells / row.length > 0.7) {
return false;
}
return true;
}
Heuristiek 3: Wat Maakt dat een Rij "Op Data Lijkt"
De omgekeerde controle helpt bevestigen dat we de juiste grens hebben gevonden:
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++;
// Controleer op numerieke patronen
if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
numericCells++;
}
// Controleer op datumpatronen
if (/^\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4}$/.test(value)) {
dateCells++;
}
}
if (totalNonEmpty === 0) return false;
// Datarijen bevatten doorgaans numerieke of datuminhoud
const dataLikeCells = numericCells + dateCells;
return dataLikeCells / totalNonEmpty > 0.3;
}
Heuristiek 4: Gegroepeerde Kolomheaders Detecteren
FBREF-stijl tabellen hebben een groepheaderrij gevolgd door een subheaderrij. De groeprij heeft:
- Lege cellen aan het begin (kolommen zonder groepen)
- Herhaalde waarden door colspan-expansie
- Meerdere unieke niet-lege waarden (niet slechts één zoals een titel)
function detectGroupHeaderRow(row, nextRow) {
if (!row || !nextRow || row.length < 4) return false;
// Groepheaderrijen MOETEN lege cellen aan het begin hebben
// Dit onderscheidt ze van horizontaal gedupliceerde tabellen
const firstCellEmpty = !(row[0] || "").trim();
if (!firstCellEmpty) return false;
// Tel unieke niet-lege waarden
const uniqueValues = new Set(
row.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
);
// Een titelrij heeft precies ÉÉN unieke waarde
// Een groepheaderrij moet MEERDERE unieke waarden hebben
if (uniqueValues.size <= 1) return false;
// Tel opeenvolgende herhaalde waarden (duidt op colspan-expansie)
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);
// Hoge herhalingsratio (>30%) suggereert colspan-expansie
// Volgende rij moet meer unieke waarden hebben (de eigenlijke subheaders)
const nextUniqueValues = new Set(
nextRow.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
);
return repeatRatio > 0.3 && nextUniqueValues.size > uniqueValues.size;
}
Heuristiek 5: Wikipedia-Navigatieprefixen Opschonen
Wikipedia-sjablonen prefixen content vaak met "v t e" (links naar bekijken/overleg/bewerken):
function cleanWikipediaNavPrefix(text) {
if (!text) return text;
// Patroon 1: "v t e " aan het begin (spatie-gescheiden)
// Patroon 2: "v | t | e " (pipe-gescheiden)
// Patroon 3: "[v] [t] [e] " (haakje-gescheiden)
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 Samenbrengen: Het Detectie-algoritme
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];
// Sla titelrijen over
if (isTitleRow(currentRow, totalColumns)) {
continue;
}
// Controleer op gegroepeerde headers (twee niveaus)
if (detectGroupHeaderRow(currentRow, nextRow)) {
// De subheaderrij (i+1) is de eigenlijke header
return i + 1;
}
// Controleer of deze rij op headers lijkt en de volgende op data
if (rowLooksLikeHeaders(currentRow) && rowLooksLikeData(nextRow)) {
return i;
}
}
// Fallback: neem aan dat rij 0 de header is
return 0;
}
Real-world Testen
Deze heuristieken zijn ontwikkeld door te testen tegen:
- Wikipedia-landen/bevolkingstabellen (titelrijen + "v t e" prefixen)
- FBREF-spelersstatistieken (gegroepeerde headers)
- Financiële tabellen met eenheidrijen
- Overheidsdatatabel met meerdere headerniveaus
Geen heuristiek is perfect. Het doel is om de gangbare patronen correct af te handelen en netjes te falen bij ongebruikelijke tabellen.
Wanneer Detectie Faalt
Voor tabellen die niet in de gangbare patronen passen, bied een handmatige override:
function extractTable(matrix, options = {}) {
const headerRowIndex = options.headerRowIndex ?? detectHeaderRowIndex(matrix);
const headerRow = matrix[headerRowIndex];
const dataRows = matrix.slice(headerRowIndex + 1);
return { headerRow, dataRows };
}
Gebruikers die hun data kennen, kunnen de headerrij expliciet opgeven.
Samenvatting
| Patroon | Detectiemethode |
|---|---|
| Titelrij | Enkele cel met grote colspan |
| Standaard header | Rij met voornamelijk tekst, gevolgd door rij met getallen |
| Gegroepeerde headers | Lege eerste cellen + herhaalde waarden + meer unieke waarden in volgende rij |
| Wikipedia-nav | "v t e" prefix-patroon |
Het kernidee: headers en data hebben verschillende kenmerken. Headers zijn tekstrijk met beschrijvende labels. Data is getallenrijk met feitelijke waarden. De grens ertussen is meestal detecteerbaar.
Lees voor meer over de specifieke uitdagingen van Wikipedia-tabellen onze gids over HTML-tabelscraping in Chrome.
Wil je automatische headerdetectie zonder code te schrijven? Meer informatie op gauchogrid.com/nl/html-table-exporter of probeer het gratis in de Chrome Web Store.
Top comments (0)