Van HTML-tabel naar SQL INSERT Statements in Eén Klik

javascript dev.to

Je hebt data op een website gevonden. Je hebt het in je database nodig. De workflow is meestal: exporteren naar CSV, importeren in SQL-tool, omgaan met type-mismatches, encoding-problemen oplossen.

Wat als je rechtstreeks van HTML-tabel naar geldige SQL kunt gaan?

Deze gids laat zien hoe je CREATE TABLE- en INSERT INTO-statements genereert uit elke webtabel, met type-inferentie, identifier-sanitisatie en correcte escaping. Dit is de aanpak die ik gebruik in HTML Table Exporter.

Het Eindresultaat

Van een tabel als deze:

| Productnaam     | Prijs  | Op Voorraad |
|-----------------|--------|-------------|
| Widget Pro      | 29,99  | Ja          |
| Gadget Basis    | 14,50  | Nee         |
Enter fullscreen mode Exit fullscreen mode

Genereer:

-- Geëxporteerd met HTML Table Exporter PRO

CREATE TABLE products (
  productnaam TEXT,
  prijs REAL,
  op_voorraad TEXT
);

INSERT INTO products (productnaam, prijs, op_voorraad) VALUES
  ('Widget Pro', 29.99, 'Ja'),
  ('Gadget Basis', 14.50, 'Nee');
Enter fullscreen mode Exit fullscreen mode

Laten we dit stap voor stap opbouwen.

Stap 1: De Tabelmatrix Extraheren

Eerst de ruwe data uit de HTML-tabel halen:

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 => {
      while (grid[rowIndex][colIndex] !== undefined) colIndex++;

      const text = cell.textContent.trim();
      const rowSpan = parseInt(cell.rowSpan) || 1;
      const colSpan = parseInt(cell.colSpan) || 1;

      for (let r = 0; r < rowSpan; r++) {
        if (!grid[rowIndex + r]) grid[rowIndex + r] = [];
        for (let c = 0; c < colSpan; c++) {
          grid[rowIndex + r][colIndex + c] = text;
        }
      }
      colIndex += colSpan;
    });
  });

  return grid;
}
Enter fullscreen mode Exit fullscreen mode

Output: Een 2D-array waarin rows[0] headers zijn en rows[1+] data.

Stap 2: Kolomnamen Sanitiseren

SQL-identifiers hebben strikte regels. Headers als "Productnaam" of "Prijs (€)" moeten gesanitiseerd worden:

function sanitizeSqlIdentifier(header, fallbackIndex) {
  let id = (header || "").toString().trim();

  if (!id) {
    return `col_${fallbackIndex + 1}`;
  }

  // Normaliseer unicode (verwijder accenten)
  id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");

  // Converteer naar lowercase snake_case
  id = id
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");

  // Zorg dat het niet met een cijfer begint
  if (/^\d/.test(id)) {
    id = `col_${id}`;
  }

  // Fallback als leeg na sanitisatie
  if (!id) {
    return `col_${fallbackIndex + 1}`;
  }

  return id;
}

// Voorbeelden:
// "Productnaam"    → "productnaam"
// "Prijs (€)"      → "prijs"
// "2024 Omzet"     → "col_2024_omzet"
// ""               → "col_1"
Enter fullscreen mode Exit fullscreen mode

Stap 3: Dubbele Kolomnamen Afhandelen

Tabellen hebben soms dubbele headers. SQL vereist unieke kolomnamen:

function makeUniqueColumnNames(headers) {
  const used = new Set();

  return headers.map((header, idx) => {
    let name = sanitizeSqlIdentifier(header, idx);
    let candidate = name;
    let counter = 1;

    while (used.has(candidate)) {
      candidate = `${name}_${counter}`;
      counter++;
    }

    used.add(candidate);
    return candidate;
  });
}

// ["Naam", "Naam", "Waarde"] → ["naam", "naam_1", "waarde"]
Enter fullscreen mode Exit fullscreen mode

Stap 4: SQL-types Afleiden

Analyseer data om de juiste SQL-types te bepalen:

function inferSqlColumnTypes(rows, headerRowIndex = 0) {
  const headerRow = rows[headerRowIndex] || [];
  const dataRows = rows.slice(headerRowIndex + 1);
  const types = new Array(headerRow.length).fill("TEXT");

  for (let col = 0; col < headerRow.length; col++) {
    // Sample maximaal 50 waarden per kolom
    const values = [];
    for (let r = 0; r < Math.min(dataRows.length, 50); r++) {
      const cell = dataRows[r][col];
      const v = cell != null ? String(cell).trim() : "";
      if (v !== "") values.push(v);
    }

    if (values.length === 0) {
      types[col] = "TEXT";
      continue;
    }

    // Controleer of alle waarden overeenkomen met een type
    let allInt = true;
    let allNumeric = true;

    for (const v of values) {
      if (!/^[-+]?\d+$/.test(v)) {
        allInt = false;
      }
      if (!/^[-+]?\d+([.,]\d+)?$/.test(v)) {
        allNumeric = false;
      }
    }

    if (allInt) {
      types[col] = "INTEGER";
    } else if (allNumeric) {
      types[col] = "REAL";
    } else {
      types[col] = "TEXT";
    }
  }

  return types;
}
Enter fullscreen mode Exit fullscreen mode

Typeprioriteit: INTEGER > REAL > TEXT

Als 100% van de niet-lege waarden integers zijn, gebruik INTEGER. Als ze allemaal numeriek zijn (inclusief decimalen), gebruik REAL. Anders TEXT.

Stap 5: Waarden Correct Escapen

SQL-injectie is niet alleen een beveiligingsprobleem — het beschadigt je data. Correcte escaping:

function sqlEscapeValue(raw, type) {
  // NULL-afhandeling
  if (raw == null) return "NULL";

  const v = String(raw).trim();
  if (v === "") return "NULL";

  // Numerieke types
  if (type === "INTEGER" || type === "REAL") {
    // Normaliseer decimaalscheidingsteken
    const normalized = v.replace(",", ".");
    const num = Number(normalized);

    if (!Number.isNaN(num) && Number.isFinite(num)) {
      return normalized;
    }
    // Val terug naar TEXT als het geen geldig getal is
  }

  // TEXT: escape enkele aanhalingstekens door ze te verdubbelen
  const escaped = v.replace(/'/g, "''");
  return `'${escaped}'`;
}

// Voorbeelden:
// sqlEscapeValue("Hallo", "TEXT")          → "'Hallo'"
// sqlEscapeValue("Het is goed", "TEXT")    → "'Het is goed'"
// sqlEscapeValue("29.99", "REAL")          → "29.99"
// sqlEscapeValue("", "TEXT")               → "NULL"
// sqlEscapeValue(null, "TEXT")             → "NULL"
Enter fullscreen mode Exit fullscreen mode

Stap 6: De SQL Genereren

Alles samenvoegen:

function tableToSql(tableInfo) {
  const rows = tableInfo.rows || [];
  if (rows.length < 2) return "";

  const headerRowIndex = tableInfo.headerRowIndex || 0;
  const headerRow = rows[headerRowIndex];
  const dataRows = rows.slice(headerRowIndex + 1);

  if (!headerRow || headerRow.length === 0) return "";

  // Genereer kolomnamen
  const columnNames = makeUniqueColumnNames(headerRow);

  // Leid types af
  const types = inferSqlColumnTypes(rows, headerRowIndex);

  // Genereer tabelnaam
  const rawTableName = tableInfo.name || tableInfo.slug || "table";
  const tableName = sanitizeSqlIdentifier(rawTableName, 0) || "table_export";

  // CREATE TABLE statement
  const createLines = columnNames.map((col, i) => 
    `  ${col}${types[i] || "TEXT"}`
  );
  const createStmt = `CREATE TABLE ${tableName} (\n${createLines.join(",\n")}\n);`;

  // INSERT statements
  const insertHeader = `INSERT INTO ${tableName} (${columnNames.join(", ")}) VALUES`;

  const valueLines = dataRows.map(row => {
    const values = columnNames.map((_, i) => {
      const cell = row[i];
      const type = types[i] || "TEXT";
      return sqlEscapeValue(cell, type);
    });
    return `  (${values.join(", ")})`;
  });

  // Combineer
  let sql = `-- Geëxporteerd met HTML Table Exporter\n\n${createStmt}\n\n`;

  if (valueLines.length > 0) {
    sql += `${insertHeader}\n${valueLines.join(",\n")};\n`;
  }

  return sql;
}
Enter fullscreen mode Exit fullscreen mode

Volledig Voorbeeld

Invoertabel:

const tableInfo = {
  name: "Q1 Verkoopdata",
  rows: [
    ["Product", "Eenheden Verkocht", "Omzet (€)", "Winstgevend"],
    ["Widget A", "1.234", "45.678,90", "Ja"],
    ["Widget B", "567", "12.345,67", "Nee"],
    ["Gadget X", "890", "23.456,78", "Ja"]
  ]
};
Enter fullscreen mode Exit fullscreen mode

Output:

-- Geëxporteerd met HTML Table Exporter

CREATE TABLE q1_verkoopdata (
  product TEXT,
  eenheden_verkocht INTEGER,
  omzet REAL,
  winstgevend TEXT
);

INSERT INTO q1_verkoopdata (product, eenheden_verkocht, omzet, winstgevend) VALUES
  ('Widget A', 1234, 45678.90, 'Ja'),
  ('Widget B', 567, 12345.67, 'Nee'),
  ('Gadget X', 890, 23456.78, 'Ja');
Enter fullscreen mode Exit fullscreen mode

Let op:

  • Kolomnamen gesanitiseerd (Omzet (€)omzet)
  • Getallen gedetecteerd en zonder aanhalingstekens
  • Komma's in getallen afgehandeld
  • Tabelnaam uit metadata

Randgevallen Afhandelen

NULL-waarden

Veel webtabellen representeren ontbrekende data anders:

const NULL_PATTERNS = ["N/A", "n/a", "-", "--", "null", "none", "."];

function sqlEscapeValue(raw, type, nullPatterns = NULL_PATTERNS) {
  if (raw == null) return "NULL";

  const v = String(raw).trim();
  if (v === "" || nullPatterns.includes(v.toLowerCase())) {
    return "NULL";
  }

  // ... rest van de functie
}
Enter fullscreen mode Exit fullscreen mode

Grote Datasets

Voor tabellen met duizenden rijen, batch de INSERTs:

function tableToSqlBatched(tableInfo, batchSize = 1000) {
  // ... zelfde setup ...

  const batches = [];
  for (let i = 0; i < valueLines.length; i += batchSize) {
    const batch = valueLines.slice(i, i + batchSize);
    batches.push(`${insertHeader}\n${batch.join(",\n")};`);
  }

  return `${createStmt}\n\n${batches.join("\n\n")}`;
}
Enter fullscreen mode Exit fullscreen mode

Database-specifieke Syntax

SQLite, PostgreSQL en MySQL hebben kleine syntaxverschillen:

function tableToSql(tableInfo, dialect = "sqlite") {
  // ... zelfde kolom/type-logica ...

  // Dialect-specifieke typemapping
  const typeMap = {
    sqlite: { INTEGER: "INTEGER", REAL: "REAL", TEXT: "TEXT" },
    postgresql: { INTEGER: "INTEGER", REAL: "NUMERIC", TEXT: "TEXT" },
    mysql: { INTEGER: "INT", REAL: "DECIMAL(10,2)", TEXT: "VARCHAR(255)" }
  };

  const dialectTypes = typeMap[dialect] || typeMap.sqlite;

  const createLines = columnNames.map((col, i) => 
    `  ${col}${dialectTypes[types[i]] || dialectTypes.TEXT}`
  );

  // ... rest van de functie ...
}
Enter fullscreen mode Exit fullscreen mode

Browser-bookmarklet

Snelle oplossing voor incidenteel gebruik:

javascript:(function(){
  const table = document.querySelector("table");
  if (!table) { alert("Geen tabel gevonden"); return; }

  const rows = Array.from(table.rows).map(r => 
    Array.from(r.cells).map(c => c.textContent.trim())
  );

  const headers = rows[0].map((h, i) => 
    (h || `col_${i+1}`).toLowerCase().replace(/[^a-z0-9]+/g, "_")
  );

  const values = rows.slice(1).map(row => 
    "(" + row.map(v => `'${v.replace(/'/g, "''")}'`).join(", ") + ")"
  ).join(",\n");

  const sql = `INSERT INTO table_data (${headers.join(", ")}) VALUES\n${values};`;

  navigator.clipboard.writeText(sql);
  alert("SQL gekopieerd naar klembord!");
})();
Enter fullscreen mode Exit fullscreen mode

Wanneer Gebruik Je Dit

Geschikt voor:

  • Snelle data-imports in SQLite/PostgreSQL
  • Testdatabases vullen
  • Eenmalige datamigraties
  • SQL leren met voorbeelden

Niet ideaal voor:

  • Grootschalige ETL (gebruik daarvoor echte tools)
  • Complexe schema's (foreign keys, constraints)
  • Binaire data

Voor productie-datapipelines zijn goede ETL-tools met validatie beter. Voor "ik heb deze tabel nu in mijn database nodig" is directe SQL-generatie snel.

Probeer Het Zonder Code

Als je dit niet zelf wilt bouwen, genereert HTML Table Exporter PRO SQL met één klik. De gratis versie exporteert naar CSV/JSON/Excel; PRO voegt SQL, NDJSON en dataopschoning toe.

Bekijk de vergelijking van verschillende exportmethodes in onze gids over HTML-tabelscraper extensies voor Chrome.

Meer informatie op gauchogrid.com/nl/html-table-exporter of probeer het in de Chrome Web Store.


Naar welke database importeer jij webdata? Ik ben benieuwd naar de use cases.

Source: dev.to

arrow_back Back to Tutorials