Encontraste datos en un sitio web. Los necesitas en tu base de datos. El flujo habitual implica: exportar a CSV, importar en una herramienta SQL, lidiar con tipos incompatibles, arreglar problemas de encoding.
¿Y si pudieras ir directamente de tabla HTML a SQL válido?
Esta guía muestra cómo generar sentencias CREATE TABLE e INSERT INTO desde cualquier tabla web, manejando inferencia de tipos, sanitización de identificadores y escaping correcto. Este es el enfoque que uso en HTML Table Exporter.
El Resultado Final
Desde una tabla como esta:
| Nombre Producto | Precio | En Stock |
|-----------------|--------|----------|
| Widget Pro | 29.99 | Sí |
| Gadget Basic | 14.50 | No |
Generar:
-- Exportado desde HTML Table Exporter PRO
CREATE TABLE productos (
nombre_producto TEXT,
precio REAL,
en_stock TEXT
);
INSERT INTO productos (nombre_producto, precio, en_stock) VALUES
('Widget Pro', 29.99, 'Sí'),
('Gadget Basic', 14.50, 'No');
Construyamos esto paso a paso.
Paso 1: Extraer la Matriz de la Tabla
Primero, extraer los datos crudos de la tabla 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;
}
Salida: Un array 2D donde rows[0] son encabezados y rows[1+] son datos.
Paso 2: Sanitizar Nombres de Columnas
Los identificadores SQL tienen reglas estrictas. Encabezados como "Nombre Producto" o "Precio ($)" necesitan sanitización:
function sanitizeSqlIdentifier(header, fallbackIndex) {
let id = (header || "").toString().trim();
if (!id) {
return `col_${fallbackIndex + 1}`;
}
// Normalizar unicode (eliminar acentos)
id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// Convertir a snake_case en minúsculas
id = id
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
// Asegurar que no empiece con número
if (/^\d/.test(id)) {
id = `col_${id}`;
}
// Fallback si queda vacío después de sanitizar
if (!id) {
return `col_${fallbackIndex + 1}`;
}
return id;
}
// Ejemplos:
// "Nombre Producto" → "nombre_producto"
// "Precio ($)" → "precio"
// "2024 Ingresos" → "col_2024_ingresos"
// "" → "col_1"
Paso 3: Manejar Nombres de Columnas Duplicados
Las tablas a veces tienen encabezados duplicados. SQL requiere nombres de columna ú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;
});
}
// ["Nombre", "Nombre", "Valor"] → ["nombre", "nombre_1", "valor"]
Paso 4: Inferir Tipos SQL
Analizar datos para determinar los tipos SQL apropiados:
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++) {
// Muestrear hasta 50 valores por columna
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 si todos los valores coinciden con un 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;
}
Prioridad de tipos: INTEGER > REAL > TEXT
Si el 100% de los valores no vacíos son enteros, usar INTEGER. Si son todos numéricos (incluyendo decimales), usar REAL. Si no, TEXT.
Paso 5: Escapar Valores Correctamente
La inyección SQL no es solo una preocupación de seguridad—corrompe tus datos. Escaping correcto:
function sqlEscapeValue(raw, type) {
// Manejo 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;
}
// Caer al caso TEXT si no es un número válido
}
// TEXT: escapar comillas simples duplicándolas
const escaped = v.replace(/'/g, "''");
return `'${escaped}'`;
}
// Ejemplos:
// sqlEscapeValue("Hola", "TEXT") → "'Hola'"
// sqlEscapeValue("Es bueno", "TEXT") → "'Es bueno'"
// sqlEscapeValue("It's good", "TEXT") → "'It''s good'"
// sqlEscapeValue("29.99", "REAL") → "29.99"
// sqlEscapeValue("", "TEXT") → "NULL"
// sqlEscapeValue(null, "TEXT") → "NULL"
Paso 6: Generar el SQL
Juntando todo:
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 "";
// Generar nombres de columnas
const columnNames = makeUniqueColumnNames(headerRow);
// Inferir tipos
const types = inferSqlColumnTypes(rows, headerRowIndex);
// Generar nombre de tabla
const rawTableName = tableInfo.name || tableInfo.slug || "table";
const tableName = sanitizeSqlIdentifier(rawTableName, 0) || "table_export";
// Sentencia CREATE TABLE
const createLines = columnNames.map((col, i) =>
` ${col} ${types[i] || "TEXT"}`
);
const createStmt = `CREATE TABLE ${tableName} (\n${createLines.join(",\n")}\n);`;
// Sentencias 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 desde HTML Table Exporter\n\n${createStmt}\n\n`;
if (valueLines.length > 0) {
sql += `${insertHeader}\n${valueLines.join(",\n")};\n`;
}
return sql;
}
Ejemplo Completo
Tabla de entrada:
const tableInfo = {
name: "Ventas Q1",
rows: [
["Producto", "Unidades Vendidas", "Ingresos ($)", "Rentable"],
["Widget A", "1,234", "45,678.90", "Sí"],
["Widget B", "567", "12,345.67", "No"],
["Gadget X", "890", "23,456.78", "Sí"]
]
};
Salida:
-- Exportado desde HTML Table Exporter
CREATE TABLE ventas_q1 (
producto TEXT,
unidades_vendidas INTEGER,
ingresos REAL,
rentable TEXT
);
INSERT INTO ventas_q1 (producto, unidades_vendidas, ingresos, rentable) VALUES
('Widget A', 1234, 45678.90, 'Sí'),
('Widget B', 567, 12345.67, 'No'),
('Gadget X', 890, 23456.78, 'Sí');
Nota:
- Nombres de columnas sanitizados (
Ingresos ($)→ingresos) - Números detectados y sin comillas
- Comas en números manejadas
- Nombre de tabla desde metadatos
Manejo de Casos Extremos
Valores NULL
Muchas tablas web representan datos faltantes de forma diferente:
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";
}
// ... resto de la función
}
Datasets Grandes
Para tablas con miles de filas, dividir los INSERTs en lotes:
function tableToSqlBatched(tableInfo, batchSize = 1000) {
// ... misma configuración ...
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")}`;
}
Sintaxis Específica por Base de Datos
SQLite, PostgreSQL y MySQL tienen diferencias sutiles de sintaxis:
function tableToSql(tableInfo, dialect = "sqlite") {
// ... misma lógica de columnas/tipos ...
// Mapeo de tipos específico por dialecto
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 de la función ...
}
Bookmarklet de Navegador
Solución rápida para uso ocasional:
javascript:(function(){
const table = document.querySelector("table");
if (!table) { alert("No se encontró tabla"); 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 al portapapeles!");
})();
Cuándo Usar Esto
Bueno para:
- Importaciones rápidas de datos a SQLite/PostgreSQL
- Poblar bases de datos de prueba
- Migraciones de datos únicas
- Aprender SQL con ejemplos
No ideal para:
- ETL a gran escala (usar herramientas dedicadas)
- Esquemas complejos (foreign keys, constraints)
- Datos binarios
Para pipelines de datos en producción, las herramientas ETL con validación son mejores. Para "necesito esta tabla en mi base de datos ahora", la generación directa de SQL es rápida.
Pruébalo Sin Código
Si no quieres construir esto tú mismo, HTML Table Exporter PRO genera SQL con un clic. La versión gratuita exporta a CSV/JSON/Excel; PRO agrega SQL, NDJSON y limpieza de datos.
Para una comparación de diferentes enfoques de exportación, mira nuestra guía sobre scraper de tablas HTML para Chrome.
Más información en gauchogrid.com/es/html-table-exporter o pruébalo en la Chrome Web Store.
¿A qué base de datos estás importando datos web? Tengo curiosidad por los casos de uso.
Top comments (0)