Webサイトでデータを見つけた。データベースに入れたい。通常のワークフロー:CSVにエクスポート → SQLツールにインポート → 型の不一致に対処 → エンコーディング問題を修正。
HTMLテーブルから直接、有効なSQLを生成できたらどうでしょう?
このガイドでは、型推論、識別子のサニタイズ、適切なエスケープ処理を含め、任意のWebテーブルから CREATE TABLE と INSERT INTO 文を生成する方法を説明します。これはHTML Table Exporterで使用しているアプローチです。
最終結果
このようなテーブルから:
| Product Name | Price | In Stock |
|-----------------|--------|----------|
| Widget Pro | 29.99 | Yes |
| Gadget Basic | 14.50 | No |
以下を生成:
-- Exported from HTML Table Exporter PRO
CREATE TABLE products (
product_name TEXT,
price REAL,
in_stock TEXT
);
INSERT INTO products (product_name, price, in_stock) VALUES
('Widget Pro', 29.99, 'Yes'),
('Gadget Basic', 14.50, 'No');
ステップバイステップで構築していきましょう。
ステップ1: テーブルマトリクスの抽出
まず、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;
}
ステップ2: カラム名のサニタイズ
SQL識別子には厳格なルールがあります。"Product Name" や "Price ($)" のようなヘッダーにはサニタイズが必要です:
function sanitizeSqlIdentifier(header, fallbackIndex) {
let id = (header || "").toString().trim();
if (!id) {
return `col_${fallbackIndex + 1}`;
}
// Unicodeを正規化(アクセント除去)
id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// 小文字のsnake_caseに変換
id = id
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
// 数字で始まらないことを確認
if (/^\d/.test(id)) {
id = `col_${id}`;
}
if (!id) {
return `col_${fallbackIndex + 1}`;
}
return id;
}
// 例:
// "Product Name" → "product_name"
// "Price ($)" → "price"
// "2024 Revenue" → "col_2024_revenue"
// "" → "col_1"
ステップ3: 重複カラム名の処理
テーブルに重複ヘッダーがある場合があります。SQLは一意のカラム名を要求します:
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", "Value"] → ["name", "name_1", "value"]
ステップ4: SQL型の推論
データを分析して適切なSQL型を決定:
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;
}
型の優先順位: INTEGER > REAL > TEXT
ステップ5: 値の適切なエスケープ
SQLインジェクションはセキュリティの問題だけでなく、データの破損にもつながります。適切なエスケープ:
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: シングルクォートを二重にしてエスケープ
const escaped = v.replace(/'/g, "''");
return `'${escaped}'`;
}
// 例:
// sqlEscapeValue("Hello", "TEXT") → "'Hello'"
// sqlEscapeValue("It's good", "TEXT") → "'It''s good'"
// sqlEscapeValue("29.99", "REAL") → "29.99"
// sqlEscapeValue("", "TEXT") → "NULL"
// sqlEscapeValue(null, "TEXT") → "NULL"
ステップ6: SQLの生成
すべてを組み合わせて:
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";
// 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\n\n${createStmt}\n\n`;
if (valueLines.length > 0) {
sql += `${insertHeader}\n${valueLines.join(",\n")};\n`;
}
return sql;
}
エッジケースの処理
NULL値
Webテーブルは欠損データを様々な方法で表現します:
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";
}
// ... 残りの関数
}
大規模データセット
数千行のテーブルにはINSERTをバッチ処理:
function tableToSqlBatched(tableInfo, batchSize = 1000) {
// ... 同じセットアップ ...
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")}`;
}
データベース固有の構文
SQLite、PostgreSQL、MySQLにはわずかな構文の違いがあります:
function tableToSql(tableInfo, dialect = "sqlite") {
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;
// ...
}
いつ使うべきか
適している場合:
- SQLite/PostgreSQLへの素早いデータインポート
- テストデータベースのシード
- 一回限りのデータ移行
- 例を通じてSQLを学ぶ
不向きな場合:
- 大規模ETL(専用ツールを使用)
- 複雑なスキーマ(外部キー、制約)
- バイナリデータ
コード不要で試す
自分で構築したくない場合、HTML Table Exporter PROがワンクリックでSQLを生成します。無料版はCSV/JSON/Excelにエクスポートでき、PROではSQL、NDJSON、データクリーニングが追加されます。
エクスポートアプローチの比較については、テーブルエクスポートに最適なChrome拡張機能 トップ5のガイドをご覧ください。
詳しくは gauchogrid.com/ja/html-table-exporter をご覧いただくか、Chrome Web Store でお試しください。
Webデータをどのデータベースにインポートしていますか?ユースケースに興味があります。
Top comments (0)