DEV Community

Cover image for Webtabel-Exports Automatiseren met JavaScript: Een Praktische Gids
circobit
circobit

Posted on

Webtabel-Exports Automatiseren met JavaScript: Een Praktische Gids

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

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>, of display:none elementen
  • 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;
}
Enter fullscreen mode Exit fullscreen mode

Nu wordt een tabel als deze:

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

Correct omgezet naar:

[
  ["A", "B"],
  ["A", "C"]  // "A" verschijnt in beide rijen
]
Enter fullscreen mode Exit fullscreen mode

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

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

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

Dit handelt het nachtmerriescenario correct af:

toCSV([['Zeg "Hallo, Wereld"', "Normaal"]])
// '"Zeg ""Hallo, Wereld""",Normaal'
Enter fullscreen mode Exit fullscreen mode

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

Invoer:

| Productnaam | Prijs (€) |
|-------------|-----------|
| Widget      | 29,99     |
Enter fullscreen mode Exit fullscreen mode

Uitvoer:

[
  {
    "productnaam": "Widget",
    "prijs": "29,99"
  }
]
Enter fullscreen mode Exit fullscreen mode

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

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

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)