Les tableaux HTML semblent simples. <table>, <tr>, <td>. Qu'est-ce qui pourrait mal tourner ?
Après avoir créé HTML Table Exporter, un outil d'export de tableaux qui a traité des milliers de tableaux réels, je peux vous dire : beaucoup de choses. Cet article couvre les cas limites qui cassent les parseurs naïfs et comment les gérer.
Le cas faussement simple
Un tableau parfait ressemble à ceci :
<table>
<thead>
<tr>
<th>Nom</th>
<th>Chiffre d'affaires</th>
</tr>
</thead>
<tbody>
<tr>
<td>Acme Inc</td>
<td>1,2 M€</td>
</tr>
</tbody>
</table>
Le parser est trivial :
const rows = table.querySelectorAll('tr');
const data = [...rows].map(row =>
[...row.querySelectorAll('td, th')].map(cell => cell.textContent.trim())
);
Terminé, non ? Pas du tout.
Problème 1 : Les cellules fusionnées (colspan/rowspan)
Les tableaux réels ont des cellules fusionnées. Beaucoup.
<tr>
<td rowspan="3">T1 2024</td>
<td>Janvier</td>
<td>100 k€</td>
</tr>
<tr>
<td>Février</td>
<td>120 k€</td>
</tr>
<tr>
<td>Mars</td>
<td>90 k€</td>
</tr>
Si vous parsez naïvement, vous obtenez :
Ligne 1 : ["T1 2024", "Janvier", "100 k€"]
Ligne 2 : ["Février", "120 k€"] // Première colonne manquante !
Ligne 3 : ["Mars", "90 k€"] // Première colonne manquante !
La solution : construire une matrice de positions
Vous devez suivre quelles cellules sont « occupées » par des rowspans des lignes précédentes :
function parseTableWithMergedCells(table) {
const rows = table.querySelectorAll('tr');
const matrix = [];
const rowspanTracker = [];
rows.forEach((row, rowIndex) => {
matrix[rowIndex] = [];
let colIndex = 0;
while (rowspanTracker[colIndex] > 0) {
matrix[rowIndex][colIndex] = matrix[rowIndex - 1]?.[colIndex] || '';
rowspanTracker[colIndex]--;
colIndex++;
}
row.querySelectorAll('td, th').forEach(cell => {
while (rowspanTracker[colIndex] > 0) {
matrix[rowIndex][colIndex] = matrix[rowIndex - 1]?.[colIndex] || '';
rowspanTracker[colIndex]--;
colIndex++;
}
const colspan = parseInt(cell.getAttribute('colspan')) || 1;
const rowspan = parseInt(cell.getAttribute('rowspan')) || 1;
const value = cell.textContent.trim();
for (let c = 0; c < colspan; c++) {
matrix[rowIndex][colIndex] = value;
if (rowspan > 1) {
rowspanTracker[colIndex] = rowspan - 1;
}
colIndex++;
}
});
});
return matrix;
}
C'est simplifié — l'implémentation réelle doit gérer les rowspans imbriqués dans les colspans, ce qui devient vite compliqué.
Problème 2 : Les tableaux qui ne sont pas des tableaux de données
Chaque <table> ne contient pas forcément des données. Beaucoup de sites (oui, encore en 2024) utilisent des tableaux pour la mise en page :
<table>
<tr>
<td><nav>Menu ici</nav></td>
<td><main>Contenu ici</main></td>
</tr>
</table>
Ou pour les formulaires :
<table>
<tr>
<td><label>Email :</label></td>
<td><input type="email"></td>
</tr>
</table>
La solution : des heuristiques
J'utilise plusieurs signaux pour détecter les « vrais » tableaux de données :
function isDataTable(table) {
const rows = table.querySelectorAll('tr');
const cells = table.querySelectorAll('td, th');
if (rows.length < 2 || cells.length < 4) return false;
if (table.querySelector('input, select, textarea, button')) return false;
const links = table.querySelectorAll('a');
const textContent = table.textContent.length;
const linkText = [...links].reduce((sum, a) => sum + a.textContent.length, 0);
if (linkText / textContent > 0.7) return false;
const colCounts = [...rows].map(row =>
row.querySelectorAll('td, th').length
);
const variance = Math.max(...colCounts) - Math.min(...colCounts);
if (variance > 3) return false;
return true;
}
Aucune de ces heuristiques n'est parfaite. Il y aura toujours des cas limites.
Problème 3 : Le contenu caché
Les cellules contiennent souvent plus que le texte visible :
<td>
<span class="value">1 234</span>
<span class="sort-key" style="display:none">1234</span>
</td>
Wikipédia fait ça fréquemment pour les tableaux triables. Si vous récupérez simplement textContent, vous obtenez « 1 234 1234 ».
La solution : extraire uniquement le texte visible
function getVisibleText(element) {
const clone = element.cloneNode(true);
clone.querySelectorAll(
'[style*="display: none"], [style*="display:none"], .hidden, [hidden]'
).forEach(el => el.remove());
return clone.textContent.trim();
}
Problème 4 : Les nombres qui ne sont pas des nombres
« 1 234,56 € » est un nombre. « $1,234.56 » aussi (format US). Tout comme « (1 234) » (négatif comptable). Ou « 1,234 M » (avec suffixe).
Votre tableur a besoin de vrais nombres pour faire des calculs.
La solution : un parsing sensible aux locales
function parseNumber(value) {
if (!value || typeof value !== 'string') return value;
let cleaned = value.replace(/[$€£¥₹\s]/g, '').trim();
if (cleaned.startsWith('(') && cleaned.endsWith(')')) {
cleaned = '-' + cleaned.slice(1, -1);
}
const suffixes = { 'K': 1e3, 'M': 1e6, 'B': 1e9, 'T': 1e12 };
const suffixMatch = cleaned.match(/([0-9.,]+)\s*([KMBT])$/i);
if (suffixMatch) {
cleaned = suffixMatch[1];
var multiplier = suffixes[suffixMatch[2].toUpperCase()];
}
const lastComma = cleaned.lastIndexOf(',');
const lastDot = cleaned.lastIndexOf('.');
if (lastComma > lastDot && lastComma > cleaned.length - 4) {
cleaned = cleaned.replace(/\./g, '').replace(',', '.');
} else {
cleaned = cleaned.replace(/,/g, '');
}
let num = parseFloat(cleaned);
if (multiplier) num *= multiplier;
return isNaN(num) ? value : num;
}
Ça gère environ 90 % des cas. Les 10 % restants vous surprendront.
Problème 5 : L'enfer de l'encodage des caractères
Vous pensiez que l'UTF-8 avait résolu ça. Ce n'est pas le cas.
Les tableaux réels contiennent :
- Des espaces insécables (
/\u00A0) qui ressemblent à des espaces mais n'en sont pas - Des caractères de largeur zéro qui cassent les comparaisons de chaînes
- Des caractères Windows-1252 convertis en UTF-8 de manière incorrecte
- Des emojis qui cassent les anciens parseurs
- Des marques droite-à-gauche dans les tableaux multilingues
La solution : tout normaliser
function normalizeText(text) {
return text
.normalize('NFC')
.replace(/\u00A0/g, ' ')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim();
}
Et lors de l'export en CSV pour Excel, ajoutez le BOM UTF-8 :
const BOM = '\uFEFF';
const csvContent = BOM + generateCSV(data);
Sans le BOM, Excel peut interpréter votre fichier UTF-8 comme du Windows-1252 et massacrer les caractères spéciaux.
Problème 6 : Les tableaux imbriqués
Oui, des tableaux dans des tableaux. Généralement pour la mise en page, mais parfois pour des données :
<table>
<tr>
<td>Produit A</td>
<td>
<table>
<tr><td>Taille S</td><td>10 €</td></tr>
<tr><td>Taille M</td><td>12 €</td></tr>
</table>
</td>
</tr>
</table>
La solution : choisir votre stratégie
Options :
- Aplatir : convertir le tableau imbriqué en texte (« Taille S : 10 €, Taille M : 12 € »)
- Extraire séparément : traiter les tableaux imbriqués comme des exports distincts
- Étendre les lignes : créer plusieurs lignes parentes, une par ligne imbriquée
J'ai opté pour l'option 2 (extraction séparée) avec l'option 1 en repli pour les cas profondément imbriqués. Il n'y a pas de réponse parfaite — ça dépend du cas d'usage.
La réalité
Après avoir géré tous ces cas, mon parseur de tableaux fait environ 800 lignes de JavaScript. Et il ne gère toujours pas tout parfaitement.
Quelques vérités :
- Aucun parseur n'est parfait. Le HTML du monde réel est chaotique.
- Les heuristiques échouent. Il faut toujours prévoir des solutions de repli pour les utilisateurs.
- Les performances comptent. Certaines pages ont 50+ tableaux. Le parsing doit être rapide.
- Les cas limites sont infinis. Livrez quelque chose qui fonctionne pour 95 % des cas, puis itérez.
Outils et ressources
Si vous construisez quelque chose de similaire :
- SheetJS (xlsx) — Bibliothèque solide pour générer des fichiers Excel
- Papa Parse — Parsing et génération de CSV rapides
-
Chrome DevTools —
$('table')dans la console pour inspecter rapidement les tableaux
Ou si vous devez simplement exporter des tableaux sans rien construire : j'ai créé HTML Table Exporter précisément parce que j'en avais assez d'écrire des scrapers ponctuels. Il gère tous les cas limites ci-dessus.
Pour une comparaison des meilleurs outils de scraping de tableaux, consultez notre guide sur les scrapers de tableaux HTML pour Chrome.
En savoir plus sur gauchogrid.com/fr/html-table-exporter ou essayez-la gratuitement sur le Chrome Web Store.
Quels cas limites bizarres avez-vous rencontrés avec les tableaux ? Je suis toujours à la recherche de nouveaux cas de test pour casser mon parseur.
Top comments (0)