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');
主要な要件:
- 有効なテーブル名(SQL安全な識別子)
- 有効な列名(スペースなし、特殊文字なし)
- 適切な列型(INTEGER、REAL、TEXT)
- 適切にエスケープされた値(シングルクォートを二重化)
- 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;
}
変換例:
- "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;
});
}
変換例:
- ["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;
}
重要な判断:
- パフォーマンスのため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}'`;
}
変換例:
-
"Hello"→'Hello' -
"O'Brien"→'O''Brien' -
"It's \"quoted\""→'It''s "quoted"' -
123(INTEGER)→123 -
45.67(REAL)→45.67 -
""→NULL -
null→NULL
ステップ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;
}
出力例
入力テーブル:
| 商品名 | 価格 | 数量 |
|---|---|---|
| ウィジェット | 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);
エッジケースの処理
空のテーブル
if (!rows.length) return "";
if (!headerRow) return "";
無効なSQLではなく空文字列を返します。
全NULLの列
空/NULL値のみの列はTEXT型になります。推論関数がこれを処理します:
if (values.length === 0) {
types[col] = "TEXT";
continue;
}
混在する数値フォーマット
エスケーパーはカンマの小数を正規化します:
const normalized = v.replace(",", ".");
「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
またはデータベースクライアントに直接ペースト。
SQLが不要でCSVエクスポートパスについては、HTMLテーブルをCSVにエクスポートする方法のガイドをご覧ください。
コードなしでSQLエクスポートが必要ですか?gauchogrid.com/ja/html-table-exporterで詳細を確認するか、Chrome ウェブストアで無料でお試しください。
Top comments (0)