Les tableaux avec cellules fusionnées sont partout. Wikipédia les utilise. Les sites de statistiques sportives les utilisent. Les rapports financiers les utilisent.
Et ils cassent presque tous les scrapers de tableaux que j'ai vus.
Voici le problème : quand vous itérez sur les éléments <tr> et <td>, vous obtenez ce qui est dans le HTML — pas ce qui est rendu visuellement. Une cellule avec rowspan="3" apparaît une seule fois dans le HTML mais occupe trois lignes dans le résultat.
L'approche naïve échoue silencieusement. Vous obtenez des colonnes désalignées, des données manquantes et des exports qui ne ressemblent en rien au tableau original.
Voici comment corriger cela.
Le Problème : Le HTML ne Correspond Pas à la Grille
Considérez ce tableau simple avec une cellule fusionnée :
<table>
<tr>
<td rowspan="2">A</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
</tr>
</table>
Représentation visuelle :
| A | B |
| | C |
Mais si vous extrayez naïvement les cellules en itérant les lignes :
// Mauvaise approche
table.querySelectorAll('tr').forEach(row => {
const cells = row.querySelectorAll('td');
console.log(cells.map(c => c.textContent));
});
// Résultat : ["A", "B"], ["C"]
// Attendu : ["A", "B"], ["A", "C"]
La ligne 2 manque le « A » qui s'étend visuellement dedans.
La Solution : Construire une Matrice
L'approche correcte est de pré-allouer une grille 2D et de la remplir selon les attributs span.
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
const gridRow = grid[rowIndex];
let colIndex = 0;
// Sauter les colonnes déjà occupées par les rowspans des lignes précédentes
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
// Avancer au-delà des cellules occupées
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const text = cell.textContent.trim();
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
// Remplir le bloc couvert par cette cellule
for (let r = 0; r < rowSpan; r++) {
const targetRowIndex = rowIndex + r;
if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
for (let c = 0; c < colSpan; c++) {
const targetColIndex = colIndex + c;
if (grid[targetRowIndex][targetColIndex] === undefined) {
grid[targetRowIndex][targetColIndex] = text;
}
}
}
colIndex += colSpan;
});
});
return grid;
}
Maintenant le résultat correspond au tableau visuel :
const matrix = extractTableMatrix(table);
// [["A", "B"], ["A", "C"]]
Pourquoi C'est Important : Exemples Concrets
Tableaux de Pays sur Wikipédia
Wikipédia utilise les cellules fusionnées massivement. Un tableau listant les pays par région pourrait avoir :
<tr>
<td rowspan="5">Europe</td>
<td>France</td>
</tr>
<tr>
<td>Allemagne</td>
</tr>
<!-- ... -->
Sans traitement approprié, vous perdez l'association « Europe » pour les lignes 2 à 5.
Sites de Statistiques Sportives
Des sites comme FBREF utilisent à la fois rowspan ET colspan. Les tableaux de stats de joueurs ont :
- Des en-têtes groupés avec colspan (Temps de Jeu → MJ | Titulaire | Min)
- Des lignes récapitulatives avec rowspan couvrant les détails du joueur
Un scraper naïf exporte du charabia. Une approche matricielle préserve la structure.
Rapports Financiers
Les rapports trimestriels ont souvent :
<td colspan="4">2024</td>
<!-- Puis : T1, T2, T3, T4 -->
L'en-tête de l'année doit se propager aux quatre colonnes trimestrielles.
Cas Limites à Gérer
Cellules Vides au Milieu
Parfois les cellules sont légitimement vides. Ne confondez pas cela avec « pas encore rempli par un span ».
// Normaliser au même nombre de colonnes
const maxCols = grid.reduce((max, row) =>
Math.max(max, row ? row.length : 0), 0
);
const normalized = grid.map(row => {
const r = row || [];
const out = new Array(maxCols);
for (let i = 0; i < maxCols; i++) {
out[i] = r[i] != null ? r[i] : "";
}
return out;
});
Tableaux Imbriqués
Les tableaux à l'intérieur de cellules créent du bruit. La plupart du temps, vous voulez le contenu textuel du tableau imbriqué, pas une extraction récursive.
function extractCellText(cell) {
const clone = cell.cloneNode(true);
// Supprimer les tableaux imbriqués, scripts, styles
clone.querySelectorAll("table, script, style, noscript")
.forEach(el => el.remove());
return clone.textContent.replace(/\s+/g, " ").trim();
}
Colspan + Rowspan Combinés
L'algorithme gère cela naturellement — vous remplissez simplement un rectangle plus grand dans la grille.
<td rowspan="2" colspan="3">Grande Cellule</td>
Remplit un bloc 2×3 avec la même valeur.
Quand Utiliser un Outil
Écrire cela de zéro est éducatif mais chronophage. Pour un usage en production, considérez :
- HTML Table Exporter — Extension de navigateur qui gère ces cas limites automatiquement
- Pandas read_html() — Fonctionne mais peine avec les tableaux imbriqués complexes
- Beautiful Soup — Nécessite un traitement manuel des spans
L'Algorithme Complet
Voici la version production avec tous les cas limites gérés :
function extractCellText(cell) {
if (!cell) return "";
const clone = cell.cloneNode(true);
// Supprimer les éléments contenant du contenu non visible
const invisibleSelectors = "style, script, noscript, template, link";
clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());
const text = clone.textContent || "";
return text.replace(/\s+/g, " ").trim();
}
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
const gridRow = grid[rowIndex];
let colIndex = 0;
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const cells = Array.from(rowEl.cells);
cells.forEach((cell) => {
while (gridRow[colIndex] !== undefined) {
colIndex++;
}
const text = extractCellText(cell);
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
for (let r = 0; r < rowSpan; r++) {
const targetRowIndex = rowIndex + r;
if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
const targetRow = grid[targetRowIndex];
for (let c = 0; c < colSpan; c++) {
const targetColIndex = colIndex + c;
if (targetRow[targetColIndex] === undefined) {
targetRow[targetColIndex] = text;
}
}
}
colIndex += colSpan;
});
});
// Normaliser toutes les lignes à la même longueur
const maxCols = grid.reduce((max, row) =>
Math.max(max, row ? row.length : 0), 0
);
return grid.map(row => {
const r = row || [];
const out = new Array(maxCols);
for (let i = 0; i < maxCols; i++) {
out[i] = r[i] != null ? r[i] : "";
}
return out;
});
}
Résumé
| Approche | Gère les Spans | Prêt pour la Production |
|---|---|---|
| Itération naïve | ❌ | ❌ |
| Matrice avec spans | ✅ | ✅ |
| Pandas read_html | Partiel | Ça dépend |
| Extension navigateur | ✅ | ✅ |
L'idée clé : pensez aux tableaux comme des grilles, pas comme des éléments HTML imbriqués. Construisez d'abord la grille, puis exportez.
Pour en savoir plus sur l'export de tableaux particulièrement complexes, consultez notre guide sur les meilleures extensions Chrome pour exporter des tableaux.
Besoin d'extraire des tableaux 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)