Les tableaux web sont des chaînes de caractères. Tout est une chaîne. Mais quand on exporte en JSON ou SQL, on veut :
-
"1 234,56"→1234.56(nombre) -
"2024-03-15"→ type date -
"Oui"→true(booléen) -
"N/A"→null
Construire un système fiable d'inférence de types, c'est gérer le chaos des données du monde réel. Voici comment je l'ai abordé pour HTML Table Exporter.
Le Problème : l'Ambiguïté Partout
Considérez ces valeurs :
| Valeur | Pourrait être... |
|---|---|
"1,234" |
Nombre 1234 (US) ou 1.234 (EU) |
"01/02/03" |
1er fév. 2003 (EU) ou 2 jan. 2003 (US) ou 2001-02-03 |
"1" |
Entier 1 ou booléen true |
"N/A" |
Chaîne ou null |
"0" |
Entier 0 ou booléen false |
L'inférence de types, ce n'est pas du parsing — c'est deviner l'intention à partir de preuves ambiguës.
Architecture : Inférence au Niveau de la Colonne
Inférer les types cellule par cellule n'est pas fiable. La valeur "1" seule pourrait être n'importe quoi. Mais une colonne contenant ["1", "2", "3", "4"] est clairement composée d'entiers.
Mon approche :
- Échantillonner les valeurs de la colonne (jusqu'à 100)
- Inférer le type pour chaque valeur non-nulle
- Agréger pour déterminer le type de la colonne
- Appliquer un seuil de 90 % — si 90 %+ des valeurs correspondent à un type, on l'utilise
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);
// Compter les occurrences de chaque type
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 est un sous-ensemble 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;
// Vérifier les types par ordre de priorité
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 };
}
Pourquoi 90 % ? Les données réelles ont du bruit. Une colonne de 100 entiers pourrait contenir un "N/A". Exiger 100 % de correspondance est trop strict.
Détection de Type au Niveau de la Valeur
La fonction inferValueType gère les valeurs individuelles :
function inferValueType(value) {
if (value == null || value === "") {
return DATA_TYPES.NULL;
}
const str = String(value).trim();
if (str === "") return DATA_TYPES.NULL;
// Vérification booléenne
const lowerStr = str.toLowerCase();
if (["true", "false", "yes", "no", "oui", "non", "sí", "si"].includes(lowerStr)) {
return DATA_TYPES.BOOLEAN;
}
// Vérification de date (format ISO préféré)
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;
}
// Vérification d'entier (strict)
if (/^-?\d+$/.test(str)) {
return DATA_TYPES.INTEGER;
}
// Vérification numérique (avec décimales et séparateurs)
const cleanedForNumber = str
.replace(/[$€£¥%\s]/g, "")
.replace(/,/g, ".");
if (/^-?\d+(\.\d+)?$/.test(cleanedForNumber)) {
return DATA_TYPES.NUMBER;
}
return DATA_TYPES.STRING;
}
Le Défi de la Normalisation des Nombres
Les formats numériques européens vs américains sont l'ambiguïté la plus difficile :
| Valeur | Interprétation US | Interprétation EU |
|---|---|---|
"1,234" |
1234 | 1.234 |
"1.234" |
1.234 | 1234 |
"1,234.56" |
1234.56 | Invalide |
"1.234,56" |
Invalide | 1234.56 |
Mon heuristique pour détecter le format :
function normalizeNumber(value) {
if (value == null) return value;
let str = String(value).trim();
// Supprimer les symboles monétaires et espaces
str = str.replace(/[$€£¥\s]/g, "");
// Gérer les pourcentages
const isPercent = str.endsWith("%");
if (isPercent) str = str.slice(0, -1);
// Détecter le format en analysant les séparateurs
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) {
// Entier simple : "1234"
normalized = str;
} else if (commaCount === 0 && dotCount === 1) {
// Soit "1.234" (millier EU) soit "1.23" (décimal)
// Heuristique : 3 chiffres après le point = séparateur de milliers
const afterDot = str.slice(lastDot + 1);
if (afterDot.length === 3 && /^\d+$/.test(afterDot)) {
// Probablement séparateur de milliers EU
normalized = str.replace(".", "");
} else {
// Probablement décimal
normalized = str;
}
} else if (commaCount === 1 && dotCount === 0) {
// Soit "1,234" (millier US) soit "1,23" (décimal EU)
const afterComma = str.slice(lastComma + 1);
if (afterComma.length === 3 && /^\d+$/.test(afterComma)) {
// Probablement séparateur de milliers US
normalized = str.replace(",", "");
} else {
// Probablement décimal EU
normalized = str.replace(",", ".");
}
} else if (lastDot > lastComma) {
// "1,234.56" - Format US
normalized = str.replace(/,/g, "");
} else if (lastComma > lastDot) {
// "1.234,56" - Format EU
normalized = str.replace(/\./g, "").replace(",", ".");
} else {
// Ambigu, retourner tel quel
return value;
}
const num = parseFloat(normalized);
if (Number.isNaN(num)) return value;
return isPercent ? num / 100 : num;
}
Insight clé : La position et le nombre de séparateurs résolvent la plupart des ambiguïtés. Le dernier séparateur est généralement le séparateur décimal.
Détection de Booléens (Avec Pièges)
Booléens évidents : "true", "false", "oui", "non"
Mais qu'en est-il de "0" et "1" ?
// Problématique : Ceci convertit des taux de taxe en booléens
// Colonne : [0, 0.05, 0.1, 0.2] -> [false, 0.05, 0.1, 0.2]
Ma solution : Ne pas convertir les valeurs purement numériques en booléens.
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; // Ignorer l'en-tête
return row.map(cell => {
if (cell == null) return cell;
const cellStr = String(cell).toLowerCase().trim();
// Ignorer si purement numérique (évite 0 -> false dans les colonnes numériques)
if (/^-?\d+(\.\d+)?$/.test(cellStr)) {
return cell;
}
if (trueValues.has(cellStr)) return "true";
if (falseValues.has(cellStr)) return "false";
return cell;
});
});
}
Gestion des Valeurs Nulles
Les données web utilisent de nombreuses représentations pour « pas de valeur » :
-
""(chaîne vide) -
"N/A","n/a","NA" -
"-","--" -
"null","NULL" -
"none","None" -
"."(vu dans les données gouvernementales)
Détection de null configurable :
function applyNullValues(rows, nullPatterns) {
const nullSet = new Set(nullPatterns.map(v => v.toLowerCase().trim()));
return rows.map((row, rowIndex) => {
if (rowIndex === 0) return row; // Ignorer l'en-tête
return row.map(cell => {
if (cell == null) return null;
const cellStr = String(cell).toLowerCase().trim();
return nullSet.has(cellStr) ? null : cell;
});
});
}
// Utilisation
applyNullValues(rows, ["N/A", "n/a", "-", "--", "null", "none", "."]);
Le Pipeline
Pipeline complet d'inférence de types et de nettoyage :
function cleanTable(tableInfo, config) {
let rows = cloneRows(tableInfo.rows);
// 1. Trim de toutes les chaînes
if (config.trimStrings) {
rows = applyTrimStrings(rows);
}
// 2. Convertir les motifs null en null réels
if (config.nullValues?.length) {
rows = applyNullValues(rows, config.nullValues);
}
// 3. Normaliser les booléens (avant les nombres, pour éviter 0->false)
if (config.booleans) {
rows = applyBooleanNormalization(rows, config.booleans);
}
// 4. Normaliser les nombres (gère les formats EU/US)
if (config.normalizeNumbers) {
rows = applyNumberNormalization(rows);
}
// 5. Normaliser les dates (optionnel, spécifique au format)
if (config.normalizeDates) {
rows = applyDateNormalization(rows, config.dateFormat);
}
return { ...tableInfo, rows };
}
Tester les Cas Limites
L'inférence de types a de nombreux cas limites. Je maintiens une suite de tests :
// Tests de normalisation de nombres
assertEqual(normalizeNumber("1,234.56"), 1234.56); // Format US
assertEqual(normalizeNumber("1.234,56"), 1234.56); // Format EU
assertEqual(normalizeNumber("€1.234,56"), 1234.56); // Avec devise
assertEqual(normalizeNumber("45,5%"), 0.455); // Pourcentage
assertEqual(normalizeNumber("1.234"), 1234); // Millier EU
assertEqual(normalizeNumber("1.23"), 1.23); // Décimal
// Tests booléens
assertEqual(inferValueType("Oui"), DATA_TYPES.BOOLEAN);
assertEqual(inferValueType("0"), DATA_TYPES.INTEGER); // PAS booléen
assertEqual(inferValueType("0.5"), DATA_TYPES.NUMBER); // PAS booléen
Intégration avec l'Export
Lors de l'export en SQL, utilisez les types inférés :
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";
}
});
}
Leçons Apprises
Le contexte de la colonne bat l'analyse cellulaire. Un seul
"1"est ambigu ; une colonne d'entiers ne l'est pas.Être conservateur avec les conversions. Mieux vaut laisser quelque chose en chaîne que de le corrompre avec une mauvaise conversion.
Rendre configurable. Différents domaines ont différentes valeurs nulles, représentations booléennes et formats numériques.
Tester avec des données réelles. Les tests synthétiques ratent le chaos des vrais tableaux web.
Ce système alimente le nettoyage de données dans HTML Table Exporter. Pour un exemple pratique de ces défis, découvrez comment les meilleures extensions Chrome pour exporter des tableaux gèrent ces formats.
La version PRO permet aux utilisateurs de configurer ces règles par profil d'export. En savoir plus sur gauchogrid.com/fr/html-table-exporter ou essayez-le sur le Chrome Web Store.
Vous construisez un système d'inférence de types pour un autre domaine ? J'adorerais entendre parler de vos cas limites.
Top comments (0)