웹사이트에서 데이터를 찾았습니다. 데이터베이스에 넣어야 합니다. 보통 워크플로우는: CSV로 내보내기, SQL 도구에 임포트, 타입 불일치 처리, 인코딩 문제 수정.
HTML 테이블에서 바로 유효한 SQL을 생성할 수 있다면 어떨까요?
이 가이드에서는 모든 웹 테이블에서 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;
}
출력: rows[0]이 헤더이고 rows[1+]이 데이터인 2D 배열.
2단계: 컬럼명 정제
SQL 식별자에는 엄격한 규칙이 있습니다. "Product Name"이나 "Price ($)" 같은 헤더는 정제가 필요합니다:
function sanitizeSqlIdentifier(header, fallbackIndex) {
let id = (header || "").toString().trim();
if (!id) {
return `col_${fallbackIndex + 1}`;
}
// 유니코드 정규화 (악센트 제거)
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++) {
// 컬럼당 최대 50개 값 샘플링
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) {
// NULL 처리
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 값
많은 웹 테이블이 누락 데이터를 다르게 표현합니다:
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;
const createLines = columnNames.map((col, i) =>
` ${col} ${dialectTypes[types[i]] || dialectTypes.TEXT}`
);
// ... 나머지 함수 ...
}
사용 시기
적합한 경우:
- SQLite/PostgreSQL에 빠른 데이터 임포트
- 테스트 데이터베이스 시딩
- 일회성 데이터 마이그레이션
- 예시를 통한 SQL 학습
부적합한 경우:
- 대규모 ETL (전용 도구 사용)
- 복잡한 스키마 (외래 키, 제약 조건)
- 바이너리 데이터
프로덕션 데이터 파이프라인에는 검증 기능이 있는 적절한 ETL 도구가 낫습니다. "이 테이블을 지금 당장 데이터베이스에 넣어야 해"라면 직접 SQL 생성이 빠릅니다.
코드 없이 사용하기
직접 구축하고 싶지 않다면 HTML Table Exporter PRO가 원클릭으로 SQL을 생성합니다. 무료 버전은 CSV/JSON/Excel로 내보내기 가능하고, PRO는 SQL, NDJSON, 데이터 정제를 추가합니다.
다양한 내보내기 접근법의 비교는 HTML 테이블을 CSV로 내보내는 방법 가이드를 참고하세요.
gauchogrid.com/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 사용해보세요.
어떤 데이터베이스에 웹 데이터를 임포트하고 계신가요? 사용 사례가 궁금합니다.
Top comments (0)