DEV Community

Cover image for Rowspan und Colspan beim Tabellenexport: So extrahiert man zusammengeführte Zellen
circobit
circobit

Posted on

Rowspan und Colspan beim Tabellenexport: So extrahiert man zusammengeführte Zellen

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>
Enter fullscreen mode Exit fullscreen mode

Visuelle Darstellung:

| A | B |
|   | C |
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Jetzt stimmt die Ausgabe mit der visuellen Tabelle überein:

const matrix = extractTableMatrix(table);
// [["A", "B"], ["A", "C"]]
Enter fullscreen mode Exit fullscreen mode

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>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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)