DEV Community

Cover image for Web-Tabellen mit JavaScript automatisiert exportieren – Ein Praxis-Leitfaden
circobit
circobit

Posted on

Web-Tabellen mit JavaScript automatisiert exportieren – Ein Praxis-Leitfaden

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

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

So wird eine Tabelle wie diese:

<table>
  <tr><td rowspan="2">A</td><td>B</td></tr>
  <tr><td>C</td></tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Korrekt zu:

[
  ["A", "B"],
  ["A", "C"]  // "A" erscheint in beiden Zeilen
]
Enter fullscreen mode Exit fullscreen mode

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

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

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

Das behandelt korrekt den Albtraum-Fall:

toCSV([['Sag "Hallo, Welt"', "Normal"]])
// '"Sag ""Hallo, Welt""",Normal'
Enter fullscreen mode Exit fullscreen mode

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

Eingabe:

| Produktname | Preis (€) |
|-------------|-----------|
| Widget      | 29,99     |
Enter fullscreen mode Exit fullscreen mode

Ausgabe:

[
  {
    "produktname": "Widget",
    "preis": "29.99"
  }
]
Enter fullscreen mode Exit fullscreen mode

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

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

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)