Sie haben Daten auf einer Website gefunden. Sie brauchen sie in Ihrer Datenbank. Der übliche Workflow: Export nach CSV, Import ins SQL-Tool, Typfehler beheben, Encoding-Probleme lösen.
Was wäre, wenn man direkt von der HTML-Tabelle zu validem SQL kommen könnte?
Diese Anleitung zeigt, wie man CREATE TABLE- und INSERT INTO-Statements aus jeder Webtabelle generiert — mit Typinferenz, Identifier-Bereinigung und korrektem Escaping. Diesen Ansatz verwende ich im HTML Table Exporter.
Das Endergebnis
Aus einer Tabelle wie dieser:
| Produktname | Preis | Auf Lager |
|-----------------|--------|-----------|
| Widget Pro | 29,99 | Ja |
| Gadget Basic | 14,50 | Nein |
Wird generiert:
-- Exportiert mit HTML Table Exporter PRO
CREATE TABLE produkte (
produktname TEXT,
preis REAL,
auf_lager TEXT
);
INSERT INTO produkte (produktname, preis, auf_lager) VALUES
('Widget Pro', 29.99, 'Ja'),
('Gadget Basic', 14.50, 'Nein');
Bauen wir das Schritt für Schritt auf.
Schritt 1: Die Tabellenmatrix extrahieren
Zunächst die Rohdaten aus der HTML-Tabelle extrahieren:
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;
}
Ausgabe: Ein 2D-Array, wobei rows[0] die Header und rows[1+] die Daten sind.
Schritt 2: Spaltennamen bereinigen
SQL-Bezeichner haben strenge Regeln. Header wie „Produktname" oder „Preis (€)" müssen bereinigt werden:
function sanitizeSqlIdentifier(header, fallbackIndex) {
let id = (header || "").toString().trim();
if (!id) {
return `col_${fallbackIndex + 1}`;
}
// Unicode normalisieren (Akzente entfernen)
id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// In Kleinbuchstaben mit Unterstrich konvertieren
id = id
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
// Sicherstellen, dass es nicht mit einer Zahl beginnt
if (/^\d/.test(id)) {
id = `col_${id}`;
}
// Fallback wenn nach Bereinigung leer
if (!id) {
return `col_${fallbackIndex + 1}`;
}
return id;
}
// Beispiele:
// "Produktname" → "produktname"
// "Preis (€)" → "preis"
// "2024 Umsatz" → "col_2024_umsatz"
// "" → "col_1"
Schritt 3: Doppelte Spaltennamen behandeln
Tabellen haben manchmal doppelte Header. SQL erfordert eindeutige Spaltennamen:
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;
});
}
// ["Name", "Name", "Wert"] → ["name", "name_1", "wert"]
Schritt 4: SQL-Typen inferieren
Daten analysieren, um passende SQL-Typen zu bestimmen:
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++) {
// Bis zu 50 Werte pro Spalte samplen
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;
}
// Prüfen ob alle Werte einem Typ entsprechen
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;
}
Typpriorität: INTEGER > REAL > TEXT
Wenn 100% der nicht-leeren Werte Integer sind, wird INTEGER verwendet. Wenn alle numerisch sind (einschließlich Dezimalzahlen), wird REAL verwendet. Ansonsten TEXT.
Schritt 5: Werte korrekt escapen
SQL-Injection ist nicht nur ein Sicherheitsproblem — es beschädigt die Daten. Korrektes Escaping:
function sqlEscapeValue(raw, type) {
// NULL-Behandlung
if (raw == null) return "NULL";
const v = String(raw).trim();
if (v === "") return "NULL";
// Numerische Typen
if (type === "INTEGER" || type === "REAL") {
// Dezimaltrennzeichen normalisieren
const normalized = v.replace(",", ".");
const num = Number(normalized);
if (!Number.isNaN(num) && Number.isFinite(num)) {
return normalized;
}
// Bei ungültiger Zahl zu TEXT zurückfallen
}
// TEXT: Einfache Anführungszeichen durch Verdopplung escapen
const escaped = v.replace(/'/g, "''");
return `'${escaped}'`;
}
// Beispiele:
// sqlEscapeValue("Hallo", "TEXT") → "'Hallo'"
// sqlEscapeValue("Das ist's", "TEXT") → "'Das ist''s'"
// sqlEscapeValue("29.99", "REAL") → "29.99"
// sqlEscapeValue("", "TEXT") → "NULL"
// sqlEscapeValue(null, "TEXT") → "NULL"
Schritt 6: Das SQL generieren
Alles zusammenfügen:
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 "";
// Spaltennamen generieren
const columnNames = makeUniqueColumnNames(headerRow);
// Typen inferieren
const types = inferSqlColumnTypes(rows, headerRowIndex);
// Tabellennamen generieren
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(", ")})`;
});
// Kombinieren
let sql = `-- Exportiert mit HTML Table Exporter\n\n${createStmt}\n\n`;
if (valueLines.length > 0) {
sql += `${insertHeader}\n${valueLines.join(",\n")};\n`;
}
return sql;
}
Vollständiges Beispiel
Eingabetabelle:
const tableInfo = {
name: "Q1 Verkaufsdaten",
rows: [
["Produkt", "Verkaufte Einheiten", "Umsatz (€)", "Profitabel"],
["Widget A", "1.234", "45.678,90", "Ja"],
["Widget B", "567", "12.345,67", "Nein"],
["Gadget X", "890", "23.456,78", "Ja"]
]
};
Ausgabe:
-- Exportiert mit HTML Table Exporter
CREATE TABLE q1_verkaufsdaten (
produkt TEXT,
verkaufte_einheiten INTEGER,
umsatz REAL,
profitabel TEXT
);
INSERT INTO q1_verkaufsdaten (produkt, verkaufte_einheiten, umsatz, profitabel) VALUES
('Widget A', 1234, 45678.90, 'Ja'),
('Widget B', 567, 12345.67, 'Nein'),
('Gadget X', 890, 23456.78, 'Ja');
Zu beachten:
- Spaltennamen bereinigt (
Umsatz (€)→umsatz) - Zahlen erkannt und ohne Anführungszeichen
- Tausendertrennzeichen in Zahlen behandelt
- Tabellenname aus Metadaten
Edge Cases behandeln
NULL-Werte
Viele Webtabellen stellen fehlende Daten unterschiedlich dar:
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 der Funktion
}
Große Datensätze
Für Tabellen mit Tausenden von Zeilen die INSERTs batchweise verarbeiten:
function tableToSqlBatched(tableInfo, batchSize = 1000) {
// ... gleiches 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")}`;
}
Datenbankspezifische Syntax
SQLite, PostgreSQL und MySQL haben leichte Syntaxunterschiede:
function tableToSql(tableInfo, dialect = "sqlite") {
// ... gleiche Spalten-/Typlogik ...
// Dialektspezifisches Typ-Mapping
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 der Funktion ...
}
Browser-Bookmarklet
Schnelle Lösung für gelegentlichen Gebrauch:
javascript:(function(){
const table = document.querySelector("table");
if (!table) { alert("Keine Tabelle gefunden"); 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 in die Zwischenablage kopiert!");
})();
Wann man das verwenden sollte
Gut geeignet für:
- Schnelle Datenimporte in SQLite/PostgreSQL
- Test-Datenbanken befüllen
- Einmalige Datenmigrationen
- SQL anhand von Beispielen lernen
Weniger geeignet für:
- Großes ETL (dafür gibt es passende Tools)
- Komplexe Schemas (Foreign Keys, Constraints)
- Binärdaten
Für produktive Datenpipelines sind dedizierte ETL-Tools mit Validierung besser. Für „Ich brauche diese Tabelle jetzt in meiner Datenbank" ist direkte SQL-Generierung schnell.
Ohne Code ausprobieren
Wenn Sie das nicht selbst bauen möchten, generiert HTML Table Exporter PRO SQL mit einem Klick. Die kostenlose Version exportiert nach CSV/JSON/Excel; PRO fügt SQL, NDJSON und Datenbereinigung hinzu.
Einen Vergleich verschiedener Export-Ansätze finden Sie in unserem Leitfaden zu den besten Chrome-Erweiterungen zum Exportieren von Tabellen.
Erfahren Sie mehr auf gauchogrid.com/de/html-table-exporter oder probieren Sie es im Chrome Web Store aus.
In welche Datenbank importieren Sie Webdaten? Mich interessieren die Anwendungsfälle.
Top comments (0)