Le scraping par langage 21 min de lecture

Web scraping en JavaScript : le guide complet, du simple au complexe

Web scraping en JavaScript et Node.js : axios, cheerio, Puppeteer et Playwright — des pages simples aux sites dynamiques complexes.

ÉW
Équipe Web-Scraping.fr
Collecte de données pour votre activité
Publié le: 28 mars 2025

Un article de synthèse sur la façon de récupérer des pages avec Node.js, d'en extraire les données et de faire évoluer le tout vers un scraper de production : encodages, multithreading, proxys, TOR, SSL, cookies, en-têtes, files d'URL et pièges à éviter. Avec des liens vers les bibliothèques officielles.


Sommaire

  1. Qu'est-ce que le web scraping et de quoi se compose-t-il
  2. Récupérer la page : les clients HTTP
  3. Les bibliothèques pour parser le contenu
  4. Récupérer le statut de la réponse et les autres en-têtes
  5. Résoudre les problèmes de parsing du cyrillique
  6. Gérer les cookies
  7. Gérer HTTPS / SSL
  8. Utiliser des proxys
  9. Scraper via TOR
  10. Multi / multithreading et concurrence
  11. Stocker les URL et les files d'attente (tour d'horizon)
  12. Les frameworks clés en main
  13. Anti-bot, robots.txt, retries (ce qu'on oublie souvent)
  14. Principaux avantages et inconvénients d'une implémentation en JavaScript

1. Qu'est-ce que le web scraping et de quoi se compose-t-il

Le scraping d'un site se décompose presque toujours en trois couches indépendantes, et c'est précisément selon ces couches qu'il est commode de concevoir un scraper :

  1. Le transport — comment obtenir les octets de la page (client HTTP ou navigateur headless).
  2. L'extraction — comment tirer les champs utiles du HTML/JSON (parseur DOM, sélecteurs).
  3. L'orchestration — comment parcourir de nombreuses URL sans se faire bannir : files d'attente, concurrence, proxys, retries, déduplication.

Tout le guide procède par montée en complexité : d'abord « récupérer une seule page », à la fin — « un crawler distribué et robuste ».

Un choix important se pose dès le départ :

  • Site statique (les données sont déjà dans le HTML) → un client HTTP + un parseur DOM suffisent. Rapide, économique, des milliers de pages par minute.
  • Site dynamique (les données sont chargées par JavaScript) → il faut soit un navigateur headless (Playwright / Puppeteer), soit le reverse engineering de l'API interne du site (souvent, les données se trouvent dans un endpoint JSON et le navigateur est inutile).

Avant d'embarquer un navigateur lourd, vérifiez toujours l'onglet Network des DevTools : si la page va chercher ses données dans /api/... et reçoit du JSON, c'est lui qu'il faut parser, pas le DOM rendu.


2. Récupérer la page : les clients HTTP

2.1. Le fetch natif (Node 18+) — le choix par défaut

Depuis Node.js 18, fetch est intégré globalement ; il est stable depuis Node 21 et pris en charge dans les branches LTS 22 et 24. Sous le capot, il repose sur undici, si bien que les paquets séparés comme node-fetch ne sont plus nécessaires pour les tâches de base.

javascript
const res = await fetch('https://example.com');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();

Deux points sur lesquels tous les débutants trébuchent :

  • fetch ne lève pas d'exception sur un 404/500 — il faut vérifier soi-même res.ok.
  • fetch n'a pas de timeout par défaut — une socket qui pend peut rester ouverte indéfiniment. Ajoutez AbortSignal.timeout() :
javascript
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });

2.2. undici en direct — quand il faut une vitesse maximale

undici est précisément le « moteur » du fetch natif, mais son API bas niveau (request, pools de connexions, pipelining) dépasse plusieurs fois fetch, axios et got dans les benchmarks. Pertinent quand vous butez sur le débit.

javascript
import { request } from 'undici';
const { statusCode, headers, body } = await request('https://example.com');
const html = await body.text();

2.3. got et got-scraping — le confort + le « déguisement en navigateur »

got est un client mature avec retries intégrés, hooks, prise en charge du cookie jar et de HTTP/2.

Pour le scraping, le fork got-scraping d'Apify est plus intéressant : il génère automatiquement des en-têtes de navigateur plausibles, dans un ordre plausible, ce qui réduit la probabilité de bannissement. C'est lui qu'utilise CheerioCrawler dans Crawlee.

javascript
import { gotScraping } from 'got-scraping';
const { body } = await gotScraping({ url: 'https://example.com' });

2.4. axios — si vous voulez des intercepteurs et une API familière

axios (dépôt) reste le client le plus populaire grâce à ses intercepteurs, à sa gestion pratique des proxys et à son parsing JSON automatique. Pour le scraping, il n'est pas plus rapide que fetch, mais son écosystème (par exemple axios-retry) fait gagner du temps.

2.5. Autres options

  • ky — une fine surcouche de fetch avec des valeurs par défaut raisonnables (retries, timeouts).
  • node-fetchlegacy : utile uniquement sur de très vieux Node ; sur les versions modernes, utilisez le fetch intégré.
  • Les modules intégrés http/https — un contrôle maximal, mais beaucoup de plomberie manuelle ; en général, on ne les croise que sous le capot des agents et des proxys.

Que choisir

Scénario Recommandation
La plupart des tâches, Node 18+ le fetch natif
Des milliers de requêtes, priorité à la performance undici (request/Pool)
Camouflage des en-têtes prêt à l'emploi got-scraping
Intercepteurs, API familière, base de code legacy axios
Site dynamique avec rendu JS Playwright / Puppeteer (voir §3.5)

3. Les bibliothèques pour parser le contenu

Une fois la chaîne HTML récupérée, il faut la transformer en données. On ne parse pas le HTML avec des expressions régulières — c'est fragile et ça casse dès la première balise imbriquée. Utilisez un vrai parseur.

3.1. Cheerio — le standard pour le statique

Cheerio (dépôt) est un parseur côté serveur rapide, à l'API façon jQuery. Il n'exécute pas le JS, ne rend rien — il construit simplement l'arbre et permet de le parcourir avec des sélecteurs. Idéal en tandem avec fetch/got.

javascript
import * as cheerio from 'cheerio';

const html = await (await fetch('https://example.com/products')).text();
const $ = cheerio.load(html);

const items = $('.product-card').map((_, el) => ({
  title: $(el).find('.title').text().trim(),
  price: $(el).find('.price').text().trim(),
  url: new URL($(el).find('a').attr('href'), 'https://example.com').href,
})).get();

3.2. jsdom — un DOM presque authentique

jsdom implémente une grande partie du DOM navigateur et peut même exécuter les scripts de la page. Plus lourd que Cheerio, mais il offre les querySelectorAll et document habituels, et rend service quand il faut une API DOM plus « fidèle ».

3.3. Des alternatives légères et rapides

  • node-html-parser — très rapide, avec sélecteurs CSS.
  • htmlparser2 — parseur bas niveau en flux (Cheerio est construit dessus).
  • parse5 — parseur HTML5 rigoureusement conforme à la spécification.
  • linkedom — alternative légère à jsdom avec une API DOM.

3.4. L'extraction par « recettes »

x-ray permet de décrire l'extraction de façon déclarative (sélecteur → champ) et de suivre la pagination dans la foulée. Pratique pour les prototypes.

3.5. Le contenu dynamique : Playwright et Puppeteer

Quand le contenu est dessiné par le JS, il faut un navigateur headless :

  • Playwright (dépôt) — le favori moderne : Chromium, Firefox et WebKit sous une même API, attentes automatiques des éléments, interception des requêtes réseau, contextes pour isoler les cookies.
  • Puppeteer (dépôt) — le standard de fait pour Chrome/Chromium, un peu plus simple, avec un immense écosystème.
javascript
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const titles = await page.$$eval('h2', els => els.map(e => e.textContent.trim()));
await browser.close();

Le navigateur est l'approche la plus coûteuse en ressources : des dizaines, voire des centaines de Mo de RAM par onglet. Ne l'utilisez que lorsqu'il n'existe réellement ni HTML statique ni API interne.

Conseil pour une approche hybride : il est souvent optimal d'ouvrir la page une seule fois dans le navigateur, d'en extraire le HTML final via page.content(), puis de l'analyser avec le rapide Cheerio — vous combinez ainsi rendu JS et sélecteurs confortables.


4. Récupérer le statut de la réponse et les autres en-têtes

Le statut et les en-têtes représentent la moitié du diagnostic d'un scraper (bannissement, redirection, limite, encodage).

Avec le fetch natif :

javascript
const res = await fetch(url, { redirect: 'follow' });

res.status;        // 200, 404, 429, 503 ...
res.statusText;    // 'OK', 'Too Many Requests'
res.ok;            // true pour un code 2xx
res.redirected;    // y a-t-il eu des redirections
res.url;           // URL final après les redirections

res.headers.get('content-type');   // text/html; charset=windows-1251
res.headers.get('set-cookie');     // les cookies
res.headers.get('retry-after');    // combien attendre en cas de 429/503
[...res.headers];                  // tous les en-têtes par paires

De bonnes habitudes :

  • 429 / 503 → lisez Retry-After et appliquez un backoff au lieu de marteler le serveur.
  • 301/302/308 → décidez s'il faut suivre la redirection (redirect: 'manual' donne un contrôle manuel).
  • Content-Type avec charset= → la première et principale source de vérité sur l'encodage (voir §5).
  • Piloter les en-têtes sortants (User-Agent, Accept-Language, Referer) n'est pas moins important — beaucoup de sites rejettent les requêtes dépourvues d'un User-Agent plausible.

Dans got/axios, tout cela est disponible via response.statusCode, response.headers. Dans le navigateur, via l'interception de la réponse : page.on('response', res => res.status()).


5. Encodages et Unicode : résoudre les problèmes de parsing

Douleur classique du web russophone : la page est en windows-1251 (ou koi8-r), et vous récupérez des caractères illisibles du genre привет. La cause : res.text() décode toujours les octets en UTF-8, alors que le site les a servis dans un autre encodage.

Règle : pour les pages non UTF-8, ne prenez pas res.text(). Prenez les octets bruts (arrayBuffer) et décodez-les avec le bon encodage via iconv-lite.

javascript
import iconv from 'iconv-lite';

const res = await fetch('https://old-site.ru/');
const buf = Buffer.from(await res.arrayBuffer());

// 1) on tente de lire l'encodage dans l'en-tête Content-Type
let charset = (res.headers.get('content-type') || '').match(/charset=([^;]+)/i)?.[1];

// 2) s'il est absent de l'en-tête — on cherche dans <meta> (on décode un extrait en latin1 pour lire la balise)
if (!charset) {
  const head = iconv.decode(buf, 'latin1');
  charset = head.match(/<meta[^>]+charset=["']?([\w-]+)/i)?.[1]
         || head.match(/charset=([\w-]+)/i)?.[1];
}

charset = (charset || 'utf-8').toLowerCase().replace('windows-', 'win');

const html = iconv.decode(buf, charset); // cyrillique correct

Si l'encodage n'est déclaré nulle part, on peut le détecter de façon heuristique :

  • jschardet — port du Universal Charset Detector de Mozilla.
  • chardet — détecteur alternatif.
javascript
import jschardet from 'jschardet';
const guess = jschardet.detect(buf); // { encoding: 'windows-1251', confidence: 0.99 }

En complément :

  • Cheerio sait décoder lui-même si on lui passe un buffer et une indication : cheerio.load(buf, { decodeEntities: true }) — mais un iconv.decode explicite est plus fiable.
  • Dans un navigateur headless, le problème d'encodage ne se pose généralement pas : le navigateur décode la page lui-même, page.content() renvoie de l'UTF-8 correct.
  • N'oubliez pas les entités HTML (&nbsp;, &#1055;) — les parseurs dignes de ce nom (Cheerio, parse5) les décodent pour vous.

Les cookies servent pour les zones authentifiées, les sessions, les paniers et pour éviter l'écran de « première visite ». Il existe trois niveaux.

6.1. À la main, via les en-têtes

javascript
const res = await fetch(url, { headers: { cookie: 'sid=abc123; lang=ru' } });
const setCookie = res.headers.get('set-cookie'); // à parser puis renvoyer aux requêtes suivantes

Suffisant pour les cas simples, mais entretenir à la main le jeu de cookies entre les requêtes est pénible.

tough-cookie est l'implémentation de référence d'un magasin de cookies qui respecte domaine, chemin, durée de vie et drapeaux. De nombreux clients s'y intègrent nativement.

got accepte le jar directement et gère la session tout seul :

javascript
import got from 'got';
import { CookieJar } from 'tough-cookie';

const cookieJar = new CookieJar();
await got('https://site.ru/login', { cookieJar, method: 'POST', form: { user, pass } });
const profile = await got('https://site.ru/account', { cookieJar }); // les cookies sont injectés automatiquement

Pour axios, il existe le wrapper axios-cookiejar-support ; pour le fetch natif, il faudra brancher tough-cookie à la main, ou utiliser got/undici.

6.3. Dans le navigateur

Dans Playwright/Puppeteer, les cookies vivent dans le contexte et peuvent être sauvegardés/restaurés — pratique pour se connecter une seule fois et réutiliser la session :

javascript
// sauvegarder l'état (cookies + localStorage)
await context.storageState({ path: 'state.json' });
// le restaurer lors d'un nouveau lancement
const context = await browser.newContext({ storageState: 'state.json' });

7. Gérer HTTPS / SSL

Un site HTTPS ordinaire ne demande aucun effort — fetch/got/axios vérifient le certificat automatiquement. Les cas particuliers :

7.1. Certificats auto-signés / expirés

Il faut parfois désactiver la vérification (par exemple derrière un proxy MITM ou face à un environnement de test). Faites-le en connaissance de cause — cela supprime la protection contre l'interception du trafic.

javascript
// undici / fetch natif — via le dispatcher
import { Agent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } }));

// got / axios — via https.Agent
import https from 'node:https';
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
// got:  got(url, { agent: { https: httpsAgent } })
// axios: axios.get(url, { httpsAgent })

Le « marteau » global NODE_TLS_REJECT_UNAUTHORIZED=0 désactive la vérification pour tout le processus — mieux vaut éviter cela en production.

7.2. Certificats racines maison / certificats clients (mTLS)

javascript
import https from 'node:https';
import fs from 'node:fs';

const agent = new https.Agent({
  ca:  fs.readFileSync('./ca.pem'),     // votre propre autorité de certification
  cert: fs.readFileSync('./client.pem'), // certificat client pour le mTLS
  key:  fs.readFileSync('./client.key'),
});

7.3. Empreinte TLS (JA3) — l'anti-bot avancé

Les protections modernes (Cloudflare, DataDome) savent distinguer les clients à leur poignée de main TLS (JA3/JA4) : celle d'un client Node diffère de celle d'un vrai Chrome, ce qui trahit le bot même avec des en-têtes parfaits. Node seul ne peut pas « réparer » cela ; ce qui aide :

  • got-scraping — masque partiellement la couche des en-têtes ;
  • CycleTLS — substitution de l'empreinte TLS ;
  • un vrai navigateur headless (Playwright) — il offre une « authentique » poignée de main TLS de navigateur.

8. Utiliser des proxys

Les proxys servent à répartir la charge entre plusieurs IP, à contourner les restrictions géographiques et les bannissements par IP. Les types : HTTP, HTTPS et SOCKS5 (ce dernier est plus universel — il transporte n'importe quel trafic, DNS compris).

8.1. Le fetch natif (particularité importante en 2026 !)

Le fetch natif n'a pas l'ancienne option { agent }. Le proxy se configure via un dispatcher undici — ProxyAgent :

javascript
import { ProxyAgent, setGlobalDispatcher } from 'undici';

// globalement : tous les fetch passeront par le proxy
setGlobalDispatcher(new ProxyAgent('http://user:pass@proxy.host:8080'));
const res = await fetch('https://example.com');

// ou ponctuellement, pour une seule requête
const res2 = await fetch('https://example.com', {
  dispatcher: new ProxyAgent('http://user:pass@proxy.host:8080'),
});

Sur Node 24+, on peut activer la lecture de HTTP_PROXY/HTTPS_PROXY depuis l'environnement avec le drapeau NODE_USE_ENV_PROXY=1 (ou --use-env-proxy), mais un ProxyAgent explicite est plus fiable.

8.2. got / axios via des agents

Via les agents https-proxy-agent et socks-proxy-agent :

javascript
import got from 'got';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';

const httpsAgent  = new HttpsProxyAgent('http://user:pass@proxy:8080');
const socksAgent  = new SocksProxyAgent('socks5h://127.0.0.1:9050'); // h = DNS via le proxy

const r1 = await got('https://example.com', { agent: { https: httpsAgent } });
const r2 = await got('https://example.com', { agent: { http: socksAgent, https: socksAgent } });

8.3. Rotation et pools de proxys

À grande échelle, il faut un pool de proxys, avec rotation et mise au rebut des adresses « mortes ». La variante la plus simple : choisir un proxy aléatoire/round-robin à chaque requête. Les outils prêts à l'emploi :

  • proxy-chain d'Apify — monte un proxy local qui relaie vers l'amont (y compris avec authentification, ce qui compte pour Chromium, incapable de gérer login/mot de passe dans --proxy-server).
  • Dans Crawlee, la rotation des proxys et des sessions est intégrée (ProxyConfiguration).
javascript
// un proxy aléatoire du pool à chaque requête
const pool = ['http://p1:8080', 'http://p2:8080', 'http://p3:8080'];
const pick = () => pool[Math.floor(Math.random() * pool.length)];
await fetch(url, { dispatcher: new ProxyAgent(pick()) });

Les types de proxys par qualité : datacenter (bon marché, vite repéré) → résidentielmobile (cher, presque jamais banni). Le choix dépend de l'agressivité de la protection de la cible.


9. Scraper via TOR

TOR offre une rotation d'IP gratuite : le trafic transite par une chaîne de nœuds, et on peut changer d'IP de sortie sur commande. C'est pratique pour apprendre et pour de petites tâches, mais l'approche a de sérieuses limites (voir la fin de la section).

9.1. Configuration

TOR expose un proxy SOCKS sur le port 9050 et un port de contrôle 9051 pour le piloter. Dans le fichier de configuration torrc :

code
SocksPort 9050
ControlPort 9051
# le mot de passe s'obtient avec la commande : tor --hash-password "votre_mot_de_passe"
HashedControlPassword 16:....
CookieAuthentication 1

9.2. Les requêtes via TOR

Il suffit de pointer le client vers le SOCKS5 local (utilisez socks5h pour que le DNS soit lui aussi résolu via TOR — sinon, fuite de l'IP réelle) :

javascript
import got from 'got';
import { SocksProxyAgent } from 'socks-proxy-agent';

const agent = new SocksProxyAgent('socks5h://127.0.0.1:9050');
const res = await got('https://httpbin.org/ip', {
  agent: { http: agent, https: agent },
});
console.log(JSON.parse(res.body).origin); // IP de sortie TOR actuelle

9.3. Changer d'identité (nouvelle IP)

Pour obtenir une nouvelle IP de sortie, on envoie le signal NEWNYM au port de contrôle. Soit via la bibliothèque tor-request, soit à la main, avec une simple socket TCP sans dépendances :

javascript
import net from 'node:net';

function newTorIdentity(password = '') {
  return new Promise((resolve, reject) => {
    const socket = net.connect(9051, '127.0.0.1', () => {
      socket.write(`AUTHENTICATE "${password}"\r\nSIGNAL NEWNYM\r\nQUIT\r\n`);
    });
    socket.once('error', reject);
    socket.once('end', resolve);
    socket.resume();
  });
}

// entre les requêtes :
await newTorIdentity('votre_mot_de_passe');

Important : TOR impose un délai d'environ 10 secondes entre deux changements de circuit — impossible de changer d'IP plus souvent.

9.4. Plusieurs instances pour gagner en débit

Un TOR = une seule IP de sortie à un instant donné, et un délai de rotation lent. Pour un pool de « proxys gratuits », on lance plusieurs processus TOR sur des ports différents (9050/9051, 9052/9053, …) et on les utilise en round-robin. Une image Docker prête à l'emploi pour cela : rotating-tor-http-proxy (plusieurs instances derrière un seul endpoint HTTP via HAProxy).

9.5. Les limites (lecture obligatoire)

  • Il existe environ 1500 nœuds de sortie TOR, leurs listes sont publiques, et Cloudflare/DataDome/la plupart des systèmes anti-bots les bloquent par avance — sur des cibles protégées, TOR est presque inutile.
  • La vitesse est faible et instable, et la nouvelle IP n'est pas garantie « propre » ni fonctionnelle.
  • Cela convient pour apprendre et pour de petites cibles non protégées ; en production, prenez des proxys résidentiels/mobiles.
  • TOR est un outil de confidentialité ; utilisez-le dans le respect de la loi et des règles des sites.

10. Concurrence, parallélisme et threads

Il faut ici distinguer deux notions différentes.

10.1. D'abord, la concurrence asynchrone (et non les threads)

Le scraping est une tâche I/O-bound (on attend le réseau). Grâce à sa boucle d'événements, Node tient facilement des centaines de requêtes simultanées avec un seul thread — de vrais threads sont ici, le plus souvent, inutiles. Le danger est exactement inverse : lancer un Promise.all sur 10 000 URL d'un coup et tuer à la fois votre réseau et le serveur cible. C'est pourquoi on limite la concurrence.

p-limit — un limiteur de tâches simultanées :

javascript
import pLimit from 'p-limit';

const limit = pLimit(5); // 5 requêtes simultanées au maximum
const results = await Promise.all(
  urls.map(url => limit(() => scrape(url)))
);

Dans la même famille :

  • p-queue — file d'attente avec priorités, intervalles et rate limit (par exemple « pas plus de 10 requêtes par seconde »).
  • p-map — un map avec limitation de la concurrence.
  • bottleneck — rate limiter avancé (y compris distribué via Redis).

10.2. worker_threads — pour les traitements limités par le processeur (CPU-bound)

Si le goulot d'étranglement n'est pas le réseau mais un traitement lourd (parsing de HTML/JSON géants, regex, post-traitement), il vaut la peine de le déporter dans des threads worker_threads pour ne pas bloquer l'event loop. Enveloppe pratique : les pools comme piscina.

javascript
import { Worker } from 'node:worker_threads';
// chaque worker parse son morceau de HTML en parallèle, sans bloquer le thread principal

10.3. cluster / plusieurs processus — pour monter en charge sur les cœurs

cluster, ou simplement le lancement de N processus (souvent dans Docker), répartit la charge entre les cœurs CPU et apporte de la tolérance aux pannes. En pratique, pour un crawler, cela donne en général « plusieurs workers lisent une file commune (Redis) » — voir §11.

10.4. La mise à l'échelle automatique (auto-scaling)

Crawlee ajuste lui-même la concurrence aux CPU/RAM disponibles (AutoscaledPool) : moins de risques de tomber dans un petit conteneur, et le maximum est tiré d'un grand.

Recette pratique : pour la plupart des scrapers — fetch + p-limit/p-queue avec une limite de 5–20 requêtes simultanées. N'ajoutez des threads/processus que lorsque vous butez sur le CPU ou sur un seul processus.


11. Stocker les URL et les files d'attente (tour d'horizon)

Dès qu'un crawler parcourt plus d'une page apparaît le frontier — le front d'exploration : une file d'URL « à visiter » + un ensemble d'URL « déjà visitées ».

Les enjeux clés :

  • La déduplication. Ne jamais visiter deux fois la même URL. En mémoire — un simple Set sur l'URL normalisée ; à grande échelle — un filtre de Bloom (compact, au prix de rares faux positifs), par exemple bloom-filters.
  • La normalisation des URL. Ramenez-les à une forme canonique (tri de la query string, suppression du #, du slash final, des balises utm), sinon les « doublons » proliféreront. normalize-url aide bien.
  • La persistance. Si le processus tombe, la file ne doit pas être perdue. La mémoire ne convient pas aux tâches sérieuses.
  • Les priorités et l'ordre de parcours — en largeur (BFS) ou en profondeur (DFS), avec priorité aux sections importantes.

Où stocker :

Échelle Solution
Petit script ponctuel Set + tableau en mémoire
Worker unique redémarrable fichier / SQLite, ou la RequestQueue de Crawlee
Plusieurs workers / distribué Redis (ioredis) comme file commune + ensemble des URL visitées
File de tâches industrielle BullMQ (dépôt) au-dessus de Redis : retries, délais, priorités, concurrence

Crawlee fournit une RequestQueue persistante intégrée, avec déduplication et parcours en largeur/profondeur — si vous ne voulez pas assembler le frontier à la main, c'est la voie la plus rapide.

L'architecture type « pour de vrai » : Redis/BullMQ comme file d'URL → un pool de workers prend les tâches, parse, remet les liens découverts dans la file (après déduplication) et écrit le résultat en base/fichier.


12. Les frameworks clés en main

Si vous ne voulez pas assembler tout ce qui précède à la main :

  • Crawlee (dépôt) — le principal framework moderne pour Node.js/TS, signé Apify. Une interface unique pour le crawling HTTP et navigateur (CheerioCrawler, PuppeteerCrawler, PlaywrightCrawler), file d'URL persistante, rotation des proxys et des sessions, auto-scaling, empreintes de navigateur « humaines », retries. Les versions récentes ajoutent un crawler adaptatif (il décide lui-même si le rendu JS est nécessaire) et des capacités orientées IA. Requiert Node 16+.

```js import { CheerioCrawler } from 'crawlee';

const crawler = new CheerioCrawler({ maxConcurrency: 10, async requestHandler({ $, request, enqueueLinks, pushData }) { await pushData({ url: request.url, title: $('title').text() }); await enqueueLinks(); // trouve les liens tout seul et les met en file, avec déduplication }, }); await crawler.run(['https://example.com']); ```

  • node-crawler — un crawler plus classique, avec file d'attente, limites et Cheerio intégré.
  • x-ray — extraction déclarative + pagination.

Pour la plupart des projets JS sérieux, la réponse par défaut est aujourd'hui Crawlee.


13. Anti-bot, robots.txt, retries (ce qu'on oublie souvent)

Ces sujets ne figuraient pas dans le plan initial, mais sans eux, un scraper de production ne survit pas.

13.1. Se faire passer pour un client ordinaire

  • Définissez un User-Agent plausible, ainsi que Accept-Language et Referer. Une liste d'UA réels : user-agents.
  • Pour générer des jeux d'en-têtes et d'empreintes cohérents — got-scraping et fingerprint-suite d'Apify.
  • Face aux protections fortes (Cloudflare et consorts), seul un vrai navigateur (Playwright) ou la substitution de l'empreinte TLS (voir §7.3) vous sauve.

13.2. Politesse et retries

  • Respectez robots.txt là où c'est requis — pour le parser, robots-parser aide bien.
  • Appliquez du rate limiting et des délais aléatoires entre les requêtes (p-queue/bottleneck).
  • Sur un 429/503, respectez Retry-After, utilisez un backoff exponentiel avec jitter, limitez le nombre de tentatives.
  • Mettez en cache ce qui est déjà téléchargé, pour ne pas solliciter le site une nouvelle fois après un redémarrage.

14. Principaux avantages et inconvénients d'une implémentation en JavaScript

Avantages

  • Le même langage que la page. Les sites sont écrits en JS — sélecteurs, logique DOM et même exécution des scripts de la page restent commodément dans le même environnement.
  • Les meilleurs navigateurs headless sont nativement JS. Playwright et Puppeteer sont des citoyens de première classe de Node ; pour les sites très dynamiques, c'est un net avantage sur les autres écosystèmes.
  • L'asynchrone d'origine. La boucle d'événements épouse parfaitement le scraping I/O-bound : une forte concurrence dans un seul processus, sans se battre avec les threads.
  • Un écosystème mûr. fetch/undici, Cheerio, Crawlee, BullMQ, agents proxy prêts à l'emploi — tout est à portée de main.
  • Crawlee couvre l'« orchestration » (files, proxys, empreintes, montée en charge) presque sans code.

Inconvénients

  • Le traitement CPU-bound (documents énormes, post-traitement lourd) est le point faible d'un Node monothread ; il faut worker_threads/plusieurs processus, là où Go/Rust font plus simple.
  • La voracité des navigateurs. Playwright/Puppeteer consomment beaucoup de RAM/CPU ; à grande échelle, ce sont des coûts sensibles.
  • L'enfer des callbacks/promesses — une orchestration manuelle sans framework tourne vite au code spaghetti.
  • L'empreinte TLS. Les clients Node se font repérer au JA3/JA4 ; « réparer » cela en Node pur est plus dur qu'il n'y paraît (il faut CycleTLS ou un navigateur).
  • La fragilité des sélecteurs. C'est le mal commun du scraping (les mises en page changent), mais l'écosystème JS ne dispense pas de maintenir les sélecteurs CSS/XPath à la main.
  • La data science en aval. Python, avec pandas/numpy, est plus fort pour l'analyse ultérieure des données collectées — il est parfois plus commode de « collecter en JS, traiter en Python ».

Quand JS est un bon choix : sites dynamiques, besoin d'un navigateur headless, équipe déjà sur Node, forte concurrence I/O requise et/ou intégration avec des services web en JS. Quand envisager une alternative : un traitement purement CPU-bound de téraoctets de HTML, ou une intégration étroite avec de l'analytique Python.