Tabelas web são strings. Tudo é string. Mas quando você exporta para JSON ou SQL, você quer:
-
"1.234,56"→1234.56(número) -
"2024-03-15"→ tipo date -
"Sim"→true(booleano) -
"N/D"→null
Construir um sistema confiável de inferência de tipos significa lidar com o caos dos dados do mundo real. Veja como eu abordei isso no HTML Table Exporter.
O Problema: Ambiguidade em Todo Lugar
Considere estes valores:
| Valor | Poderia ser... |
|---|---|
"1,234" |
Número 1234 (EUA) ou 1.234 (BR/EU) |
"01/02/03" |
1 Jan 2003 (EUA) ou 1 Fev 2003 (BR) ou 2001-02-03 |
"1" |
Inteiro 1 ou booleano true |
"N/D" |
String ou null |
"0" |
Inteiro 0 ou booleano false |
Inferência de tipos não é sobre parsing — é sobre adivinhar a intenção a partir de evidências ambíguas.
Arquitetura: Inferência no Nível da Coluna
Inferir tipos célula por célula não é confiável. O valor "1" sozinho pode ser qualquer coisa. Mas uma coluna contendo ["1", "2", "3", "4"] é claramente de inteiros.
Minha abordagem:
- Amostrar valores da coluna (até 100)
- Inferir o tipo para cada valor não-nulo
- Agregar para determinar o tipo da coluna
- Aplicar limiar de 90% — se 90%+ dos valores correspondem a um tipo, usar esse tipo
const DATA_TYPES = {
STRING: "string",
INTEGER: "integer",
NUMBER: "number",
BOOLEAN: "boolean",
DATE: "date",
NULL: "null",
};
function inferColumnType(columnValues, maxSamples = 100) {
const nonNullValues = columnValues.filter(v => v != null && v !== "");
if (nonNullValues.length === 0) {
return { type: DATA_TYPES.STRING, confidence: 0 };
}
const sample = nonNullValues.slice(0, maxSamples);
// Contar ocorrências de cada tipo
const typeCounts = {
[DATA_TYPES.INTEGER]: 0,
[DATA_TYPES.NUMBER]: 0,
[DATA_TYPES.BOOLEAN]: 0,
[DATA_TYPES.DATE]: 0,
[DATA_TYPES.STRING]: 0,
};
for (const val of sample) {
const type = inferValueType(val);
if (type === DATA_TYPES.INTEGER) {
// INTEGER é um subconjunto de NUMBER
typeCounts[DATA_TYPES.INTEGER]++;
typeCounts[DATA_TYPES.NUMBER]++;
} else if (type !== DATA_TYPES.NULL) {
typeCounts[type]++;
}
}
const total = sample.length;
const threshold = 0.9;
// Verificar tipos em ordem de prioridade
if (typeCounts[DATA_TYPES.BOOLEAN] >= total * threshold) {
return { type: DATA_TYPES.BOOLEAN, confidence: typeCounts[DATA_TYPES.BOOLEAN] / total };
}
if (typeCounts[DATA_TYPES.DATE] >= total * threshold) {
return { type: DATA_TYPES.DATE, confidence: typeCounts[DATA_TYPES.DATE] / total };
}
if (typeCounts[DATA_TYPES.INTEGER] >= total * threshold) {
return { type: DATA_TYPES.INTEGER, confidence: typeCounts[DATA_TYPES.INTEGER] / total };
}
if (typeCounts[DATA_TYPES.NUMBER] >= total * threshold) {
return { type: DATA_TYPES.NUMBER, confidence: typeCounts[DATA_TYPES.NUMBER] / total };
}
return { type: DATA_TYPES.STRING, confidence: 1 };
}
Por que 90%? Dados reais têm ruído. Uma coluna com 100 inteiros pode ter um "N/D". Exigir 100% de correspondência é rigoroso demais.
Detecção de Tipo no Nível do Valor
A função inferValueType lida com valores individuais:
function inferValueType(value) {
if (value == null || value === "") {
return DATA_TYPES.NULL;
}
const str = String(value).trim();
if (str === "") return DATA_TYPES.NULL;
// Verificação de booleano
const lowerStr = str.toLowerCase();
if (["true", "false", "yes", "no", "sim", "não", "sí", "si"].includes(lowerStr)) {
return DATA_TYPES.BOOLEAN;
}
// Verificação de data (formato ISO preferido)
if (/^\d{4}-\d{2}-\d{2}/.test(str)) {
return DATA_TYPES.DATE;
}
if (/^\d{1,2}[/\-\.]\d{1,2}[/\-\.]\d{2,4}$/.test(str)) {
return DATA_TYPES.DATE;
}
// Verificação de inteiro (estrita)
if (/^-?\d+$/.test(str)) {
return DATA_TYPES.INTEGER;
}
// Verificação de número (com decimais e separadores)
const cleanedForNumber = str
.replace(/[$€£¥R\$%\s]/g, "")
.replace(/,/g, ".");
if (/^-?\d+(\.\d+)?$/.test(cleanedForNumber)) {
return DATA_TYPES.NUMBER;
}
return DATA_TYPES.STRING;
}
O Desafio da Normalização Numérica
Formatos numéricos europeu/brasileiro vs americano são a ambiguidade mais difícil:
| Valor | Interpretação EUA | Interpretação BR/EU |
|---|---|---|
"1,234" |
1234 | 1.234 |
"1.234" |
1.234 | 1234 |
"1,234.56" |
1234.56 | Inválido |
"1.234,56" |
Inválido | 1234.56 |
Minha heurística para detectar o formato:
function normalizeNumber(value) {
if (value == null) return value;
let str = String(value).trim();
// Remover símbolos de moeda e espaços
str = str.replace(/[$€£¥R\$\s]/g, "");
// Tratar porcentagem
const isPercent = str.endsWith("%");
if (isPercent) str = str.slice(0, -1);
// Detectar formato analisando separadores
const commaCount = (str.match(/,/g) || []).length;
const dotCount = (str.match(/\./g) || []).length;
const lastComma = str.lastIndexOf(",");
const lastDot = str.lastIndexOf(".");
let normalized;
if (commaCount === 0 && dotCount === 0) {
// Inteiro simples: "1234"
normalized = str;
} else if (commaCount === 0 && dotCount === 1) {
// "1.234" (separador de milhar EU/BR) ou "1.23" (decimal)
// Heurística: 3 dígitos após o ponto = separador de milhar
const afterDot = str.slice(lastDot + 1);
if (afterDot.length === 3 && /^\d+$/.test(afterDot)) {
// Provavelmente separador de milhar EU/BR
normalized = str.replace(".", "");
} else {
// Provavelmente decimal
normalized = str;
}
} else if (commaCount === 1 && dotCount === 0) {
// "1,234" (separador de milhar EUA) ou "1,23" (decimal EU/BR)
const afterComma = str.slice(lastComma + 1);
if (afterComma.length === 3 && /^\d+$/.test(afterComma)) {
// Provavelmente separador de milhar EUA
normalized = str.replace(",", "");
} else {
// Provavelmente decimal EU/BR
normalized = str.replace(",", ".");
}
} else if (lastDot > lastComma) {
// "1,234.56" - formato EUA
normalized = str.replace(/,/g, "");
} else if (lastComma > lastDot) {
// "1.234,56" - formato EU/BR
normalized = str.replace(/\./g, "").replace(",", ".");
} else {
// Ambíguo, retornar como está
return value;
}
const num = parseFloat(normalized);
if (Number.isNaN(num)) return value;
return isPercent ? num / 100 : num;
}
Insight chave: A posição e contagem dos separadores resolve a maioria das ambiguidades. O último separador é geralmente o separador decimal.
Detecção de Booleanos (Com Armadilhas)
Booleanos óbvios: "true", "false", "sim", "não"
Mas e quanto a "0" e "1"?
// Problemático: Isso converte alíquotas de imposto em booleanos
// Coluna: [0, 0.05, 0.1, 0.2] -> [false, 0.05, 0.1, 0.2]
Minha solução: Não converter valores puramente numéricos em booleanos.
function applyBooleanNormalization(rows, booleanConfig) {
const trueValues = new Set(booleanConfig.true.map(v => v.toLowerCase()));
const falseValues = new Set(booleanConfig.false.map(v => v.toLowerCase()));
return rows.map((row, rowIndex) => {
if (rowIndex === 0) return row; // Pular cabeçalho
return row.map(cell => {
if (cell == null) return cell;
const cellStr = String(cell).toLowerCase().trim();
// Pular se puramente numérico (previne 0 -> false em colunas numéricas)
if (/^-?\d+(\.\d+)?$/.test(cellStr)) {
return cell;
}
if (trueValues.has(cellStr)) return "true";
if (falseValues.has(cellStr)) return "false";
return cell;
});
});
}
Tratamento de Valores Nulos
Dados web usam muitas representações para "sem valor":
-
""(string vazia) -
"N/D","n/d","N/A","NA" -
"-","--" -
"null","NULL" -
"nenhum","none","None" -
"."(comum em dados governamentais)
Detecção de nulo configurável:
function applyNullValues(rows, nullPatterns) {
const nullSet = new Set(nullPatterns.map(v => v.toLowerCase().trim()));
return rows.map((row, rowIndex) => {
if (rowIndex === 0) return row; // Pular cabeçalho
return row.map(cell => {
if (cell == null) return null;
const cellStr = String(cell).toLowerCase().trim();
return nullSet.has(cellStr) ? null : cell;
});
});
}
// Uso
applyNullValues(rows, ["N/A", "n/a", "N/D", "-", "--", "null", "none", "nenhum", "."]);
O Pipeline
Pipeline completo de inferência de tipos e limpeza:
function cleanTable(tableInfo, config) {
let rows = cloneRows(tableInfo.rows);
// 1. Remover espaços de todas as strings
if (config.trimStrings) {
rows = applyTrimStrings(rows);
}
// 2. Converter padrões de nulo em null real
if (config.nullValues?.length) {
rows = applyNullValues(rows, config.nullValues);
}
// 3. Normalizar booleanos (antes dos números, para evitar problemas 0->false)
if (config.booleans) {
rows = applyBooleanNormalization(rows, config.booleans);
}
// 4. Normalizar números (lida com formatos EU/EUA)
if (config.normalizeNumbers) {
rows = applyNumberNormalization(rows);
}
// 5. Normalizar datas (opcional, específico por formato)
if (config.normalizeDates) {
rows = applyDateNormalization(rows, config.dateFormat);
}
return { ...tableInfo, rows };
}
Testando Casos Extremos
Inferência de tipos tem muitos casos extremos. Eu mantenho uma suíte de testes:
// Testes de normalização de números
assertEqual(normalizeNumber("1,234.56"), 1234.56); // Formato EUA
assertEqual(normalizeNumber("1.234,56"), 1234.56); // Formato BR/EU
assertEqual(normalizeNumber("€1.234,56"), 1234.56); // Com moeda
assertEqual(normalizeNumber("45,5%"), 0.455); // Porcentagem
assertEqual(normalizeNumber("1.234"), 1234); // Milhar BR/EU
assertEqual(normalizeNumber("1.23"), 1.23); // Decimal
// Testes de booleano
assertEqual(inferValueType("Sim"), DATA_TYPES.BOOLEAN);
assertEqual(inferValueType("0"), DATA_TYPES.INTEGER); // NÃO booleano
assertEqual(inferValueType("0.5"), DATA_TYPES.NUMBER); // NÃO booleano
Integração com Exportação
Ao exportar para SQL, use os tipos inferidos:
function inferSqlColumnTypes(rows) {
const header = rows[0];
const dataRows = rows.slice(1);
return header.map((_, colIndex) => {
const columnValues = dataRows.map(row => row[colIndex]);
const { type } = inferColumnType(columnValues);
switch (type) {
case DATA_TYPES.INTEGER: return "INTEGER";
case DATA_TYPES.NUMBER: return "REAL";
case DATA_TYPES.BOOLEAN: return "BOOLEAN";
case DATA_TYPES.DATE: return "DATE";
default: return "TEXT";
}
});
}
Lições Aprendidas
Contexto da coluna supera análise da célula. Um único
"1"é ambíguo; uma coluna de inteiros não é.Seja conservador com conversões. É melhor deixar algo como string do que corrompê-lo com uma conversão errada.
Torne configurável. Diferentes domínios têm diferentes valores nulos, representações de booleanos e formatos numéricos.
Teste com dados reais. Testes sintéticos não capturam o caos das tabelas web reais.
Este sistema alimenta a limpeza de dados no HTML Table Exporter. Para conhecer as melhores ferramentas para extrair dados de tabelas web, confira nossa comparação das melhores extensões Chrome para exportar tabelas.
A versão PRO permite que os usuários configurem essas regras por perfil de exportação. Saiba mais em gauchogrid.com/pt-br/html-table-exporter ou experimente na Chrome Web Store.
Construindo inferência de tipos para outro domínio? Adoraria ouvir sobre seus casos extremos.