DEV Community

Cover image for Automatiser l'Export de Tableaux Web avec JavaScript : Guide Pratique
circobit
circobit

Posted on

Automatiser l'Export de Tableaux Web avec JavaScript : Guide Pratique

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

Ç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>, ou display: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;
}
Enter fullscreen mode Exit fullscreen mode

Maintenant un tableau comme celui-ci :

<table>
  <tr><td rowspan="2">A</td><td>B</td></tr>
  <tr><td>C</td></tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Devient correctement :

[
  ["A", "B"],
  ["A", "C"]  // "A" apparaît dans les deux lignes
]
Enter fullscreen mode Exit fullscreen mode

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

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

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

Cela gère correctement le cas cauchemardesque :

toCSV([['Dire "Bonjour, Monde"', "Normal"]])
// '"Dire ""Bonjour, Monde""",Normal'
Enter fullscreen mode Exit fullscreen mode

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

Entrée :

| Nom du Produit | Prix (€) |
|----------------|----------|
| Widget         | 29,99    |
Enter fullscreen mode Exit fullscreen mode

Sortie :

[
  {
    "nom_du_produit": "Widget",
    "prix": "29,99"
  }
]
Enter fullscreen mode Exit fullscreen mode

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

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

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)