ie haben eine Tabelle auf einer Website gefunden. Sie brauchen die Daten in einer Tabellenkalkulation. Der naheliegende Weg – kopieren, einfügen, in Excel bereinigen – funktioniert einmal. Aber was, wenn Sie diese Daten wöchentlich brauchen? Oder von 50 verschiedenen Seiten?
Dieser Leitfaden zeigt, wie Sie HTML-Tabellen programmatisch mit JavaScript extrahieren, die Sonderfälle behandeln, die naive Ansätze sprengen, und in Formate exportieren, die Ihre Tools tatsächlich akzeptieren.
Der naive Ansatz (und warum er scheitert)
Die einfachste Extraktion sieht so aus:
function extractTable(table) {
return Array.from(table.rows).map(row =>
Array.from(row.cells).map(cell => cell.textContent.trim())
);
}
Das funktioniert bei einfachen Tabellen. Es bricht sofort zusammen, wenn man auf Folgendes trifft:
- Rowspan/Colspan — Zellen, die mehrere Zeilen oder Spalten umspannen
- Verschachtelte Tabellen — Tabellen innerhalb von Tabellenzellen
-
Versteckte Inhalte —
<style>,<script>oderdisplay:none-Elemente - Sonderzeichen — Zeilenumbrüche, Tabs und Anführungszeichen im Zelleninhalt
Lösen wir jedes Problem einzeln.
Rowspan und Colspan verarbeiten
Wenn eine Zelle rowspan="2" hat, belegt sie Platz in der aktuellen Zeile UND der nächsten. Ein naiver Extraktor sieht weniger Zellen als erwartet und bringt Spalten durcheinander.
Die Lösung: Ein virtuelles Raster aufbauen, das belegte Positionen nachverfolgt.
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 => {
// Bereits durch vorherige Rowspans belegte Spalten überspringen
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;
// Den rechteckigen Block füllen, den diese Zelle belegt
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;
}
So wird eine Tabelle wie diese:
<table>
<tr><td rowspan="2">A</td><td>B</td></tr>
<tr><td>C</td></tr>
</table>
Korrekt zu:
[
["A", "B"],
["A", "C"] // "A" erscheint in beiden Zeilen
]
Sauberen Text extrahieren
textContent erfasst alles – einschließlich CSS-Regeln in <style>-Tags und JavaScript in <script>-Tags, die manche Seiten in Tabellenzellen injizieren.
Saubere Extraktion erfordert Filterung:
function extractCellText(cell) {
if (!cell) return "";
// Klonen, um das DOM nicht zu verändern
const clone = cell.cloneNode(true);
// Unsichtbare Elemente entfernen
const invisibleSelectors = "style, script, noscript, template, link";
clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());
// Whitespace normalisieren
return (clone.textContent || "").replace(/\s+/g, " ").trim();
}
Verschachtelte Tabellen erkennen
Wenn eine Tabelle eine andere Tabelle in einer Zelle enthält, will man normalerweise die Daten der äußeren Tabelle, nicht ein rekursives Chaos.
Die Erkennung ist unkompliziert:
function isNestedTable(table, allTables) {
let parent = table.parentElement;
while (parent) {
if (parent.tagName === "TABLE") {
return true; // Diese Tabelle ist innerhalb einer anderen Tabelle
}
parent = parent.parentElement;
}
return false;
}
// Beim Scannen einer Seite filtern
const allTables = document.querySelectorAll("table");
const topLevelTables = Array.from(allTables)
.filter(t => !isNestedTable(t, allTables));
Konvertierung zu CSV
CSV sieht einfach aus, bis man Folgendes verarbeiten muss:
- Kommas innerhalb von Werten
- Anführungszeichen innerhalb von Werten
- Zeilenumbrüche innerhalb von Werten
Der RFC-4180-konforme Ansatz:
function toCSV(rows, delimiter = ",") {
return rows.map(row =>
row.map(cell => {
if (cell == null) cell = "";
const str = String(cell);
// In Anführungszeichen setzen, wenn Trennzeichen, Anführungszeichen oder Zeilenumbrüche enthalten sind
const needsQuotes = str.includes(delimiter) || /["\r\n]/.test(str);
const escaped = str.replace(/"/g, '""');
return needsQuotes ? `"${escaped}"` : escaped;
}).join(delimiter)
).join("\r\n");
}
Das behandelt korrekt den Albtraum-Fall:
toCSV([['Sag "Hallo, Welt"', "Normal"]])
// '"Sag ""Hallo, Welt""",Normal'
Eine vollständige Anleitung zum CSV-Export finden Sie unter Die beste Chrome-Erweiterung zum Kopieren von Tabellen nach Excel.
Konvertierung zu JSON
Für den JSON-Export wird die erste Zeile zu Schlüsseln:
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}`;
// In snake_case (Kleinbuchstaben) normalisieren
return key
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // Akzente entfernen
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
}
Eingabe:
| Produktname | Preis (€) |
|-------------|-----------|
| Widget | 29,99 |
Ausgabe:
[
{
"produktname": "Widget",
"preis": "29.99"
}
]
Den Download auslösen
Im Browser-Kontext kann man einen Download ohne Server auslösen:
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);
}
// Verwendung
const csv = toCSV(extractTableMatrix(table));
downloadFile(csv, "daten.csv", "text/csv;charset=utf-8");
Alles zusammenfügen
Hier ist ein minimales Bookmarklet, das die erste Tabelle auf jeder Seite exportiert:
javascript:(function(){
const table = document.querySelector("table");
if (!table) { alert("Keine Tabelle gefunden"); 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 = "tabelle.csv";
link.click();
})();
Wann man stattdessen eine Browser-Erweiterung verwenden sollte
Dieser Code funktioniert, aber ihn über verschiedene Websites hinweg zu pflegen ist mühsam. Wenn Sie regelmäßig Tabellen extrahieren, bietet eine Browser-Erweiterung:
- Mehrere Tabellen pro Seite
- Formatauswahl (CSV, JSON, Excel)
- Datenbereinigung (Zahlennormalisierung, Null-Behandlung)
- Spaltenauswahl und -umsortierung
Genau für diesen Workflow habe ich HTML Table Exporter gebaut. Die grundlegenden Algorithmen ähneln dem hier Gezeigten, verpackt in einer benutzerfreundlichen Oberfläche.
Mehr erfahren auf gauchogrid.com/de/html-table-exporter oder kostenlos im Chrome Web Store testen.
Fragen zu Sonderfällen bei der Tabellenextraktion? Schreiben Sie einen Kommentar – ich bin wahrscheinlich schon darüber gestolpert.
Top comments (0)