La première ligne d'un tableau HTML est la ligne d'en-tête.
Sauf quand ce n'est pas le cas.
Les tableaux Wikipédia ont souvent une ligne de titre couvrant toutes les colonnes avant les vrais en-têtes. Les sites de statistiques sportives ont des en-têtes groupés où « Temps de Jeu » couvre plusieurs sous-colonnes comme « MJ », « Titulaire », « Min ». Les tableaux financiers ont des lignes d'unité (« en millions USD ») qui ressemblent à des en-têtes mais n'en sont pas.
Si vous supposez que la ligne 0 est toujours l'en-tête, vos exports seront cassés pour une portion significative des tableaux réels.
Voici comment détecter la véritable ligne d'en-tête programmatiquement.
Le Problème : Trois Types de « Premières Lignes »
Considérez ces schémas courants :
Schéma 1 : Ligne de Titre
<table>
<tr>
<th colspan="4">Population Mondiale par Pays</th> <!-- Titre, pas en-tête -->
</tr>
<tr>
<th>Rang</th>
<th>Pays</th>
<th>Population</th>
<th>% du Monde</th>
</tr>
<tr>
<td>1</td>
<td>Inde</td>
<td>1 428 627 663</td>
<td>17,85%</td>
</tr>
</table>
La ligne 0 est un titre. La ligne 1 est l'en-tête. Les lignes 2+ sont les données.
Schéma 2 : En-Têtes Groupés (Deux Niveaux)
<table>
<tr>
<th></th>
<th></th>
<th colspan="3">Temps de Jeu</th>
<th colspan="2">Performance</th>
</tr>
<tr>
<th>Joueur</th>
<th>Nation</th>
<th>MJ</th>
<th>Titulaire</th>
<th>Min</th>
<th>Buts</th>
<th>PD</th>
</tr>
<tr>
<td>Jean Dupont</td>
<td>FRA</td>
<td>34</td>
<td>30</td>
<td>2700</td>
<td>12</td>
<td>8</td>
</tr>
</table>
La ligne 0 contient les en-têtes de groupe. La ligne 1 contient les vrais en-têtes de colonnes. Les lignes 2+ sont les données.
Schéma 3 : Préfixe de Navigation Wikipédia
<tr>
<th colspan="3">v t e Sites du Patrimoine Mondial</th>
</tr>
Les liens « v t e » (voir/discuter/modifier) sont la navigation de modèle de Wikipédia. Ils doivent être supprimés, et la ligne pourrait être un titre plutôt qu'un en-tête.
Heuristique 1 : Détecter les Lignes de Titre
Une ligne de titre a typiquement :
- Une seule cellule (ou très peu)
- Un grand colspan couvrant la plupart/toutes les colonnes
- Un contenu textuel qui ressemble à un titre, pas à des noms de colonnes
function isTitleRow(row, totalColumns) {
if (!row || row.length === 0) return false;
// Compter les cellules non vides
const nonEmptyCells = row.filter(cell => cell && cell.trim()).length;
// Les lignes de titre ont généralement 1-2 cellules non vides
if (nonEmptyCells > 2) return false;
// Vérifier si la première cellule couvre la plupart des colonnes (indique un colspan)
// Dans une matrice normalisée, cela se traduit par des valeurs répétées
const firstValue = row[0];
const repeatedCount = row.filter(cell => cell === firstValue).length;
// Si la première valeur se répète sur >50% des colonnes, c'est probablement un titre colspan
if (repeatedCount > totalColumns * 0.5) {
return true;
}
return false;
}
Heuristique 2 : Ce Qui Fait qu'une Ligne « Ressemble à des En-Têtes »
Les lignes d'en-tête ont des caractéristiques qui les distinguent des lignes de données :
function rowLooksLikeHeaders(row) {
if (!row || row.length === 0) return false;
let numericCells = 0;
let textCells = 0;
let emptyCells = 0;
for (const cell of row) {
const value = (cell || "").trim();
if (!value) {
emptyCells++;
} else if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
// Nombre pur ou pourcentage
numericCells++;
} else {
textCells++;
}
}
const totalNonEmpty = numericCells + textCells;
if (totalNonEmpty === 0) return false;
// Les en-têtes sont principalement du texte, pas des nombres
// Si >70% des cellules non vides sont numériques, c'est probablement des données
if (numericCells / totalNonEmpty > 0.7) {
return false;
}
// Les en-têtes ne devraient pas être majoritairement vides
if (emptyCells / row.length > 0.7) {
return false;
}
return true;
}
Heuristique 3 : Ce Qui Fait qu'une Ligne « Ressemble à des Données »
La vérification inverse aide à confirmer qu'on a trouvé la bonne frontière :
function rowLooksLikeData(row) {
if (!row || row.length === 0) return false;
let numericCells = 0;
let dateCells = 0;
let totalNonEmpty = 0;
for (const cell of row) {
const value = (cell || "").trim();
if (!value) continue;
totalNonEmpty++;
// Vérifier les patterns numériques
if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
numericCells++;
}
// Vérifier les patterns de dates
if (/^\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4}$/.test(value)) {
dateCells++;
}
}
if (totalNonEmpty === 0) return false;
// Les lignes de données contiennent typiquement du contenu numérique ou des dates
const dataLikeCells = numericCells + dateCells;
return dataLikeCells / totalNonEmpty > 0.3;
}
Heuristique 4 : Détecter les En-Têtes de Colonnes Groupés
Les tableaux style FBREF ont une ligne d'en-tête de groupe suivie d'une ligne de sous-en-têtes. La ligne de groupe a :
- Des cellules vides au début (colonnes sans groupes)
- Des valeurs répétées dues à l'expansion des colspan
- Plusieurs valeurs uniques non vides (pas une seule comme un titre)
function detectGroupHeaderRow(row, nextRow) {
if (!row || !nextRow || row.length < 4) return false;
// Les lignes d'en-tête de groupe DOIVENT avoir des cellules vides au début
// Cela les distingue des tableaux dupliqués horizontalement
const firstCellEmpty = !(row[0] || "").trim();
if (!firstCellEmpty) return false;
// Compter les valeurs uniques non vides
const uniqueValues = new Set(
row.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
);
// Une ligne de titre a exactement UNE valeur unique
// Une ligne d'en-tête de groupe doit avoir PLUSIEURS valeurs uniques
if (uniqueValues.size <= 1) return false;
// Compter les répétitions consécutives (indique l'expansion de colspan)
let consecutiveRepeats = 0;
for (let i = 1; i < row.length; i++) {
const curr = (row[i] || "").trim();
const prev = (row[i - 1] || "").trim();
if (curr === prev) consecutiveRepeats++;
}
const repeatRatio = consecutiveRepeats / (row.length - 1);
// Un ratio de répétition élevé (>30%) suggère une expansion de colspan
// La ligne suivante devrait avoir plus de valeurs uniques (les vrais sous-en-têtes)
const nextUniqueValues = new Set(
nextRow.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
);
return repeatRatio > 0.3 && nextUniqueValues.size > uniqueValues.size;
}
Heuristique 5 : Nettoyer les Préfixes de Navigation Wikipédia
Les modèles Wikipédia préfixent souvent le contenu avec « v t e » (liens vers voir/discuter/modifier le modèle) :
function cleanWikipediaNavPrefix(text) {
if (!text) return text;
// Pattern 1 : "v t e " au début (séparés par des espaces)
// Pattern 2 : "v | t | e " (séparés par des barres)
// Pattern 3 : "[v] [t] [e] " (séparés par des crochets)
return text
.replace(/^\s*v\s+t\s+e\s+/i, "")
.replace(/^\s*v\s*\|\s*t\s*\|\s*e\s+/i, "")
.replace(/^\s*\[v\]\s*\[t\]\s*\[e\]\s+/i, "")
.trim();
}
Assembler le Tout : L'Algorithme de Détection
function detectHeaderRowIndex(matrix) {
if (!matrix || matrix.length < 2) return 0;
const totalColumns = matrix[0]?.length || 0;
for (let i = 0; i < Math.min(matrix.length - 1, 5); i++) {
const currentRow = matrix[i];
const nextRow = matrix[i + 1];
// Ignorer les lignes de titre
if (isTitleRow(currentRow, totalColumns)) {
continue;
}
// Vérifier les en-têtes groupés (deux niveaux)
if (detectGroupHeaderRow(currentRow, nextRow)) {
// La ligne de sous-en-têtes (i+1) est le vrai en-tête
return i + 1;
}
// Vérifier si cette ligne ressemble à des en-têtes et la suivante à des données
if (rowLooksLikeHeaders(currentRow) && rowLooksLikeData(nextRow)) {
return i;
}
}
// Par défaut : supposer que la ligne 0 est l'en-tête
return 0;
}
Tests en Conditions Réelles
Ces heuristiques ont été développées en testant contre :
- Les tableaux pays/population de Wikipédia (lignes de titre + préfixes « v t e »)
- Les statistiques de joueurs FBREF (en-têtes groupés)
- Les tableaux financiers avec lignes d'unité
- Les tableaux de données gouvernementales avec plusieurs niveaux d'en-têtes
Aucune heuristique n'est parfaite. L'objectif est de gérer correctement les schémas courants et d'échouer gracieusement sur les tableaux inhabituels.
Quand la Détection Échoue
Pour les tableaux qui ne correspondent pas aux schémas courants, fournissez un remplacement manuel :
function extractTable(matrix, options = {}) {
const headerRowIndex = options.headerRowIndex ?? detectHeaderRowIndex(matrix);
const headerRow = matrix[headerRowIndex];
const dataRows = matrix.slice(headerRowIndex + 1);
return { headerRow, dataRows };
}
Les utilisateurs qui connaissent leurs données peuvent spécifier la ligne d'en-tête explicitement.
Résumé
| Schéma | Méthode de Détection |
|---|---|
| Ligne de titre | Cellule unique avec grand colspan |
| En-tête standard | Ligne avec principalement du texte, suivie d'une ligne avec des nombres |
| En-têtes groupés | Premières cellules vides + valeurs répétées + plus de valeurs uniques dans la ligne suivante |
| Navigation Wikipédia | Pattern de préfixe « v t e » |
L'idée clé : les en-têtes et les données ont des caractéristiques différentes. Les en-têtes sont riches en texte avec des libellés descriptifs. Les données sont riches en nombres avec des valeurs réelles. La frontière entre les deux est généralement détectable.
Pour en savoir plus sur les défis spécifiques des tableaux Wikipédia, consultez notre guide sur les scraper de tableaux HTML pour Chrome.
Besoin de détection automatique des en-têtes sans écrire de code ? En savoir plus sur gauchogrid.com/fr/html-table-exporter ou essayez-le gratuitement sur le Chrome Web Store.
Top comments (0)