DEV Community

Cover image for WebテーブルをSQL INSERT文に変換する方法
circobit
circobit

Posted on

WebテーブルをSQL INSERT文に変換する方法

Webページにテーブルがある。それをデータベースに入れたい。

手動のアプローチ:Excelにコピー → クリーンアップ → CSVにエクスポート → 手動でCREATE TABLEを書く → LOAD DATAやCOPYで取り込む → エラーをデバッグ。

もっと良いアプローチ:型推論付きCREATE TABLE、適切にエスケープされたINSERT文を含む完全なSQLを直接生成する。

WebテーブルからSQLコンバーターを構築する方法を解説します。

出力フォーマット

完全なSQLエクスポートには以下が含まれるべきです:

-- Exported from HTML Table Exporter PRO

CREATE TABLE products (
  product_id INTEGER,
  name TEXT,
  price REAL,
  in_stock TEXT
);

INSERT INTO products (product_id, name, price, in_stock) VALUES
  (1, 'Widget', 29.99, 'true'),
  (2, 'Gadget', 49.99, 'false'),
  (3, 'O''Brien''s Special', 19.99, 'true');
Enter fullscreen mode Exit fullscreen mode

主要な要件:

  1. 有効なテーブル名(SQL安全な識別子)
  2. 有効な列名(スペースなし、特殊文字なし)
  3. 適切な列型(INTEGER、REAL、TEXT)
  4. 適切にエスケープされた値(シングルクォートを二重化)
  5. NULLの処理

ステップ1:識別子のサニタイズ

SQL識別子(テーブル名と列名)には厳格なルールがあります:

  • スペースや特殊文字不可
  • 数字で始まることは不可
  • ポータビリティのために小文字にすべき
function sanitizeSqlIdentifier(name, fallback) {
  let id = (name || "").toString().trim();

  if (!id) id = fallback || "col";

  id = id
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")  // アクセント除去(café → cafe)
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, "_")     // 無効な文字をアンダースコアに
    .replace(/^_+|_+$/g, "");          // 先頭/末尾のアンダースコアを除去

  // SQL識別子は数字で始められない
  if (/^[0-9]/.test(id)) {
    id = "_" + id;
  }

  if (!id) id = fallback || "col";
  return id;
}
Enter fullscreen mode Exit fullscreen mode

変換例:

  • "Product Name" → "product_name"
  • "Price ($)" → "price"
  • "2024 Revenue" → "_2024_revenue"
  • "価格" → 適切にサニタイズ

ステップ2:一意な列名の生成

テーブルには重複ヘッダーがありえます。SQLには重複列名は使えません。

function generateColumnNames(headerRow) {
  const usedNames = new Set();

  return headerRow.map((header, index) => {
    const base = sanitizeSqlIdentifier(header, `col_${index + 1}`);
    let candidate = base;
    let counter = 1;

    while (usedNames.has(candidate)) {
      candidate = `${base}_${counter}`;
      counter++;
    }

    usedNames.add(candidate);
    return candidate;
  });
}
Enter fullscreen mode Exit fullscreen mode

変換例:

  • ["Name", "Name", "Value"] → ["name", "name_1", "value"]
  • ["", "", "Data"] → ["col_1", "col_2", "data"]

ステップ3:列型の推論

SQLで重要な3つの型:

  • INTEGER:整数
  • REAL:小数を含む数値
  • TEXT:その他すべて

推論は列の値をサンプリングし、すべての値に適合する最も具体的な型を選択します:

function inferSqlColumnTypes(rows, headerRowIndex = 0) {
  const headerRow = rows[headerRowIndex] || [];
  const dataRows = rows.slice(headerRowIndex + 1);

  const colCount = headerRow.length;
  const types = new Array(colCount).fill("TEXT");

  for (let col = 0; col < colCount; col++) {
    // 最大50個の非空値をサンプリング
    const values = [];

    for (let r = 0; r < dataRows.length && values.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

重要な判断:

  • パフォーマンスのため50値のみサンプリング
  • INTEGERはREALより厳格(小数不可)
  • いずれかの値が一致しなければTEXTにフォールバック
  • 空の列はデフォルトでTEXT

ステップ4:値のエスケープ

SQL文字列はシングルクォートを二重化してエスケープする必要があります:

function sqlEscapeValue(raw, type) {
  // NULLの処理
  if (raw == null) return "NULL";

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

  // 数値型の場合、クォートなしの数値を返す
  if (type === "INTEGER" || type === "REAL") {
    const normalized = v.replace(",", ".");  // EU式小数フォーマットに対応
    const num = Number(normalized);

    if (!Number.isNaN(num) && Number.isFinite(num)) {
      return normalized;
    }
    // 有効な数値でなければTEXT処理にフォールスルー
  }

  // TEXTまたはフォールバック:シングルクォートをエスケープ
  const escaped = v.replace(/'/g, "''");
  return `'${escaped}'`;
}
Enter fullscreen mode Exit fullscreen mode

変換例:

  • "Hello"'Hello'
  • "O'Brien"'O''Brien'
  • "It's \"quoted\""'It''s "quoted"'
  • 123(INTEGER)→ 123
  • 45.67(REAL)→ 45.67
  • ""NULL
  • nullNULL

ステップ5:完全なSQLの生成

function tableToSqlString(tableInfo) {
  const rows = tableInfo.rows || [];
  if (!rows.length) return "";

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

  if (!headerRow) return "";

  // 安全な列名を生成
  const columnNames = generateColumnNames(headerRow);

  // 列型を推論
  const types = inferSqlColumnTypes(rows, headerRowIndex);

  // 安全なテーブル名を生成
  const rawTableName = tableInfo.slug || tableInfo.name || "table";
  let tableName = sanitizeSqlIdentifier(rawTableName, "table");
  if (!tableName) tableName = "table_export";

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

  // 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 = `-- Exported from HTML Table Exporter PRO\n\n${createStmt}\n\n`;

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

  return sql;
}
Enter fullscreen mode Exit fullscreen mode

出力例

入力テーブル:

商品名 価格 数量
ウィジェット 29.99 100
O'Brienの特選品 19.99 50

出力:

-- Exported from HTML Table Exporter PRO

CREATE TABLE products (
  product TEXT,
  price REAL,
  qty INTEGER
);

INSERT INTO products (product, price, qty) VALUES
  ('ウィジェット', 29.99, 100),
  ('O''Brienの特選品', 19.99, 50);
Enter fullscreen mode Exit fullscreen mode

エッジケースの処理

空のテーブル

if (!rows.length) return "";
if (!headerRow) return "";
Enter fullscreen mode Exit fullscreen mode

無効なSQLではなく空文字列を返します。

全NULLの列

空/NULL値のみの列はTEXT型になります。推論関数がこれを処理します:

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

混在する数値フォーマット

エスケーパーはカンマの小数を正規化します:

const normalized = v.replace(",", ".");
Enter fullscreen mode Exit fullscreen mode

「1,234.56」はそのまま。「1.234,56」(EU式)はSQLジェネレーターに到達する前に正規化すべきです——それはクリーニングプリセットの役割です。

非常に長い値

SQLite/PostgreSQLのTEXT列は任意の長さに対応。切り捨て不要。MySQLのVARCHAR制限には長さ指定するか、明示的にTEXTを使用する必要があります。

データベース互換性

生成されるSQLは意図的にシンプルです:

機能 SQLite PostgreSQL MySQL
CREATE TABLE
INTEGER/REAL/TEXT
複数行INSERT
シングルクォートエスケープ

データベース固有の機能(制約、インデックス、スキーマ)が必要な場合はジェネレーターを拡張します。基本的なデータインポートには、これでどこでも動作します。

エクスポートの使用方法

# SQLite
sqlite3 mydb.db < export.sql

# PostgreSQL
psql -d mydb -f export.sql

# MySQL
mysql mydb < export.sql
Enter fullscreen mode Exit fullscreen mode

またはデータベースクライアントに直接ペースト。

SQLが不要でCSVエクスポートパスについては、HTMLテーブルをCSVにエクスポートする方法のガイドをご覧ください。


コードなしでSQLエクスポートが必要ですか?gauchogrid.com/ja/html-table-exporterで詳細を確認するか、Chrome ウェブストアで無料でお試しください。

Top comments (0)