Tabellen mit zusammengeführten Zellen sind überall. Wikipedia nutzt sie. Sportstatistik-Seiten nutzen sie. Finanzberichte nutzen sie.
Und sie machen fast jeden Tabellen-Scraper kaputt, den ich gesehen habe.
Das Problem: Wenn man durch <tr> und <td> Elemente iteriert, bekommt man das, was im HTML steht — nicht das, was visuell gerendert wird. Eine Zelle mit rowspan="3" erscheint einmal im HTML, belegt aber drei Zeilen in der Ausgabe.
Der naive Ansatz scheitert lautlos. Man bekommt verschobene Spalten, fehlende Daten und Exporte, die nichts mit der Originaltabelle zu tun haben.
So behebt man es.
Das Problem: HTML stimmt nicht mit dem Raster überein
Eine einfache Tabelle mit einer zusammengeführten Zelle:
<table>
<tr>
<td rowspan="2">A</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
</tr>
</table>
Visuelle Darstellung:
| A | B |
| | C |
Aber wenn man naiv Zellen durch Zeileniteration extrahiert:
// Falscher Ansatz
table.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('td');
console.log(cells.map(c => c.textContent));
});
// Ausgabe: ["A", "B"], ["C"]
// Erwartet: ["A", "B"], ["A", "C"]
Zeile 2 fehlt das „A", das visuell in sie hineinreicht.
Die Lösung: Eine Matrix bauen
Der korrekte Ansatz ist, ein 2D-Raster vorab zuzuweisen und es gemäß der Span-Attribute zu füllen.
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;
// Spalten überspringen, die bereits durch Rowspans belegt sind
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
// Belegte Zellen überspringen
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const text = cell.textContent.trim();
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
// Den Block füllen, den diese Zelle überspannt
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;
}
Jetzt stimmt die Ausgabe mit der visuellen Tabelle überein:
const matrix = extractTableMatrix(table);
// [["A", "B"], ["A", "C"]]
Warum das wichtig ist: Praxisbeispiele
Wikipedia-Ländertabellen
Wikipedia verwendet zusammengeführte Zellen extensiv. Eine Tabelle, die Länder nach Region auflistet, könnte so aussehen:
<tr>
<td rowspan="5">Europa</td>
<td>Frankreich</td>
</tr>
<tr>
<td>Deutschland</td>
</tr>
<!-- ... -->
Ohne korrekte Behandlung verliert man die Zuordnung „Europa" für die Zeilen 2–5.
Sportstatistik-Seiten
Seiten wie FBREF verwenden sowohl rowspan ALS AUCH colspan. Spielerstatistik-Tabellen haben:
- Gruppierte Header mit colspan (Spielzeit → SP | Starts | Min)
- Zusammenfassungszeilen mit rowspan über Spielerdetails
Ein naiver Scraper exportiert Zeichensalat. Ein matrixbasierter Ansatz bewahrt die Struktur.
Finanzberichte
Quartalsberichte haben oft:
<td colspan="4">2024</td>
<!-- Dann: Q1, Q2, Q3, Q4 -->
Der Jahres-Header muss sich auf alle vier Quartalsspalten übertragen.
Sonderfälle behandeln
Leere Zellen in der Mitte
Manchmal sind Zellen wirklich leer. Das darf nicht mit „noch nicht durch einen Span gefüllt" verwechselt werden.
// Auf gleiche Spaltenanzahl normalisieren
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;
});
Verschachtelte Tabellen
Tabellen innerhalb von Zellen erzeugen Rauschen. Meistens will man den Textinhalt der verschachtelten Tabelle, keine rekursive Extraktion.
function extractCellText(cell) {
const clone = cell.cloneNode(true);
// Verschachtelte Tabellen, Scripts, Styles entfernen
clone.querySelectorAll("table, script, style, noscript")
.forEach(el => el.remove());
return clone.textContent.replace(/\s+/g, " ").trim();
}
Colspan + Rowspan kombiniert
Der Algorithmus behandelt das natürlich — man füllt einfach ein größeres Rechteck im Raster.
<td rowspan="2" colspan="3">Große Zelle</td>
Füllt einen 2×3-Block mit demselben Wert.
Wann man ein Tool verwenden sollte
Das von Grund auf zu schreiben ist lehrreich, aber zeitaufwendig. Für den Produktionseinsatz gibt es:
- HTML Table Exporter — Browser-Erweiterung, die diese Sonderfälle automatisch behandelt
- Pandas read_html() — Funktioniert, hat aber Schwierigkeiten mit komplexen verschachtelten Tabellen
- Beautiful Soup — Erfordert manuelle Span-Behandlung
Der vollständige Algorithmus
Hier die Produktionsversion mit allen Sonderfällen:
function extractCellText(cell) {
if (!cell) return "";
const clone = cell.cloneNode(true);
// Elemente mit nicht-sichtbarem Inhalt entfernen
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;
});
});
// Alle Zeilen auf gleiche Länge normalisieren
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;
});
}
Zusammenfassung
| Ansatz | Behandelt Spans | Produktionsreif |
|---|---|---|
| Naive Iteration | ❌ | ❌ |
| Matrix mit Spans | ✅ | ✅ |
| Pandas read_html | Teilweise | Kommt drauf an |
| Browser-Erweiterung | ✅ | ✅ |
Die zentrale Erkenntnis: Tabellen als Raster denken, nicht als verschachtelte HTML-Elemente. Erst das Raster bauen, dann exportieren.
Für mehr zum Export der besonders kniffligen Wikipedia-Tabellen siehe unseren Leitfaden zur besten Chrome-Erweiterung zum Kopieren von Tabellen nach Excel.
Tabellen ohne Code extrahieren? Erfahren Sie mehr auf gauchogrid.com/de/html-table-exporter oder probieren Sie es kostenlos im Chrome Web Store.
Top comments (0)