Un Web Crawler avec Puppeteer

Introduction

L’exploration automatique de sites web (web crawling) est une méthode puissante pour extraire et structurer des données de manière automatisée. Grâce à Puppeteer, un framework basé sur Chromium, il est possible de naviguer sur des sites dynamiques, d’interagir avec des éléments (comme des boutons et des liens) et de télécharger des fichiers.

Dans cet article, nous verrons comment créer un crawler intelligent qui :

  • Explore un site web en suivant les liens internes et externes.
  • Télécharge des documents (PDF, Word, Excel, etc.).
  • Organise les fichiers en respectant l’arborescence du site.
  • Gère les erreurs et optimise les performances.

Pourquoi utiliser Puppeteer pour le Web Scraping ?

Contrairement aux requêtes HTTP classiques avec axios ou fetch, Puppeteer simule un véritable navigateur et permet :

D’exécuter du JavaScript (essentiel pour les sites SPA – Single Page Application).
De cliquer sur des boutons dynamiques (fa-eye, fa-external-link-alt).
De contourner certaines protections contre les crawlers (ex. chargement différé, AJAX, authentification).
D’automatiser le téléchargement de fichiers.


Définition des objectifs

Nous allons créer un crawler en Node.js capable de :

  1. Explorer le site de manière récursive, en respectant une profondeur maximale.
  2. Cliquer sur des boutons dynamiques (fa-eye, fa-external-link-alt, routerLink).
  3. Télécharger et organiser les fichiers dans un répertoire local.
  4. Gérer les erreurs et optimiser le temps d’exécution.

Mettre à jour Node.js sous Ubuntu

Sous Ubuntu, vous pouvez mettre à jour Node.js en utilisant NVM (recommandé) ou en installant la dernière version depuis les dépôts officiels.


1. Vérifier votre version actuelle de Node.js

Avant de mettre à jour, vérifiez votre version actuelle en exécutant :

node -v

Si la version affichée est inférieure à 16.x, vous devez la mettre à jour.


2. Mettre à jour Node.js avec NVM (recommandé)

Si vous utilisez NVM (Node Version Manager), c’est la méthode la plus simple et flexible :

  1. Installer NVM (si ce n’est pas encore fait) curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash source ~/.bashrc
  2. Installer la dernière version stable de Node.js nvm install 18 nvm use 18
  3. Vérifier que la nouvelle version est bien utilisée node -v
  4. Définir cette version comme la version par défaut nvm alias default 18

3. Mettre à jour Node.js via les dépôts officiels

Si vous préférez installer Node.js directement depuis les dépôts, voici la procédure :

  1. Ajouter le dépôt officiel de Node.js (pour la version 18) curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
  2. Installer Node.js et npm sudo apt install -y nodejs
  3. Vérifier que Node.js et npm sont bien installés node -v npm -v

4. Nettoyer l’ancienne version et forcer la mise à jour

Si vous avez déjà une version obsolète installée, vous pouvez la supprimer avant d’installer la nouvelle :

sudo apt remove -y nodejs npm
sudo apt autoremove -y

Puis relancez l’installation avec la méthode de votre choix.


5. Exécuter votre script après la mise à jour

Une fois Node.js mis à jour en version 18+, essayez de relancer votre script :

node aspire.mjs

Si tout fonctionne bien, le problème de timers/promises sera résolu ! 🚀

Installation de Puppeteer

Tout d’abord, il faut installer Puppeteer dans un projet Node.js.

npm init -y
npm install puppeteer website-scraper website-scraper-puppeteer website-scraper-existing-directory

Si vous souhaitez éviter le téléchargement automatique de Chromium (gain de place), utilisez :

npm install puppeteer-core

Dans ce cas, il faudra spécifier le chemin d’un navigateur Chromium installé.

Voici un aperçu des packages installés :

  • puppeteer : Permet d’automatiser l’accès au site Web, y compris la gestion de l’authentification et l’extraction des liens.
  • website-scraper : Gère l’aspiration et l’enregistrement des pages Web sur votre disque.
  • website-scraper-puppeteer : Plugin qui permet à website-scraper d’utiliser Puppeteer pour interagir avec des sites dynamiques.
  • website-scraper-existing-directory : Plugin qui permet de stocker les fichiers téléchargés dans un dossier existant sans conflit.

2. Mise en place du Web Crawler

Voici le code de base de notre crawler Puppeteer :

2.1 Configuration du projet

On définit :

  • L’URL du site cible (anonymisée pour respecter la confidentialité).
  • Le répertoire de stockage des fichiers.
  • Le nombre maximal de niveaux d’exploration.
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';

const username = 'user'; // Modifier si nécessaire
const password = 'password'; // Modifier si nécessaire
const baseURL = 'https://example.com/'; // URL à crawler
const saveDirectory = './site-aspire';
const maxDepth = 5; // Profondeur d'exploration maximale

if (!fs.existsSync(saveDirectory)) {
  fs.mkdirSync(saveDirectory, { recursive: true });
}

function normalizeUrl(url) {
  try {
    let u = new URL(url);
    u.hash = '';
    u.search = '';
    return u.href;
  } catch (e) {
    return null;
  }
}

2.2 Organisation du stockage

Chaque page visitée et chaque fichier téléchargé seront enregistrés dans un répertoire structuré.

Exemple d’organisation :

site-aspire/
└── example.com/
    ├── doc-fonc/
    │   ├── gfc/
    │   │   ├── lien1-index.html
    │   │   ├── lien1-document1.pdf
    │   │   ├── lien1-document2.docx
    │   │   ├── lien2-index.html
    │   │   └── lien2-document1.xlsx

📌 Convention des noms de fichiers :

  • lienX-index.html → Capture des pages HTML accessibles via fa-external-link-alt.
  • lienX-documentX.pdf → Documents PDF téléchargés via fa-eye.

2.3 Gestion de l’authentification et de l’interception des téléchargements

Certains documents ne peuvent être récupérés qu’après authentification. Pour cela, nous utilisons Basic Auth.

async function downloadDirectWithAuth(url) {
  console.log(`Téléchargement direct : ${url}`);
  const authHeader = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
  try {
    const res = await fetch(url, {
      headers: { 'Authorization': authHeader, 'User-Agent': 'Mozilla/5.0' }
    });

    if (!res.ok) {
      console.log(`Echec : ${res.status} ${res.statusText}`);
      return;
    }

    const buffer = Buffer.from(await res.arrayBuffer());
    if (buffer.length > 0) {
      const urlObj = new URL(url);
      let filePath = path.join(saveDirectory, urlObj.hostname, urlObj.pathname);
      fs.mkdirSync(path.dirname(filePath), { recursive: true });
      fs.writeFileSync(filePath, buffer);
      console.log(`Document téléchargé : ${url} -> ${filePath}`);
    }
  } catch (e) {
    console.log(`Erreur téléchargement direct: ${e}`);
  }
}

3. Exploration récursive du site

Notre fonction explorePageRecursively :

  • Clique sur les éléments interactifs (fa-eye, fa-external-link-alt, routerLink).
  • Télécharge les documents et sauvegarde les pages HTML.
  • Suit une structure de navigation récursive pour éviter les doublons.
async function explorePageRecursively(page, visitedUrls = new Set(), depth = 0) {
  if (depth > maxDepth) return;

  const currentUrl = page.url();
  if (visitedUrls.has(currentUrl)) return;
  visitedUrls.add(currentUrl);

  console.log(`Exploration : ${currentUrl} (profondeur : ${depth})`);
  saveHtml(currentUrl, await page.content());

  async function clickAndExplore(selector, label) {
    const elements = await page.$$(selector);
    for (let i = 0; i < elements.length; i++) {
      try {
        console.log(`Clic sur ${label} ${i + 1}/${elements.length}`);
        await elements[i].click();
        await page.waitForTimeout(3000);
        if (label === 'fa-external-link-alt') saveHtml(page.url(), await page.content());
        await explorePageRecursively(page, visitedUrls, depth + 1);
        await page.goto(currentUrl, { waitUntil: 'networkidle2' });
      } catch (e) {
        console.error(`Erreur clic sur ${label}:`, e);
      }
    }
  }

  await clickAndExplore('i.fa-eye', 'fa-eye');
  await clickAndExplore('i.fa-external-link-alt', 'fa-external-link-alt');

  const routerLinks = await page.$$eval('[routerlink]', elems => elems.map(el => el.getAttribute('routerlink')));
  for (const routerLink of routerLinks) {
    const fullUrl = new URL(routerLink, currentUrl).href;
    if (!visitedUrls.has(fullUrl)) {
      await page.goto(fullUrl, { waitUntil: 'networkidle2' });
      await explorePageRecursively(page, visitedUrls, depth + 1);
      await page.goto(currentUrl, { waitUntil: 'networkidle2' });
    }
  }
}

4. Lancement du crawler

Enfin, nous démarrons le crawler avec Puppeteer.

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.authenticate({ username, password });

  console.log(`Démarrage du crawl à partir de : ${baseURL}`);
  await page.goto(baseURL, { waitUntil: 'networkidle2' });
  await explorePageRecursively(page);
  
  await browser.close();
  console.log('Crawl terminé avec succès !');
})();

Exécution du Scraper

Une fois le fichier créé, vous pouvez exécuter le script avec la commande suivante :

node scraper.mjs

Pendant l’exécution :

  • Le script récupère automatiquement tous les liens Angular dynamiques.
  • Il scrape les pages découvertes et télécharge leur contenu.
  • Il stocke tous les fichiers dans le dossier site-aspire/ de manière organisée.

Conclusion

Ce crawler Puppeteer est capable : ✅ D’explorer un site dynamiquement via ses liens et boutons.
De télécharger les documents automatiquement.
De sauvegarder et structurer les pages HTML.
De gérer l’authentification et éviter les doublons.

🔎 Optimisations possibles :

  • Gérer les captcha / protections anti-bot.
  • Ajouter un mécanisme de parallélisation pour optimiser le temps d’exécution.
  • Ajouter une interface web pour monitorer les explorations.

💡 À vous de tester et d’améliorer ce crawler selon vos besoins ! 🚀