DEV Community

Cover image for 웹 테이블을 SQL INSERT 문으로 변환하기
circobit
circobit

Posted on

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

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

수동 접근 방식: 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');
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"
  • "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;
  });
}
Enter fullscreen mode Exit fullscreen mode

예시:

  • ["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;
}
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(",", ".");  // 유럽식 소수점 처리
    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 = `-- 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

출력 예시

입력 테이블:

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);
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" (유럽식 형식)은 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/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 무료로 사용해 보세요.

Top comments (0)