Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

웹 테이블을 SQL INSERT 문으로 변환하기

웹페이지에 테이블이 있습니다. 데이터베이스에 넣어야 합니다. 수동 접근 방식: Excel에 복사, 정리, CSV 내보내기, 직접 CREATE TABLE 작성, LOAD DATA 또는 COPY 사용, 오류 디버깅. 더 나은 접근 방식: 추론된 타입이 포함된 CREATE TABLE과 적절한 이스케이프가 적용된 INSERT 문을 포함한 완전한 SQL을 직접 생성. 웹 테이블을 SQL로 변환하

웹페이지에 테이블이 있습니다. 데이터베이스에 넣어야 합니다.

수동 접근 방식: Excel에 복사, 정리, CSV 내보내기, 직접 CREATE TABLE 작성, LOAD DATA 또는 COPY 사용, 오류 디버깅.

더 나은 접근 방식: 추론된 타입이 포함된 CREATE TABLE과 적절한 이스케이프가 적용된 INSERT 문을 포함한 완전한 SQL을 직접 생성.

웹 테이블을 SQL로 변환하는 방법을 알아보겠습니다.

출력 형식

완전한 SQL 내보내기에는 다음이 포함되어야 합니다:

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

핵심 요구사항:

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

예시:

  • "Product Name" → "product_name"
  • "Price ($)" → "price"
  • "2024 Revenue" → "_2024_revenue"
  • "Préço" → "preco"

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에서 주로 사용하는 세 가지 타입:

  • 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(",", ".");  // 유럽식 소수점 처리
    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 = `-- HTML Table Exporter PRO에서 내보냄\n\n${createStmt}\n\n`;

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

  return sql;
}

출력 예시

입력 테이블:

Product Price Qty
Widget 29.99 100
O'Brien's 19.99 50

출력:

-- HTML Table Exporter PRO에서 내보냄

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

INSERT INTO products (product, price, qty) VALUES
  ('Widget', 29.99, 100),
  ('O''Brien''s', 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" (유럽식 형식)은 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/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 무료로 사용해 보세요.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.