Je hebt een tabel gevonden op een website. Je hebt die data nodig in een spreadsheet. De voor de hand liggende route—kopiëren, plakken, opschonen in Excel—werkt één keer. Maar wat als je deze data wekelijks nodig hebt? Of van 50 verschillende pagina's?
Deze gids laat je zien hoe je HTML-tabellen programmatisch extraheert met JavaScript, de edge cases afhandelt die naïeve benaderingen breken, en exporteert naar formaten die je tools daadwerkelijk accepteren.
De Naïeve Benadering (En Waarom Die Faalt)
De eenvoudigste extractie ziet er zo uit:
function extractTable(table) {
return Array.from(table.rows).map(row =>
Array.from(row.cells).map(cell => cell.textContent.trim())
);
}
Dit werkt voor simpele tabellen. Het breekt direct als je het volgende tegenkomt:
- Rowspan/colspan — Cellen die meerdere rijen of kolommen overspannen
- Geneste tabellen — Tabellen in tabelcellen
-
Verborgen content —
<style>,<script>, ofdisplay:noneelementen - Speciale tekens — Regelovergangen, tabs en aanhalingstekens in celcontent
Laten we elk probleem oplossen.
Rowspan en Colspan Afhandelen
Wanneer een cel rowspan="2" heeft, bezet deze ruimte in de huidige rij EN de volgende rij. Een naïeve extractor ziet minder cellen dan verwacht en lijnt kolommen verkeerd uit.
De oplossing: bouw een virtueel raster dat bezette posities bijhoudt.
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
let colIndex = 0;
Array.from(rowEl.cells).forEach(cell => {
// Kolommen overslaan die al bezet zijn door eerdere rowspans
while (grid[rowIndex][colIndex] !== undefined) {
colIndex++;
}
const text = cell.textContent.trim();
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
// Het rechthoekige blok vullen dat deze cel bezet
for (let r = 0; r < rowSpan; r++) {
const targetRow = rowIndex + r;
if (!grid[targetRow]) grid[targetRow] = [];
for (let c = 0; c < colSpan; c++) {
const targetCol = colIndex + c;
if (grid[targetRow][targetCol] === undefined) {
grid[targetRow][targetCol] = text;
}
}
}
colIndex += colSpan;
});
});
return grid;
}
Nu wordt een tabel als deze:
<table>
<tr><td rowspan="2">A</td><td>B</td></tr>
<tr><td>C</td></tr>
</table>
Correct omgezet naar:
[
["A", "B"],
["A", "C"] // "A" verschijnt in beide rijen
]
Schone Tekst Extraheren
textContent pakt alles—inclusief CSS-regels in <style>-tags en JavaScript in <script>-tags die sommige pagina's in tabelcellen injecteren.
Schone extractie vereist filtering:
function extractCellText(cell) {
if (!cell) return "";
// Klonen om de DOM niet aan te passen
const clone = cell.cloneNode(true);
// Onzichtbare elementen verwijderen
const invisibleSelectors = "style, script, noscript, template, link";
clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());
// Witruimte normaliseren
return (clone.textContent || "").replace(/\s+/g, " ").trim();
}
Geneste Tabellen Detecteren
Wanneer een tabel een andere tabel in een cel bevat, wil je doorgaans de data van de buitenste tabel, niet een recursieve puinhoop.
Detectie is eenvoudig:
function isNestedTable(table, allTables) {
let parent = table.parentElement;
while (parent) {
if (parent.tagName === "TABLE") {
return true; // Deze tabel zit in een andere tabel
}
parent = parent.parentElement;
}
return false;
}
// Filteren bij het scannen van een pagina
const allTables = document.querySelectorAll("table");
const topLevelTables = Array.from(allTables)
.filter(t => !isNestedTable(t, allTables));
Converteren naar CSV
CSV lijkt simpel totdat je het volgende moet afhandelen:
- Komma's in waarden
- Aanhalingstekens in waarden
- Regelovergangen in waarden
De RFC 4180-conforme aanpak:
function toCSV(rows, delimiter = ",") {
return rows.map(row =>
row.map(cell => {
if (cell == null) cell = "";
const str = String(cell);
// Quoten als het scheidingsteken, aanhalingstekens of regelovergangen bevat
const needsQuotes = str.includes(delimiter) || /["\r\n]/.test(str);
const escaped = str.replace(/"/g, '""');
return needsQuotes ? `"${escaped}"` : escaped;
}).join(delimiter)
).join("\r\n");
}
Dit handelt het nachtmerriescenario correct af:
toCSV([['Zeg "Hallo, Wereld"', "Normaal"]])
// '"Zeg ""Hallo, Wereld""",Normaal'
Zie voor een complete gids over CSV-exports HTML-Tabellen Exporteren naar CSV in Chrome.
Converteren naar JSON
Voor JSON-export worden de eerste rij de sleutels:
function toJSON(rows) {
if (rows.length < 2) return "[]";
const headers = rows[0].map((h, i) => sanitizeKey(h, i));
const dataRows = rows.slice(1);
const objects = dataRows.map(row => {
const obj = {};
headers.forEach((key, i) => {
obj[key] = row[i] ?? "";
});
return obj;
});
return JSON.stringify(objects, null, 2);
}
function sanitizeKey(header, index) {
let key = (header || "").toString().trim();
if (!key) return `col_${index + 1}`;
// Normaliseren naar lowercase snake_case
return key
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // Accenten verwijderen
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
}
Invoer:
| Productnaam | Prijs (€) |
|-------------|-----------|
| Widget | 29,99 |
Uitvoer:
[
{
"productnaam": "Widget",
"prijs": "29,99"
}
]
De Download Triggeren
In een browsercontext kun je een download triggeren zonder server:
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
// Gebruik
const csv = toCSV(extractTableMatrix(table));
downloadFile(csv, "data.csv", "text/csv;charset=utf-8");
Alles Samenvoegen
Hier is een minimale bookmarklet die de eerste tabel op elke pagina exporteert:
javascript:(function(){
const table = document.querySelector("table");
if (!table) { alert("Geen tabel gevonden"); return; }
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, ri) => {
if (!grid[ri]) grid[ri] = [];
let ci = 0;
Array.from(rowEl.cells).forEach(cell => {
while (grid[ri][ci] !== undefined) ci++;
const text = cell.textContent.trim();
const rs = parseInt(cell.rowSpan) || 1;
const cs = parseInt(cell.colSpan) || 1;
for (let r = 0; r < rs; r++) {
if (!grid[ri+r]) grid[ri+r] = [];
for (let c = 0; c < cs; c++) {
if (grid[ri+r][ci+c] === undefined) grid[ri+r][ci+c] = text;
}
}
ci += cs;
});
});
return grid;
}
const data = extractTableMatrix(table);
const csv = data.map(row =>
row.map(c => c.includes(",") ? `"${c}"` : c).join(",")
).join("\n");
const blob = new Blob([csv], {type: "text/csv"});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "tabel.csv";
link.click();
})();
Wanneer een Browserextensie te Gebruiken
Deze code werkt, maar het onderhouden ervan voor verschillende sites is vervelend. Als je regelmatig tabellen extraheert, handelt een browserextensie het volgende af:
- Meerdere tabellen per pagina
- Formaatselectie (CSV, JSON, Excel)
- Datacleaning (getalnormalisatie, null-afhandeling)
- Kolomselectie en -herordening
Ik heb HTML Table Exporter gebouwd voor precies deze workflow. De kernalgoritmen zijn vergelijkbaar met wat hier wordt getoond, verpakt in een bruikbare UI.
Meer informatie op gauchogrid.com/nl/html-table-exporter of probeer het gratis in de Chrome Web Store.
Vragen over tabelextractie edge cases? Laat een reactie achter; ik heb het waarschijnlijk al meegemaakt.
Top comments (0)