Tabellen met samengevoegde cellen zijn overal. Wikipedia gebruikt ze. Sportsitesstatistieken gebruiken ze. Financiële rapporten gebruiken ze.
En ze breken vrijwel elke tabelscraper die ik heb gezien.
Het probleem: wanneer je door <tr>- en <td>-elementen itereert, krijg je wat er in de HTML staat—niet wat er visueel getoond wordt. Een cel met rowspan="3" verschijnt één keer in de HTML maar beslaat drie rijen in de output.
De naïeve aanpak faalt stilletjes. Je krijgt verkeerd uitgelijnde kolommen, ontbrekende data en exports die er totaal niet uitzien als de originele tabel.
Zo los je het op.
Het Probleem: HTML Komt Niet Overeen met het Raster
Bekijk deze eenvoudige tabel met een samengevoegde cel:
<table>
<tr>
<td rowspan="2">A</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
</tr>
</table>
Visuele weergave:
| A | B |
| | C |
Maar als je naïef cellen extraheert door rijen te doorlopen:
// Verkeerde aanpak
table.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('td');
console.log(cells.map(c => c.textContent));
});
// Output: ["A", "B"], ["C"]
// Verwacht: ["A", "B"], ["A", "C"]
Rij 2 mist de "A" die er visueel overheen spant.
De Oplossing: Bouw een Matrix
De juiste aanpak is om een 2D-raster vooraf te vullen volgens de span-attributen.
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
const gridRow = grid[rowIndex];
let colIndex = 0;
// Sla kolommen over die al bezet zijn door rowspans van eerdere rijen
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
// Ga voorbij bezette cellen
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const text = cell.textContent.trim();
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
// Vul het blok dat deze cel beslaat
for (let r = 0; r < rowSpan; r++) {
const targetRowIndex = rowIndex + r;
if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
for (let c = 0; c < colSpan; c++) {
const targetColIndex = colIndex + c;
if (grid[targetRowIndex][targetColIndex] === undefined) {
grid[targetRowIndex][targetColIndex] = text;
}
}
}
colIndex += colSpan;
});
});
return grid;
}
Nu komt de output overeen met de visuele tabel:
const matrix = extractTableMatrix(table);
// [["A", "B"], ["A", "C"]]
Waarom Dit Ertoe Doet: Praktijkvoorbeelden
Wikipedia-landentabellen
Wikipedia maakt veelvuldig gebruik van samengevoegde cellen. Een tabel die landen per regio opsomt kan er zo uitzien:
<tr>
<td rowspan="5">Europa</td>
<td>Frankrijk</td>
</tr>
<tr>
<td>Duitsland</td>
</tr>
<!-- ... -->
Zonder juiste verwerking verlies je de "Europa"-koppeling voor rij 2-5.
Sportstatistiekensites
Sites zoals FBREF gebruiken zowel rowspan ALS colspan. Spelersstatistiekentabellen bevatten:
- Gegroepeerde headers met colspan (Speeltijd → WS | Basis | Min)
- Samenvattingsrijen met rowspan die over spelersdetails heen spannen
Een naïeve scraper exporteert onzin. Een matrixgebaseerde aanpak behoudt de structuur.
Financiële Rapporten
Kwartaalrapporten bevatten vaak:
<td colspan="4">2024</td>
<!-- Dan: K1, K2, K3, K4 -->
De jaarkop moet doorwerken naar alle vier kwartaalkolommen.
Randgevallen om te Verwerken
Lege Cellen in het Midden
Soms zijn cellen echt leeg. Verwar dit niet met "nog niet ingevuld door een span."
// Normaliseer naar hetzelfde aantal kolommen
const maxCols = grid.reduce((max, row) =>
Math.max(max, row ? row.length : 0), 0
);
const normalized = grid.map(row => {
const r = row || [];
const out = new Array(maxCols);
for (let i = 0; i < maxCols; i++) {
out[i] = r[i] != null ? r[i] : "";
}
return out;
});
Geneste Tabellen
Tabellen binnen cellen veroorzaken ruis. Meestal wil je de tekstinhoud van de geneste tabel, niet een recursieve extractie.
function extractCellText(cell) {
const clone = cell.cloneNode(true);
// Verwijder geneste tabellen, scripts, stijlen
clone.querySelectorAll("table, script, style, noscript")
.forEach(el => el.remove());
return clone.textContent.replace(/\s+/g, " ").trim();
}
Colspan + Rowspan Gecombineerd
Het algoritme verwerkt dit automatisch—je vult gewoon een grotere rechthoek in het raster.
<td rowspan="2" colspan="3">Grote Cel</td>
Vult een 2×3 blok met dezelfde waarde.
Wanneer een Tool Gebruiken
Dit helemaal zelf schrijven is leerzaam maar tijdrovend. Voor productiegebruik, overweeg:
- HTML Table Exporter — Browserextensie die deze randgevallen automatisch afhandelt
- Pandas read_html() — Werkt maar heeft moeite met complexe geneste tabellen
- Beautiful Soup — Vereist handmatige span-verwerking
Het Volledige Algoritme
Hier is de productieversie met alle randgevallen afgehandeld:
function extractCellText(cell) {
if (!cell) return "";
const clone = cell.cloneNode(true);
// Verwijder elementen die niet-zichtbare content bevatten
const invisibleSelectors = "style, script, noscript, template, link";
clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());
const text = clone.textContent || "";
return text.replace(/\s+/g, " ").trim();
}
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
const gridRow = grid[rowIndex];
let colIndex = 0;
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const text = extractCellText(cell);
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
for (let r = 0; r < rowSpan; r++) {
const targetRowIndex = rowIndex + r;
if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
const targetRow = grid[targetRowIndex];
for (let c = 0; c < colSpan; c++) {
const targetColIndex = colIndex + c;
if (targetRow[targetColIndex] === undefined) {
targetRow[targetColIndex] = text;
}
}
}
colIndex += colSpan;
});
});
// Normaliseer alle rijen naar dezelfde lengte
const maxCols = grid.reduce((max, row) =>
Math.max(max, row ? row.length : 0), 0
);
return grid.map(row => {
const r = row || [];
const out = new Array(maxCols);
for (let i = 0; i < maxCols; i++) {
out[i] = r[i] != null ? r[i] : "";
}
return out;
});
}
Samenvatting
| Aanpak | Verwerkt Spans | Productierijp |
|---|---|---|
| Naïeve iteratie | ❌ | ❌ |
| Matrix met spans | ✅ | ✅ |
| Pandas read_html | Gedeeltelijk | Hangt af |
| Browserextensie | ✅ | ✅ |
Het kernidee: denk aan tabellen als rasters, niet als geneste HTML-elementen. Bouw eerst het raster, exporteer daarna.
Lees voor meer over het exporteren van Wikipedia's bijzonder lastige tabellen onze gids over de beste Chrome-extensies voor het exporteren van tabellen.
Wil je tabellen extraheren 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)