DEV Community

Cover image for Détecter les Lignes d'En-Tête Automatiquement : Les Heuristiques du Parsing de Tableaux
circobit
circobit

Posted on

Détecter les Lignes d'En-Tête Automatiquement : Les Heuristiques du Parsing de Tableaux

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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)