DEV Community

Cover image for De Tabela HTML a INSERT SQL em Um Clique
circobit
circobit

Posted on

De Tabela HTML a INSERT SQL em Um Clique

Você encontrou dados em um site. Precisa deles no seu banco de dados. O fluxo geralmente envolve: exportar para CSV, importar na ferramenta SQL, lidar com incompatibilidades de tipo, corrigir problemas de encoding.

E se você pudesse ir direto da tabela HTML para SQL válido?

Este guia mostra como gerar instruções CREATE TABLE e INSERT INTO a partir de qualquer tabela web, lidando com inferência de tipos, sanitização de identificadores e escape adequado. Esta é a abordagem que uso no HTML Table Exporter.

O Resultado Final

De uma tabela como esta:

| Nome do Produto | Preço  | Em Estoque |
|-----------------|--------|------------|
| Widget Pro      | 29.99  | Sim        |
| Gadget Basic    | 14.50  | Não        |
Enter fullscreen mode Exit fullscreen mode

Gerar:

-- Exportado pelo HTML Table Exporter PRO

CREATE TABLE produtos (
  nome_do_produto TEXT,
  preco REAL,
  em_estoque TEXT
);

INSERT INTO produtos (nome_do_produto, preco, em_estoque) VALUES
  ('Widget Pro', 29.99, 'Sim'),
  ('Gadget Basic', 14.50, 'Não');
Enter fullscreen mode Exit fullscreen mode

Vamos construir isso passo a passo.

Passo 1: Extrair a Matriz da Tabela

Primeiro, extrair os dados brutos da tabela 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;
}
Enter fullscreen mode Exit fullscreen mode

Saída: Um array 2D onde rows[0] são cabeçalhos e rows[1+] são dados.

Passo 2: Sanitizar Nomes de Colunas

Identificadores SQL têm regras estritas. Cabeçalhos como "Nome do Produto" ou "Preço (R$)" precisam de sanitização:

function sanitizeSqlIdentifier(header, fallbackIndex) {
  let id = (header || "").toString().trim();

  if (!id) {
    return `col_${fallbackIndex + 1}`;
  }

  // Normalizar unicode (remover acentos)
  id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");

  // Converter para snake_case minúsculo
  id = id
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");

  // Garantir que não começa com número
  if (/^\d/.test(id)) {
    id = `col_${id}`;
  }

  // Fallback se vazio após sanitização
  if (!id) {
    return `col_${fallbackIndex + 1}`;
  }

  return id;
}

// Exemplos:
// "Nome do Produto" → "nome_do_produto"
// "Preço (R$)"      → "preco_r"
// "2024 Receita"    → "col_2024_receita"
// ""                → "col_1"
Enter fullscreen mode Exit fullscreen mode

Passo 3: Tratar Nomes de Colunas Duplicados

Tabelas às vezes têm cabeçalhos duplicados. SQL exige nomes de colunas únicos:

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

// ["Nome", "Nome", "Valor"] → ["nome", "nome_1", "valor"]
Enter fullscreen mode Exit fullscreen mode

Passo 4: Inferir Tipos SQL

Analisar dados para determinar os tipos SQL apropriados:

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++) {
    // Amostrar até 50 valores por coluna
    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;
    }

    // Verificar se todos os valores correspondem a um tipo
    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

Prioridade de tipos: INTEGER > REAL > TEXT

Se 100% dos valores não-vazios são inteiros, usa INTEGER. Se são todos numéricos (incluindo decimais), usa REAL. Caso contrário, TEXT.

Passo 5: Escapar Valores Corretamente

Injeção SQL não é apenas uma preocupação de segurança — corrompe seus dados. Escape adequado:

function sqlEscapeValue(raw, type) {
  // Tratamento de NULL
  if (raw == null) return "NULL";

  const v = String(raw).trim();
  if (v === "") return "NULL";

  // Tipos numéricos
  if (type === "INTEGER" || type === "REAL") {
    // Normalizar separador decimal
    const normalized = v.replace(",", ".");
    const num = Number(normalized);

    if (!Number.isNaN(num) && Number.isFinite(num)) {
      return normalized;
    }
    // Cair para TEXT se não for um número válido
  }

  // TEXT: escapar aspas simples duplicando-as
  const escaped = v.replace(/'/g, "''");
  return `'${escaped}'`;
}

// Exemplos:
// sqlEscapeValue("Olá", "TEXT")         → "'Olá'"
// sqlEscapeValue("It's good", "TEXT")   → "'It''s good'"
// sqlEscapeValue("29.99", "REAL")       → "29.99"
// sqlEscapeValue("", "TEXT")            → "NULL"
// sqlEscapeValue(null, "TEXT")          → "NULL"
Enter fullscreen mode Exit fullscreen mode

Passo 6: Gerar o SQL

Juntando tudo:

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

  // Gerar nomes de colunas
  const columnNames = makeUniqueColumnNames(headerRow);

  // Inferir tipos
  const types = inferSqlColumnTypes(rows, headerRowIndex);

  // Gerar nome da tabela
  const rawTableName = tableInfo.name || tableInfo.slug || "tabela";
  const tableName = sanitizeSqlIdentifier(rawTableName, 0) || "tabela_exportada";

  // Instrução CREATE TABLE
  const createLines = columnNames.map((col, i) => 
    `  ${col} ${types[i] || "TEXT"}`
  );
  const createStmt = `CREATE TABLE ${tableName} (\n${createLines.join(",\n")}\n);`;

  // Instruções 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(", ")})`;
  });

  // Combinar
  let sql = `-- Exportado pelo HTML Table Exporter\n\n${createStmt}\n\n`;

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

  return sql;
}
Enter fullscreen mode Exit fullscreen mode

Exemplo Completo

Tabela de entrada:

const tableInfo = {
  name: "Vendas Q1",
  rows: [
    ["Produto", "Unid. Vendidas", "Receita (R$)", "Lucrativo"],
    ["Widget A", "1.234", "45.678,90", "Sim"],
    ["Widget B", "567", "12.345,67", "Não"],
    ["Gadget X", "890", "23.456,78", "Sim"]
  ]
};
Enter fullscreen mode Exit fullscreen mode

Saída:

-- Exportado pelo HTML Table Exporter

CREATE TABLE vendas_q1 (
  produto TEXT,
  unid_vendidas INTEGER,
  receita_r REAL,
  lucrativo TEXT
);

INSERT INTO vendas_q1 (produto, unid_vendidas, receita_r, lucrativo) VALUES
  ('Widget A', 1234, 45678.90, 'Sim'),
  ('Widget B', 567, 12345.67, 'Não'),
  ('Gadget X', 890, 23456.78, 'Sim');
Enter fullscreen mode Exit fullscreen mode

Observe:

  • Nomes de colunas sanitizados (Receita (R$)receita_r)
  • Números detectados e sem aspas
  • Vírgulas nos números tratadas
  • Nome da tabela vindo dos metadados

Tratando Casos Extremos

Valores NULL

Muitas tabelas web representam dados ausentes de formas diferentes:

const NULL_PATTERNS = ["N/A", "n/a", "N/D", "-", "--", "null", "none", "nenhum", "."];

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

  // ... resto da função
}
Enter fullscreen mode Exit fullscreen mode

Grandes Conjuntos de Dados

Para tabelas com milhares de linhas, divida os INSERTs em lotes:

function tableToSqlBatched(tableInfo, batchSize = 1000) {
  // ... mesma configuração ...

  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")}`;
}
Enter fullscreen mode Exit fullscreen mode

Sintaxe Específica por Banco de Dados

SQLite, PostgreSQL e MySQL têm diferenças sutis de sintaxe:

function tableToSql(tableInfo, dialect = "sqlite") {
  // ... mesma lógica de colunas/tipos ...

  // Mapeamento de tipos específico por dialeto
  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}`
  );

  // ... resto da função ...
}
Enter fullscreen mode Exit fullscreen mode

Bookmarklet para o Navegador

Solução rápida para uso ocasional:

javascript:(function(){
  const table = document.querySelector("table");
  if (!table) { alert("Nenhuma tabela encontrada"); return; }

  const rows = Array.from(table.rows).map(r => 
    Array.from(r.cells).map(c => c.textContent.trim())
  );

  const headers = rows[0].map((h, i) => 
    (h || `col_${i+1}`).toLowerCase().replace(/[^a-z0-9]+/g, "_")
  );

  const values = rows.slice(1).map(row => 
    "(" + row.map(v => `'${v.replace(/'/g, "''")}'`).join(", ") + ")"
  ).join(",\n");

  const sql = `INSERT INTO table_data (${headers.join(", ")}) VALUES\n${values};`;

  navigator.clipboard.writeText(sql);
  alert("SQL copiado para a área de transferência!");
})();
Enter fullscreen mode Exit fullscreen mode

Quando Usar Isso

Bom para:

  • Importações rápidas de dados para SQLite/PostgreSQL
  • Popular bancos de dados de teste
  • Migrações de dados pontuais
  • Aprender SQL por exemplos

Não ideal para:

  • ETL em larga escala (use ferramentas próprias)
  • Schemas complexos (chaves estrangeiras, constraints)
  • Dados binários

Para pipelines de dados em produção, ferramentas ETL com validação são melhores. Para "preciso desta tabela no meu banco agora", geração direta de SQL é rápida.

Experimente Sem Código

Se não quer construir isso você mesmo, o HTML Table Exporter PRO gera SQL com um clique. A versão gratuita exporta para CSV/JSON/Excel; a PRO adiciona SQL, NDJSON e limpeza de dados.

Para saber qual é a melhor extensão Chrome para copiar tabelas para planilhas, confira nossa análise comparativa.

Saiba mais em gauchogrid.com/pt-br/html-table-exporter ou experimente na Chrome Web Store.


Para qual banco de dados você está importando dados web? Tenho curiosidade sobre os casos de uso.

Top comments (0)