DEV Community

Cover image for Du Tableau HTML aux Instructions SQL INSERT en Un Clic
circobit
circobit

Posted on

Du Tableau HTML aux Instructions SQL INSERT en Un Clic

Vous avez trouvé des données sur un site web. Vous en avez besoin dans votre base de données. Le workflow habituel : exporter en CSV, importer dans un outil SQL, gérer les incompatibilités de types, corriger les problèmes d'encodage.

Et si vous pouviez passer directement du tableau HTML à du SQL valide ?

Ce guide montre comment générer des instructions CREATE TABLE et INSERT INTO à partir de n'importe quel tableau web, en gérant l'inférence de types, l'assainissement des identifiants et l'échappement correct. C'est l'approche que j'utilise dans HTML Table Exporter.

Le Résultat Final

À partir d'un tableau comme celui-ci :

| Nom du Produit  | Prix   | En Stock |
|-----------------|--------|----------|
| Widget Pro      | 29.99  | Oui      |
| Gadget Basic    | 14.50  | Non      |
Enter fullscreen mode Exit fullscreen mode

Générer :

-- Exporté depuis HTML Table Exporter PRO

CREATE TABLE produits (
  nom_du_produit TEXT,
  prix REAL,
  en_stock TEXT
);

INSERT INTO produits (nom_du_produit, prix, en_stock) VALUES
  ('Widget Pro', 29.99, 'Oui'),
  ('Gadget Basic', 14.50, 'Non');
Enter fullscreen mode Exit fullscreen mode

Construisons cela étape par étape.

Étape 1 : Extraire la Matrice du Tableau

D'abord, extraire les données brutes du tableau HTML :

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

Sortie : Un tableau 2D où rows[0] est les en-têtes et rows[1+] sont les données.

Étape 2 : Assainir les Noms de Colonnes

Les identifiants SQL ont des règles strictes. Des en-têtes comme « Nom du Produit » ou « Prix (€) » nécessitent un assainissement :

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

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

  // Normaliser l'unicode (supprimer les accents)
  id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");

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

  // S'assurer que ça ne commence pas par un chiffre
  if (/^\d/.test(id)) {
    id = `col_${id}`;
  }

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

  return id;
}

// Exemples :
// "Nom du Produit" → "nom_du_produit"
// "Prix (€)"       → "prix"
// "2024 Revenus"   → "col_2024_revenus"
// ""               → "col_1"
Enter fullscreen mode Exit fullscreen mode

Étape 3 : Gérer les Noms de Colonnes en Double

Les tableaux ont parfois des en-têtes dupliqués. SQL exige des noms de colonnes uniques :

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;
  });
}

// ["Nom", "Nom", "Valeur"] → ["nom", "nom_1", "valeur"]
Enter fullscreen mode Exit fullscreen mode

Étape 4 : Inférer les Types SQL

Analyser les données pour déterminer les types SQL appropriés :

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++) {
    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;
    }

    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

Priorité des types : INTEGER > REAL > TEXT

Étape 5 : Échapper les Valeurs Correctement

L'injection SQL n'est pas seulement un problème de sécurité — elle corrompt vos données. Échappement correct :

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

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

  if (type === "INTEGER" || type === "REAL") {
    const normalized = v.replace(",", ".");
    const num = Number(normalized);

    if (!Number.isNaN(num) && Number.isFinite(num)) {
      return normalized;
    }
  }

  // TEXT : échapper les apostrophes en les doublant
  const escaped = v.replace(/'/g, "''");
  return `'${escaped}'`;
}

// Exemples :
// sqlEscapeValue("Bonjour", "TEXT")      → "'Bonjour'"
// sqlEscapeValue("C'est bon", "TEXT")    → "'C''est bon'"
// sqlEscapeValue("29.99", "REAL")        → "29.99"
// sqlEscapeValue("", "TEXT")             → "NULL"
// sqlEscapeValue(null, "TEXT")           → "NULL"
Enter fullscreen mode Exit fullscreen mode

Étape 6 : Générer le SQL

En assemblant le tout :

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 "";

  const columnNames = makeUniqueColumnNames(headerRow);
  const types = inferSqlColumnTypes(rows, headerRowIndex);

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

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

  // Instructions INSERT
  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(", ")})`;
  });

  let sql = `-- Exporté depuis 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

Gestion des Cas Limites

Valeurs NULL

De nombreux tableaux web représentent les données manquantes différemment :

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";
  }

  // ... reste de la fonction
}
Enter fullscreen mode Exit fullscreen mode

Grands Jeux de Données

Pour les tableaux avec des milliers de lignes, regroupez les INSERT par lots :

function tableToSqlBatched(tableInfo, batchSize = 1000) {
  // ... même configuration ...

  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

Syntaxe Spécifique aux Bases de Données

SQLite, PostgreSQL et MySQL ont de légères différences de syntaxe :

function tableToSql(tableInfo, dialect = "sqlite") {
  // ... même logique colonnes/types ...

  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}`
  );

  // ... reste de la fonction ...
}
Enter fullscreen mode Exit fullscreen mode

Quand Utiliser Ceci

Adapté pour :

  • Imports rapides de données dans SQLite/PostgreSQL
  • Peupler des bases de données de test
  • Migrations de données ponctuelles
  • Apprendre le SQL par l'exemple

Moins adapté pour :

  • ETL à grande échelle (utilisez des outils dédiés)
  • Schémas complexes (clés étrangères, contraintes)
  • Données binaires

Pour des pipelines de données en production, des outils ETL avec validation sont préférables. Pour « j'ai besoin de ce tableau dans ma base de données maintenant », la génération directe de SQL est rapide.

Essayez-le Sans Code

Si vous ne voulez pas construire tout cela vous-même, HTML Table Exporter PRO génère du SQL en un clic. La version gratuite exporte en CSV/JSON/Excel ; la version PRO ajoute SQL, NDJSON et le nettoyage de données.

Pour une comparaison des meilleures extensions d'export, consultez notre guide des scrapers de tableaux HTML pour Chrome.

En savoir plus sur gauchogrid.com/fr/html-table-exporter ou essayez-le sur le Chrome Web Store.


Dans quelle base de données importez-vous des données web ? Je suis curieux de connaître les cas d'utilisation.

Top comments (0)