Vous avez trouvé un tableau sur un site web. Vous avez besoin de ces données dans un tableur. L'approche évidente — copier, coller, nettoyer dans Excel — fonctionne une fois. Mais que faire si vous avez besoin de ces données chaque semaine ? Ou depuis 50 pages différentes ?
Ce guide vous montre comment extraire des tableaux HTML programmatiquement avec JavaScript, gérer les cas limites qui cassent les approches naïves, et exporter dans des formats que vos outils acceptent réellement.
L'Approche Naïve (Et Pourquoi Elle Échoue)
L'extraction la plus simple ressemble à ça :
function extractTable(table) {
return Array.from(table.rows).map(row =>
Array.from(row.cells).map(cell => cell.textContent.trim())
);
}
Ça fonctionne pour les tableaux simples. Ça casse immédiatement quand vous rencontrez :
- Rowspan/colspan — Des cellules qui couvrent plusieurs lignes ou colonnes
- Tables imbriquées — Des tableaux à l'intérieur de cellules
-
Contenu caché — Éléments
<style>,<script>, oudisplay:none - Caractères spéciaux — Sauts de ligne, tabulations et guillemets dans le contenu des cellules
Corrigeons chaque problème.
Gérer Rowspan et Colspan
Quand une cellule a rowspan="2", elle occupe de l'espace dans la ligne actuelle ET la suivante. Un extracteur naïf voit moins de cellules que prévu et désaligne les colonnes.
La solution : construire une grille virtuelle qui suit les positions occupées.
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
let colIndex = 0;
Array.from(rowEl.cells).forEach(cell => {
// Sauter les colonnes déjà occupées par des rowspans précédents
while (grid[rowIndex][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 rectangulaire que cette cellule occupe
for (let r = 0; r < rowSpan; r++) {
const targetRow = rowIndex + r;
if (!grid[targetRow]) grid[targetRow] = [];
for (let c = 0; c < colSpan; c++) {
const targetCol = colIndex + c;
if (grid[targetRow][targetCol] === undefined) {
grid[targetRow][targetCol] = text;
}
}
}
colIndex += colSpan;
});
});
return grid;
}
Maintenant un tableau comme celui-ci :
<table>
<tr><td rowspan="2">A</td><td>B</td></tr>
<tr><td>C</td></tr>
</table>
Devient correctement :
[
["A", "B"],
["A", "C"] // "A" apparaît dans les deux lignes
]
Extraire du Texte Propre
textContent récupère tout — y compris les règles CSS dans les balises <style> et le JavaScript dans les balises <script> que certaines pages injectent dans les cellules.
Une extraction propre nécessite du filtrage :
function extractCellText(cell) {
if (!cell) return "";
// Cloner pour ne pas modifier le DOM
const clone = cell.cloneNode(true);
// Supprimer les éléments invisibles
const invisibleSelectors = "style, script, noscript, template, link";
clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());
// Normaliser les espaces
return (clone.textContent || "").replace(/\s+/g, " ").trim();
}
Détecter les Tables Imbriquées
Quand un tableau contient un autre tableau dans une cellule, vous voulez typiquement les données du tableau extérieur, pas un fouillis récursif.
La détection est simple :
function isNestedTable(table, allTables) {
let parent = table.parentElement;
while (parent) {
if (parent.tagName === "TABLE") {
return true; // Ce tableau est à l'intérieur d'un autre
}
parent = parent.parentElement;
}
return false;
}
// Filtrer en scannant une page
const allTables = document.querySelectorAll("table");
const topLevelTables = Array.from(allTables)
.filter(t => !isNestedTable(t, allTables));
Conversion en CSV
Le CSV semble simple jusqu'à ce que vous deviez gérer :
- Les virgules dans les valeurs
- Les guillemets dans les valeurs
- Les sauts de ligne dans les valeurs
L'approche conforme RFC 4180 :
function toCSV(rows, delimiter = ",") {
return rows.map(row =>
row.map(cell => {
if (cell == null) cell = "";
const str = String(cell);
// Mettre entre guillemets si contient le délimiteur, des guillemets ou des sauts de ligne
const needsQuotes = str.includes(delimiter) || /["\r\n]/.test(str);
const escaped = str.replace(/"/g, '""');
return needsQuotes ? `"${escaped}"` : escaped;
}).join(delimiter)
).join("\r\n");
}
Cela gère correctement le cas cauchemardesque :
toCSV([['Dire "Bonjour, Monde"', "Normal"]])
// '"Dire ""Bonjour, Monde""",Normal'
Pour un guide complet sur les exports CSV, consultez Scraper de Tableaux HTML : Les Meilleures Extensions Chrome.
Conversion en JSON
Pour l'export JSON, la première ligne devient les clés :
function toJSON(rows) {
if (rows.length < 2) return "[]";
const headers = rows[0].map((h, i) => sanitizeKey(h, i));
const dataRows = rows.slice(1);
const objects = dataRows.map(row => {
const obj = {};
headers.forEach((key, i) => {
obj[key] = row[i] ?? "";
});
return obj;
});
return JSON.stringify(objects, null, 2);
}
function sanitizeKey(header, index) {
let key = (header || "").toString().trim();
if (!key) return `col_${index + 1}`;
// Normaliser en snake_case minuscule
return key
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // Supprimer les accents
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
}
Entrée :
| Nom du Produit | Prix (€) |
|----------------|----------|
| Widget | 29,99 |
Sortie :
[
{
"nom_du_produit": "Widget",
"prix": "29,99"
}
]
Déclencher le Téléchargement
Dans un contexte navigateur, vous pouvez déclencher un téléchargement sans serveur :
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
// Utilisation
const csv = toCSV(extractTableMatrix(table));
downloadFile(csv, "donnees.csv", "text/csv;charset=utf-8");
Assembler le Tout
Voici un bookmarklet minimal qui exporte le premier tableau sur n'importe quelle page :
javascript:(function(){
const table = document.querySelector("table");
if (!table) { alert("Aucun tableau trouvé"); return; }
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, ri) => {
if (!grid[ri]) grid[ri] = [];
let ci = 0;
Array.from(rowEl.cells).forEach(cell => {
while (grid[ri][ci] !== undefined) ci++;
const text = cell.textContent.trim();
const rs = parseInt(cell.rowSpan) || 1;
const cs = parseInt(cell.colSpan) || 1;
for (let r = 0; r < rs; r++) {
if (!grid[ri+r]) grid[ri+r] = [];
for (let c = 0; c < cs; c++) {
if (grid[ri+r][ci+c] === undefined) grid[ri+r][ci+c] = text;
}
}
ci += cs;
});
});
return grid;
}
const data = extractTableMatrix(table);
const csv = data.map(row =>
row.map(c => c.includes(",") ? `"${c}"` : c).join(",")
).join("\n");
const blob = new Blob([csv], {type: "text/csv"});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "tableau.csv";
link.click();
})();
Quand Utiliser une Extension Navigateur à la Place
Ce code fonctionne, mais le maintenir sur différents sites est fastidieux. Si vous extrayez des tableaux régulièrement, une extension navigateur gère :
- Plusieurs tableaux par page
- Sélection de format (CSV, JSON, Excel)
- Nettoyage de données (normalisation des nombres, gestion des nulls)
- Sélection et réorganisation des colonnes
J'ai créé HTML Table Exporter pour exactement ce workflow. Les algorithmes principaux sont similaires à ce qui est montré ici, packagés dans une UI utilisable.
En savoir plus sur gauchogrid.com/fr/html-table-exporter ou essayez-le gratuitement sur le Chrome Web Store.
Des questions sur les cas limites de l'extraction de tableaux ? Laissez un commentaire ; j'ai probablement déjà rencontré le problème.
Top comments (0)