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
- Qu'est-ce que le web scraping et de quoi se compose-t-il
- Récupérer la page : les clients HTTP
- Les bibliothèques pour parser le contenu
- Récupérer le statut de la réponse et les autres en-têtes
- Résoudre les problèmes de parsing du cyrillique
- Gérer les cookies
- Gérer HTTPS / SSL
- Utiliser des proxys
- Scraper via TOR
- Multi / multithreading et concurrence
- Stocker les URL et les files d'attente (tour d'horizon)
- Les frameworks clés en main
- Anti-bot, robots.txt, retries (ce qu'on oublie souvent)
- 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 :
- Le transport — comment obtenir les octets de la page (client HTTP ou navigateur headless).
- L'extraction — comment tirer les champs utiles du HTML/JSON (parseur DOM, sélecteurs).
- 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.
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 :
fetchne lève pas d'exception sur un 404/500 — il faut vérifier soi-mêmeres.ok.fetchn'a pas de timeout par défaut — une socket qui pend peut rester ouverte indéfiniment. AjoutezAbortSignal.timeout():
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.
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.
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 defetchavec des valeurs par défaut raisonnables (retries, timeouts).node-fetch— legacy : utile uniquement sur de très vieux Node ; sur les versions modernes, utilisez lefetchinté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.
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.
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 :
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-Afteret 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-Typeaveccharset=→ 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'unUser-Agentplausible.
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.
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 :
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 uniconv.decodeexplicite 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 (
,П) — les parseurs dignes de ce nom (Cheerio, parse5) les décodent pour vous.
6. Gérer les cookies
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
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.
6.2. Cookie jar (recommandé)
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 :
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 :
// 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.
// 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)
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 :
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 :
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-chaind'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).
// 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ésidentiel → mobile (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 :
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) :
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 :
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 :
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— unmapavec 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.
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
Setsur l'URL normalisée ; à grande échelle — un filtre de Bloom (compact, au prix de rares faux positifs), par exemplebloom-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-urlaide 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-Agentplausible, ainsi queAccept-LanguageetReferer. Une liste d'UA réels :user-agents. - Pour générer des jeux d'en-têtes et d'empreintes cohérents —
got-scrapinget 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.txtlà où c'est requis — pour le parser,robots-parseraide 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.