DEV Community

Cover image for Von der HTML-Tabelle zu SQL-INSERT-Statements mit einem Klick
circobit
circobit

Posted on

Von der HTML-Tabelle zu SQL-INSERT-Statements mit einem Klick

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

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

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

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

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

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

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

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

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

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

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

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

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 ...
}
Enter fullscreen mode Exit fullscreen mode

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

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)