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 |
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');
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;
}
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"
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"]
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;
}
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"
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;
}
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"]
]
};
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');
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
}
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")}`;
}
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 ...
}
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!");
})();
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.