Aller au contenu
Retour au blog

Cheerio Web Scraping sous Node.js : la fonction load() pour un scraper testé

Raluca PenciucDernière mise à jour le 22 min read
Cheerio Web Scraping sous Node.js : la fonction load() pour un scraper testé
En bref : Cheerio est un analyseur HTML rapide pour Node.js doté d'une API de type jQuery ; il convient au web scraping quand les données sont déjà dans le HTML brut de la page. Utilisez fetch avec Cheerio pour les pages statiques, appelez directement le JSON de la page lorsqu'elle charge ses données ainsi, et ne passez par un navigateur que si JavaScript construit le contenu. Ce guide construit un scraper testé avec pagination, relances et export JSON ou CSV.

À la fin de ce guide, vous disposerez d'un scraper Cheerio capable de charger une page, d'extraire les champs de saisie, de gérer la pagination, de réessayer poliment en cas de refus du serveur, d'enregistrer les résultats au format JSON et CSV, et de réussir un test hors ligne. Les exemples en ligne utilisent books.toscrape.com et quotes.toscrape.com, deux sites d'entraînement conçus pour le scraping, ce qui vous permet d'exécuter le code tel quel. Vous apprendrez également à repérer les pages que Cheerio ne parvient pas à lire et à corriger les erreurs « cheerio.load » qui posent le plus souvent problème. Vous débutez dans le scraping sous Node ? Commencez par consulter notre guide sur le scraping web avec JavaScript et Node.js.

Qu’est-ce que Cheerio, et quand faut-il l’utiliser pour le scraping Web ?

Cheerio est un analyseur HTML et XML rapide pour Node.js, doté d’une API de type jQuery. Il lit le balisage dont vous disposez déjà et vous permet de sélectionner des éléments à l’aide de sélecteurs CSS. Il n’exécute pas de JavaScript, ne charge pas de CSS et ne rend pas les pages ; utilisez-le donc lorsque les données dont vous avez besoin se trouvent déjà dans le code HTML brut de la page.

Considérez Cheerio comme l’opposé d’un navigateur. Vous lui fournissez une chaîne de balisage, et il construit une arborescence d’éléments que vous pouvez interroger. Rien ne s’exécute : pas de mise en page, pas de requêtes d’images ou de feuilles de style, pas de scripts ajoutant du contenu par la suite. C’est pourquoi Cheerio est rapide et léger, et pourquoi il ne trouve parfois rien sur des pages qui semblent regorger de données dans Chrome.

En coulisses, le projet Cheerio sur GitHub analyse le HTML avec parse5 et peut basculer vers htmlparser2, plus tolérant. Par-dessus se trouve l’API familière de type jQuery : $('selector'), .find(), .text(), et .attr(). Pour savoir comment il se compare à d’autres outils, consultez notre tour d’horizon des bibliothèques de web scraping pour Node.js.

Commencez par déterminer de quel type de page il s’agit :

Type de page

Que faut-il utiliser ?

Pourquoi

Données dans le code HTML brut

fetch + Cheerio

La solution la plus rapide et la moins chère. Aucune exécution n'est nécessaire

Données au format JSON demandées par la page

Appeler directement le JSON

Aucune analyse du balisage

Contenu rendu ou contrôlé par JavaScript

HTML rendu (API Scraper render_js ou API du navigateur), puis Cheerio

Cheerio ne peut pas exécuter de scripts

Une fois que vous savez que Cheerio peut lire la page, procédez à l'extraction dans cet ordre :

  • Recherchez d'abord les données structurées, telles que les blocs JSON-LD ou un objet de données à l'intérieur d'une balise script.
  • Si aucune donnée structurée n'existe, utilisez les sélecteurs CSS sur le balisage visible.
  • N’utilisez le rendu que lorsque les données n’apparaissent jamais dans le code HTML envoyé par le serveur.

Si vous hésitez entre un analyseur syntaxique et un navigateur complet, notre comparaison Cheerio vs Puppeteer passe en revue les avantages et les inconvénients de chacun.

Comment configurer un projet Cheerio sous Node.js ?

Installez une version LTS récente de Node.js, créez un projet avec `npm init`, ajoutez « type » : « module » au fichier `package.json`, puis exécutez `npm install cheerio`. Importez-le avec `import * as cheerio from 'cheerio'`. La fonction `fetch` intégrée à Node.js permet de télécharger des pages ; vous n’avez donc pas besoin d’Axios ni d’aucun autre client HTTP pour commencer.

Utilisez Node.js 22 ou une version ultérieure ; la version LTS actuelle est le meilleur choix. Vérifiez votre version, puis créez le projet :

node -v
mkdir cheerio-scraper && cd cheerio-scraper
npm init -y
npm pkg set type=module
npm install cheerio

L' .mjs extension indique à Node de traiter chaque fichier comme un module ES. Dans un projet CommonJS, const cheerio = require('cheerio') fonctionne également. Le guide d’installation de Cheerio décrit les mêmes étapes.

La fonction `fetch` intégrée à Node gère les téléchargements. Elle repose sur undici, ne nécessite aucune importation et prend en charge AbortSignal.timeout().

Vous utilisez déjà Axios ? Conservez-le. Transmettez response.data à cheerio.load() au lieu de await res.text(), et le reste de ce guide s'applique tel quel. Voici comment configurer les en-têtes dans Axios pour le scraping, ou comparer la fonction `fetch` native et ses alternatives.

Utilisation de Cheerio avec TypeScript

Cheerio fournit ses propres définitions de types ; il n'y a donc pas de @types/cheerio paquet à installer. Importez load et le CheerioAPI type, annotez vos types de retour, et le compilateur vérifie votre analyseur comme n'importe quel autre code :

// TypeScript: Cheerio ships its own types, so no @types package is needed.
import { load, type CheerioAPI } from 'cheerio';

interface Book {
  title: string;
  price: number;
}

export function parseBooks($: CheerioAPI): Book[] {
  return $('article.product_pod')
    .map((_, el) => ({
      title: $(el).find('h3 a').attr('title') ?? '',
      price: Number($(el).find('.price_color').text().replace(/[^0-9.]/g, '')),
    }))
    .get();
}

const $ = load('<article class="product_pod"><h3><a title="Sample">S</a></h3><p class="price_color">£9.99</p></article>');
console.log(parseBooks($));

Exécutez-le avec npx tsx 09-typed.ts:

[ { title: 'Sample', price: 9.99 } ]

Compiler avec tsc à la place ? Ajoutez @types/node en tant que dépendance de développement, car les définitions de types de Cheerio font référence à des éléments intégrés de Node tels que Buffer et node:stream. La logique d'analyse est identique à celle de la version JavaScript.

Comment fonctionne cheerio.load() ?

cheerio.load() analyse une chaîne HTML et renvoie une fonction $. Appelez $ avec un sélecteur CSS pour obtenir les éléments correspondants, puis lisez-les avec .text() ou .attr(). Utilisez .find() pour effectuer une recherche à l'intérieur d'un élément, .map() avec .get() pour créer des tableaux, et .next() ou .parent() pour naviguer dans l'arborescence du document.

Commencez par une petite page statique avant de vous attaquer au réseau. Elle couvre toutes les méthodes utilisées dans la suite du guide :

// Cheerio basics: load HTML, select elements, read text and attributes.
import * as cheerio from 'cheerio';

const html = `
<ul id="products">
  <li class="product" data-sku="A1"><a href="/p/a1">Desk lamp</a><span class="price">$24.00</span></li>
  <li class="product" data-sku="B2"><a href="/p/b2">Monitor arm</a><span class="price">$61.50</span></li>
  <li class="product sold-out" data-sku="C3"><a href="/p/c3">Cable tray</a><span class="price">$12.99</span></li>
</ul>`;

const $ = cheerio.load(html);

// One element: .text() and .attr()
console.log('First product:', $('li.product a').first().text());
console.log('First link:', $('li.product a').first().attr('href'));

// Many elements: .map() returns a Cheerio object, .get() turns it into an array
const products = $('li.product')
  .map((_, el) => {
    const item = $(el);
    return {
      sku: item.attr('data-sku'),
      name: item.find('a').text().trim(),
      price: Number(item.find('.price').text().replace(/[^0-9.]/g, '')),
      inStock: !item.hasClass('sold-out'),
    };
  })
  .get();
console.log(products);

// Traversal: move from one element to its neighbors
console.log('After A1:', $('li[data-sku="A1"]').next().find('a').text());
console.log('Parent id:', $('.price').first().parent().parent().attr('id'));

Le résultat de node 01-basics.mjs:

First product: Desk lamp
First link: /p/a1
[
  { sku: 'A1', name: 'Desk lamp', price: 24, inStock: true },
  { sku: 'B2', name: 'Monitor arm', price: 61.5, inStock: true },
  { sku: 'C3', name: 'Cable tray', price: 12.99, inStock: false }
]
After A1: Monitor arm
Parent id: products

Sélection d’éléments

La chaîne que vous transmettez à $() est un sélecteur CSS standard ; vous pouvez donc utiliser des noms de balises, .class, #id, les sélecteurs d’attributs tels que [data-sku="A1"], ainsi que les combinateurs tels que ul > li fonctionnent tous. Le guide MDN sur les sélecteurs CSS constitue la meilleure référence. Cheerio ajoute quelques extensions de type jQuery, telles que :contains() et :first, mais il ne prend pas en charge XPath ; voici dans quels cas chaque langage de sélecteurs est adapté.

Lecture du texte et des attributs

.text() renvoie le texte combiné de chaque élément correspondant et de ses descendants. .attr('name') renvoie un attribut du premier élément correspondant, exactement tel qu'il est écrit dans le balisage. .hasClass() vérifie la présence d’une classe, ce qui est pratique pour des indicateurs tels que « épuisé ».

.map() C’est l’étape qui déroute le plus les débutants. Dans Cheerio, elle renvoie un autre objet Cheerio plutôt qu’un simple tableau, donc console.log() affiche un wrapper rempli d'éléments internes. Ajoutez .get() à la fin pour obtenir le tableau d’objets simples auquel vous vous attendiez.

Se déplacer dans l'arborescence

Lorsque l’élément que vous recherchez ne dispose pas de sélecteur utile, partez d’un élément qui en possède un et parcourez l’arborescence jusqu’à lui. .find() recherche les descendants, .parent() remonte d’un niveau, .next() passe au frère suivant, et .first() ne conserve que la première correspondance. Les deux dernières lignes de l'exemple passent d'un produit au suivant, puis d'un prix à sa liste.

Comment extraire les données d’une véritable page web avec Cheerio ?

Téléchargez la page avec `fetch`, vérifiez le statut de la réponse, puis transmettez le texte HTML à `cheerio.load()`. Ensuite, parcourez chaque carte répétitive avec `.map()`, lisez tous les champs dont vous avez besoin, convertissez les prix en nombres et résolvez les liens relatifs avec `new URL()` afin que le résultat soit constitué de données propres et typées plutôt que de chaînes brutes.

La page « Books to Scrape » répertorie 20 livres par page, chacun dans une balise article.product_pod carte. Ce script récupère la première page et transforme chaque carte en un enregistrement typé :

// Fetch one page with Node's built-in fetch and parse it with Cheerio.
import * as cheerio from 'cheerio';

const START_URL = 'https://books.toscrape.com/';
const RATINGS = { One: 1, Two: 2, Three: 3, Four: 4, Five: 5 };

async function getHtml(url) {
  const res = await fetch(url, {
    headers: { 'User-Agent': 'cheerio-tutorial/1.0 (+https://www.webscrapingapi.com)' },
    signal: AbortSignal.timeout(15_000),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
  return res.text();
}

const html = await getHtml(START_URL);
const $ = cheerio.load(html);

const books = $('article.product_pod')
  .map((_, el) => {
    const card = $(el);
    const link = card.find('h3 a');
    const ratingWord = card.find('p.star-rating').attr('class').replace('star-rating', '').trim();
    return {
      title: link.attr('title'),
      price: Number(card.find('.price_color').text().replace(/[^0-9.]/g, '')),
      rating: RATINGS[ratingWord] ?? null,
      inStock: card.find('.availability').text().includes('In stock'),
      url: new URL(link.attr('href'), START_URL).href,
    };
  })
  .get();

console.log(`Found ${books.length} books`);
console.log(books.slice(0, 3));

Voici les trois premiers enregistrements tirés de node 02-books-page.mjs:

Found 20 books
[
  {
    title: 'A Light in the Attic',
    price: 51.77,
    rating: 3,
    inStock: true,
    url: 'https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html'
  },
  {
    title: 'Tipping the Velvet',
    price: 53.74,
    rating: 1,
    inStock: true,
    url: 'https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html'
  },
  {
    title: 'Soumission',
    price: 50.1,
    rating: 1,
    inStock: true,
    url: 'https://books.toscrape.com/catalogue/soumission_998/index.html'
  }
]

Quatre petites astuces permettent de transformer des chaînes brutes en données exploitables :

  • Prix : la page affiche « £51,77 ». Supprimez tout sauf les chiffres et la virgule décimale, appelez Number(), et vous obtenez 51,77, que vous pouvez trier et additionner.
  • Note : les étoiles sont du pur CSS. Le seul indice dans le code HTML est une classe telle que star-rating Three, le script supprime donc star-rating et associe le mot restant à un nombre.
  • Stock : « En stock » devient une valeur booléenne, ce qui est plus facile à filtrer que du texte libre.
  • URL : les href valeurs sont relatives, comme catalogue/a-light-in-the-attic_1000/index.html. Le constructeur d’URL les résout par rapport à l’adresse de la page pour en faire des liens qui fonctionnent partout.

Choisissez vos sélecteurs dans « Afficher la source », et non dans le panneau « Éléments » de DevTools. DevTools affiche le DOM après l’exécution des scripts, tandis que Cheerio ne voit que le code source brut envoyé par le serveur. Un sélecteur qui fonctionne dans DevTools mais pas dans « Afficher la source » ne correspondra à rien dans votre script.

L'appel fetch définit également un délai d'expiration, afin qu'une réponse bloquée ne puisse pas geler l'exécution, ainsi qu'un User-Agent honnête qui identifie votre outil. Sans cela, Node s'identifie simplement comme un « node », comme le montre httpbin :

// See which User-Agent your scraper sends, then set an honest one.
const plain = await fetch('https://httpbin.org/user-agent').then((r) => r.json());
console.log('default:', plain['user-agent']);

const named = await fetch('https://httpbin.org/user-agent', {
  headers: { 'User-Agent': 'cheerio-tutorial/1.0 (+https://www.webscrapingapi.com)' },
}).then((r) => r.json());
console.log('custom:', named['user-agent']);
default: node
custom: cheerio-tutorial/1.0 (+https://www.webscrapingapi.com)

Vous souhaitez extraire des lignes et des colonnes plutôt que des cartes ? Suivez notre tutoriel sur l’extraction de tableaux HTML en JavaScript.

Comment extraire plusieurs pages avec Cheerio ?

Extrayez la première page, lisez l’attribut href de son lien « suivant », résolvez-le par rapport à l’URL actuelle, puis répétez l’opération jusqu’à ce qu’il ne reste plus de lien suivant ou que vous atteigniez la limite de pages. Attendez entre chaque requête, ne réessayez qu’en cas de réponses 429 et 5xx avec des délais croissants, puis enregistrez les résultats au format JSON ou CSV à la fin.

Ce crawler s’appuie sur le script précédent. Il suit le bouton « Suivant » à travers le catalogue, réessaie en cas d’échecs temporaires et enregistre les résultats dans deux fichiers :

// Follow "next" links, retry politely, and save results to JSON and CSV.
import * as cheerio from 'cheerio';
import { writeFile } from 'node:fs/promises';
import { setTimeout as sleep } from 'node:timers/promises';

const START_URL = 'https://books.toscrape.com/catalogue/page-1.html';
const MAX_PAGES = Number(process.argv[2] ?? 3);
const DELAY_MS = 1_000;
const RATINGS = { One: 1, Two: 2, Three: 3, Four: 4, Five: 5 };

async function getHtml(url, attempts = 3) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    let waitMs = DELAY_MS * 2 ** attempt; // 2 s, then 4 s
    try {
      const res = await fetch(url, {
        headers: { 'User-Agent': 'cheerio-tutorial/1.0 (+https://www.webscrapingapi.com)' },
        signal: AbortSignal.timeout(15_000),
      });
      if (res.ok) return res.text();
      // Retry only on rate limits and server errors
      if (res.status !== 429 && res.status < 500) throw new Error(`HTTP ${res.status} for ${url}`);
      // Honor Retry-After when the server sends it in seconds
      const retryAfter = Number(res.headers.get('retry-after'));
      if (retryAfter > 0) waitMs = retryAfter * 1_000;
      console.warn(`HTTP ${res.status} on attempt ${attempt} for ${url}`);
    } catch (err) {
      if (attempt === attempts || err.message.startsWith('HTTP 4')) throw err;
      console.warn(`${err.name} on attempt ${attempt} for ${url}`);
    }
    if (attempt < attempts) await sleep(waitMs);
  }
  throw new Error(`Gave up on ${url}`);
}

function parseBooks($, pageUrl) {
  return $('article.product_pod')
    .map((_, el) => {
      const card = $(el);
      const link = card.find('h3 a');
      const ratingWord = card.find('p.star-rating').attr('class').replace('star-rating', '').trim();
      return {
        title: link.attr('title'),
        price: Number(card.find('.price_color').text().replace(/[^0-9.]/g, '')),
        rating: RATINGS[ratingWord] ?? null,
        inStock: card.find('.availability').text().includes('In stock'),
        url: new URL(link.attr('href'), pageUrl).href,
      };
    })
    .get();
}

function toCsv(rows) {
  const cols = Object.keys(rows[0]);
  const escape = (v) => {
    const s = String(v ?? '');
    return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
  };
  return [cols.join(','), ...rows.map((r) => cols.map((c) => escape(r[c])).join(','))].join('\n');
}

const books = [];
let url = START_URL;
for (let page = 1; url && page <= MAX_PAGES; page++) {
  const $ = cheerio.load(await getHtml(url));
  const found = parseBooks($, url);
  books.push(...found);
  console.log(`Page ${page}: ${found.length} books`);

  const next = $('li.next a').attr('href');
  url = next ? new URL(next, url).href : null;
  if (url) await sleep(DELAY_MS);
}

await writeFile('out/books.json', JSON.stringify(books, null, 2));
await writeFile('out/books.csv', toCsv(books));
console.log(`Saved ${books.length} books to out/books.json and out/books.csv`);

Créez le dossier de sortie, puis exécutez-le en limitant le nombre de pages à trois :

mkdir out
node 03-crawl-books.mjs 3
Page 1: 20 books
Page 2: 20 books
Page 3: 20 books
Saved 60 books to out/books.json and out/books.csv

Les premières lignes de out/books.csv:

title,price,rating,inStock,url
A Light in the Attic,51.77,3,true,https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html

Trois mesures de sécurité garantissent un fonctionnement correct et prévisible du robot d’indexation :

  • Une limite de pages. Le premier argument de ligne de commande définit MAX_PAGES. La boucle s'arrête également lorsqu'une page ne comporte pas de lien « Suivant ».
  • Un délai fixe. Le script attend une seconde entre chaque page.
  • Une politique de réessai restrictive. Seules les erreurs HTTP 429 « Too Many Requests » et les erreurs serveur 5xx font l’objet d’un réessai, après 2 secondes puis 4 secondes, ou après la Retry-After valeur envoyée par le serveur en secondes. Toute autre erreur 4xx, telle qu’une erreur 404, est immédiatement abandonnée, car une nouvelle requête ne changera pas la réponse.

Le générateur de fichiers CSV met entre guillemets tout champ contenant une virgule, un guillemet double ou un saut de ligne, et double les guillemets à l’intérieur de celui-ci. De nombreux titres de livres contiennent des virgules, et une seule virgule non mise entre guillemets décalerait toutes les colonnes suivantes.

Les délais et les tentatives de reconnexion constituent le strict minimum. Notre guide des bonnes pratiques en matière de web scraping aborde les limites de débit, la planification et la surveillance.

Comment utiliser les fonctions fromURL() et extract() de Cheerio ?

fromURL() télécharge une page et la charge en un seul appel en attente. extract() associe ensuite un objet simple de sélecteurs à des valeurs : une chaîne de caractères renvoie le texte de la première correspondance, un tableau renvoie toutes les correspondances, et un objet ou une fonction imbriqués construisent des enregistrements structurés. Ensemble, ils remplacent la plupart des boucles .map() écrites à la main pour les pages simples.

Voici la même idée appliquée à « Quotes to Scrape », sans appel fetch ni boucle manuelle :

// Cheerio's fromURL() and extract(): fetch and map fields in one step.
import { fromURL } from 'cheerio';

const PAGE_URL = 'https://quotes.toscrape.com/';
const $ = await fromURL(PAGE_URL);

const data = $.extract({
  pageTitle: 'title',
  quotes: [
    {
      selector: 'div.quote',
      value: {
        text: 'span.text',
        author: 'small.author',
        authorUrl: {
          selector: 'a[href^="/author/"]',
          value: (el) => new URL($(el).attr('href'), PAGE_URL).href, // resolve relative links
        },
        tags: { selector: '.tags', value: (el) => $(el).find('a.tag').map((_, a) => $(a).text()).get() },
      },
    },
  ],
});

console.log(data.pageTitle.trim());
console.log(`Quotes on page: ${data.quotes.length}`);
console.log(data.quotes[0]);
Quotes to Scrape
Quotes on page: 10
{
  text: '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
  author: 'Albert Einstein',
  authorUrl: 'https://quotes.toscrape.com/author/Albert-Einstein',
  tags: [ 'change', 'deep-thoughts', 'thinking', 'world' ]
}

Chaque clé correspond à un champ de sortie. pageTitle: 'title' lit le texte du premier <title> élément. En encapsulant un descripteur dans un tableau, comme quotes le fait, renvoie un résultat par correspondance, et la value propriété peut être un sélecteur, un objet imbriqué ou une fonction qui reçoit l’élément. La documentation d’extract() répertorie toutes les options.

Résolvez les liens vous-même. Selon votre version de Cheerio, href les valeurs lues via extract() ou .prop('href') peuvent encore être renvoyées sous forme relative après fromURL(), c’est pourquoi l’extrait les fait passer par new URL(). Cette petite précaution garantit la justesse de votre résultat dans les deux cas.

La documentation de Cheerio sur la manière dont Cheerio charge les documents apporte deux précisions supplémentaires. fromURL() rejette les réponses autres que 2xx en renvoyant une erreur, au lieu de vous fournir une page d’erreur à analyser. Et si vous passez requestOptions pour définir des en-têtes, vous devez définir method vous-même, car ils ne sont pas renseignés automatiquement.

Utilisez fetch plus load() à la place lorsque vous avez besoin de tentatives de connexion personnalisées, de délais ou d’une couche proxy, comme dans le robot d’indexation ci-dessus.

Comment extraire des données des balises JSON-LD et des balises de script ?

Avant d’écrire des sélecteurs, recherchez dans le code HTML les blocs de type « script type="application/ld+json" » et les scripts intégrés qui contiennent des données. Sélectionnez-les avec Cheerio, lisez leur contenu et analysez-le avec JSON.parse à l’intérieur d’un bloc try/catch. Les données structurées résistent généralement mieux aux refontes que le balisage visible et incluent souvent des prix, des références produit et des informations de disponibilité clairs.

De nombreuses pages de boutique et d’articles intègrent des données schema.org destinées aux moteurs de recherche, et lorsqu’elles sont présentes, elles constituent souvent la source la plus fiable de la page :

// Read structured data (JSON-LD) before writing selectors for visible text.
import { load } from 'cheerio';

const html = `<html><head>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Product","name":"Desk lamp","sku":"A1",
 "offers":{"@type":"Offer","price":"24.00","priceCurrency":"USD","availability":"https://schema.org/InStock"}}
</script></head><body><h1>Desk lamp</h1></body></html>`;

const $ = load(html);
const items = $('script[type="application/ld+json"]')
  .map((_, el) => {
    try {
      return JSON.parse($(el).text());
    } catch {
      return null; // skip malformed blocks instead of crashing
    }
  })
  .get()
  .filter(Boolean);

const product = items.find((item) => item['@type'] === 'Product');
console.log({ name: product.name, price: Number(product.offers.price), currency: product.offers.priceCurrency });
{ name: 'Desk lamp', price: 24, currency: 'USD' }

La balise try/catch empêche un seul bloc mal formé de faire échouer l’exécution. Filtrez par le @type critères de votre choix, tels que « Produit » ou « Article », car une même page contient souvent plusieurs blocs.

Certains sites fournissent l’intégralité de leur jeu de données dans un script intégré et construisent le balisage visible avec JavaScript. Ce cas semble à première vue ne pas fonctionner, c’est pourquoi il fait l’objet d’une section distincte.

Pourquoi Cheerio ne renvoie-t-il aucun résultat sur les pages générées par JavaScript ?

Cheerio ne voit que le code HTML envoyé par le serveur. Si une page construit son contenu avec JavaScript dans le navigateur, vos sélecteurs ne correspondent à rien car les éléments n’existent pas encore. Vérifiez d’abord le code HTML brut. Recherchez ensuite les données dans les scripts ou via un point de terminaison JSON, ou récupérez le code HTML rendu et analysez-le avec Cheerio.

Quotes to Scrape propose une version JavaScript à l’adresse /js/ qui s’affiche de manière identique dans un navigateur. Ce script montre ce que Cheerio reçoit réellement et où se cachent les données :

// Spot a JavaScript-rendered page, then look for the data inside a <script> tag.
import { fromURL } from 'cheerio';

const $ = await fromURL('https://quotes.toscrape.com/js/');

// 1. The selector that works on the static version finds nothing here
console.log('div.quote elements in raw HTML:', $('div.quote').length);

// 2. The quotes still ship in the HTML, inside an inline script
const script = $('script')
  .map((_, el) => $(el).text())
  .get()
  .find((code) => code.includes('var data ='));

// Capture the array literal between "var data =" and the first "];"
const json = script.match(/var data = (\[[\s\S]*?\]);/)[1];
const quotes = JSON.parse(json);

console.log(`Quotes found in <script>: ${quotes.length}`);
console.log({ author: quotes[0].author.name, tags: quotes[0].tags });
div.quote elements in raw HTML: 0
Quotes found in <script>: 10
{
  author: 'Albert Einstein',
  tags: [ 'change', 'deep-thoughts', 'thinking', 'world' ]
}

Le sélecteur qui a trouvé 10 citations sur la page statique n’en trouve aucune ici, car les div.quote éléments sont créés dans le navigateur. Les citations sont toujours présentes dans le code HTML, à l’intérieur d’une var data = [...] affectation ; une expression régulière et JSON.parse() permet de récupérer les 10 citations. C’est la différence entre le rendu côté serveur et le rendu côté client en pratique.

Lorsque les données ne se trouvent pas dans un script, essayez ces options dans l’ordre :

  1. Recherchez la requête JSON. Dans DevTools, ouvrez l’onglet Réseau, filtrez par Fetch/XHR, puis rechargez la page. De nombreuses pages chargent leur contenu à partir d’un point de terminaison JSON. S’il est public et que vous êtes autorisé à l’utiliser, appelez-le directement et évitez l’analyse du code HTML.
  2. Effectuez d’abord le rendu, puis l’analyse. Le rendu consiste à exécuter la page dans un moteur réel, généralement un navigateur sans interface graphique, avant de l’analyser. Votre code Cheerio ne change pas ; seule la source du code HTML change.

Vous pouvez exécuter un navigateur vous-même ou laisser une API s’en charger à votre place. L’API Scraper peut afficher la page et renvoyer le code HTML final, que vous transmettez directement à `cheerio.load()`. Ajoutez render_js=1 à la requête, et la réponse correspondra au code HTML après l’exécution des scripts. Le rendu JavaScript et le ciblage géographique n’entraînent aucun coût supplémentaire, et les requêtes ayant échoué ne sont pas facturées.

// When a page needs a browser, fetch rendered HTML from Scraper API and keep parsing with Cheerio.
import { load } from 'cheerio';

const apiKey = process.env.WSA_API_KEY;
if (!apiKey) {
  console.error('Set WSA_API_KEY to run this example.');
  process.exit(1);
}

const params = new URLSearchParams({
  api_key: apiKey,
  url: 'https://quotes.toscrape.com/js/',
  render_js: '1',
});

const res = await fetch(`https://api.webscrapingapi.com/v2?${params}`, {
  signal: AbortSignal.timeout(120_000),
});
if (!res.ok) throw new Error(`Scraper API returned HTTP ${res.status}`);

const $ = load(await res.text());
console.log('div.quote elements after rendering:', $('div.quote').length);
console.log('First author:', $('div.quote small.author').first().text());

Exécutez-la en plaçant votre clé dans une variable d’environnement, afin qu’elle n’apparaisse jamais dans votre code :

WSA_API_KEY=your_key node 08-render-handoff.mjs

Lorsque les données n’apparaissent qu’après des clics, des défilements ou un délai d’attente, l’API Browser exécute ces étapes pour vous. Vous décrivez les délais d’attente et les actions telles que cliquer, faire défiler, saisir du texte et valider dans une requête REST ; le navigateur s’exécute de notre côté, et le code HTML qu’il renvoie est intégré à cheerio.load() comme n’importe quelle autre page.

Pourquoi cheerio.load ne fonctionne-t-il pas ? Erreurs courantes et solutions

La plupart des problèmes liés à cheerio.load proviennent de quelques causes : une importation par défaut que Cheerio 1.x ne fournit plus, des sélecteurs écrits par rapport au DOM rendu par le navigateur, des fragments encapsulés dans des balises `html` et `body`, du XML analysé comme du HTML, ainsi que des liens relatifs ou des espaces non respectés. Chaque solution ci-dessous correspond à une modification d’une ligne que vous pouvez tester en quelques secondes.

Ce script présente cinq des sept solutions ci-dessous, avec un avant/après :

// Fixes for the errors people hit most often with cheerio.load().
import * as cheerio from 'cheerio';
import { load } from 'cheerio';

// Fix 1: there is no default export in Cheerio 1.x.
// `import cheerio from 'cheerio'` fails; use a namespace or named import.
console.log('namespace load is a function:', typeof cheerio.load === 'function');
console.log('named load is a function:', typeof load === 'function');

// Fix 2: load() wraps fragments in <html><head><body> by default.
const fragment = '<li>Apples</li><li>Pears</li>';
console.log('document mode:', load(fragment).html());
console.log('fragment mode:', load(fragment, null, false).html());

// Fix 3: parse XML (RSS, sitemaps) with the xml option so tags like <link> keep their text.
const rss = `<rss><channel><item><title>Post</title><link>https://example.com/post</link></item></channel></rss>`;
console.log('HTML mode <link>:', JSON.stringify(load(rss)('item link').text()));
console.log('XML mode <link>:', JSON.stringify(load(rss, { xml: true })('item link').text()));

// Fix 4: .text() keeps whitespace and joins nested text. Normalize it yourself.
const $ = load('<p class="desc">\n  Fast,\n  <b>small</b>   and   tested.\n</p>');
console.log('raw text:', JSON.stringify($('.desc').text()));
console.log('clean text:', JSON.stringify($('.desc').text().replace(/\s+/g, ' ').trim()));

// Fix 5: .attr('href') returns the value as written. Resolve it against the page URL.
const page = 'https://books.toscrape.com/catalogue/page-2.html';
console.log('absolute URL:', new URL('../index.html', page).href);
namespace load is a function: true
named load is a function: true
document mode: <html><head></head><body><li>Apples</li><li>Pears</li></body></html>
fragment mode: <li>Apples</li><li>Pears</li>
HTML mode <link>: ""
XML mode <link>: "https://example.com/post"
raw text: "\n  Fast,\n  small   and   tested.\n"
clean text: "Fast, small and tested."
absolute URL: https://books.toscrape.com/index.html

« cheerio.load n'est pas une fonction » ou « cheerio.default n'est pas défini »

Symptôme : votre script échoue lors de son premier appel à Cheerio, ou cheerio.default renvoie la valeur « undefined ». Dans les modules ES natifs, import cheerio from 'cheerio' l'échec survient encore plus tôt avec le message « ne fournit pas d'export nommé 'default' ».

Cause : Cheerio 1.x ne dispose pas d’exportation par défaut. Les anciens tutoriels et le code antérieur à la version 1.0 en utilisaient une.

Solution : utilisez une importation d'espace de noms, import * as cheerio from 'cheerio'ou une importation nommée, import { load } from 'cheerio'. En CommonJS, const cheerio = require('cheerio') cela fonctionne.

Vos sélecteurs ne renvoient rien

Symptôme : $('...').length est égal à 0, et .text() renvoie une chaîne vide.

Cause : l’une des trois raisons suivantes. Vous avez copié le sélecteur depuis DevTools, qui affiche le DOM après l’exécution des scripts ; le contenu est rendu par JavaScript, comme expliqué dans la section sur les pages rendues par JavaScript ; ou le serveur a renvoyé une page de blocage, une page de connexion ou une erreur.

Solution : affichez res.status et les 200 premiers caractères du code HTML à l'aide de console.log(res.status, html.slice(0, 200)). Vérifiez ensuite votre sélecteur dans « Afficher la source », et non dans le panneau « Éléments ».

Balises HTML, <head> et <body> supplémentaires dans la sortie

Symptôme : vous chargez un extrait de code tel que <li>Apples</li>, et .html() le système le renvoie enveloppé dans <html><head></head><body>.

Cause : par défaut, load() traite l’entrée comme un document complet et ajoute les balises d’encadrement manquantes, exactement comme le ferait un navigateur.

Solution : chargez les fragments en mode « fragment » avec load(html, null, false). Le troisième argument désactive le mode document.

Les flux XML et RSS perdent des données

Symptôme : lorsque vous analysez un flux RSS ou un plan du site, des champs tels que <link> sont vides.

Cause : en mode HTML, <link> est un élément vide et ne peut pas contenir de texte ; l’URL qu’il contient est donc supprimée. Dans le résultat ci-dessus, le mode HTML a renvoyé "" tandis que le mode XML a renvoyé l’URL.

Solution : utilisez load(xml, { xml: true }). Le mode XML respecte également la casse des noms de balises et n’ajoute pas de balises d’encapsulation « html » ou « body ».

Le texte s'affiche avec des espaces supplémentaires

Symptôme : .text() renvoie des chaînes remplies de sauts de ligne et de séries d’espaces.

Cause : .text() conserve les espaces de la source, y compris les indentations, et joint les éléments imbriqués tels que <b> sans séparateurs.

Solution : normalisez le contenu avec .text().replace(/\s+/g, ' ').trim(). Pour une page entière, sélectionnez d'abord l'élément de contenu principal afin d'exclure la navigation et le texte du pied de page.

Erreurs 403 ou 429

Symptôme : la requête échoue avec le code HTTP 403 Forbidden ou 429 Too Many Requests, et votre analyseur ne s’exécute jamais.

Cause : ces erreurs proviennent du serveur, et non de Cheerio. Cheerio ne voit le code HTML qu’une fois la requête réussie.

Solution : définissez un User-Agent valide, ralentissez le rythme, respectez Retry-Afteret respectez le fichier robots.txt ainsi que les conditions d’utilisation du site. Si un site continue de bloquer les requêtes simples sur des pages que vous êtes autorisé à collecter, acheminez la récupération via l’API Scraper et conservez votre analyseur.

Les liens sont renvoyés sous forme relative

Symptôme : .attr('href') renvoie des chemins tels que ../index.html ou /author/Albert-Einstein qui ne fonctionnent pas en dehors de la page.

Cause : .attr() renvoie la valeur exactement telle qu'elle est écrite dans le code source.

Solution : résoudre chaque lien avec new URL(href, pageUrl).href, en utilisant l'adresse de la page que vous avez récupérée comme base.

Comment éviter qu’un scraper Cheerio ne tombe en panne ?

Séparez l’analyse de la récupération, enregistrez une page réelle comme fixture et testez l’analyseur hors ligne avec `node:test`. Privilégiez les hooks stables tels que les identifiants, les attributs de données et `itemprop` plutôt que les classes de style, et générez une erreur lorsqu’une page ne renvoie aucun enregistrement, afin qu’une refonte échoue de manière évidente au lieu d’enregistrer des fichiers vides.

Les scrapers tombent rarement en panne avec une erreur. Généralement, la mise en page change, tous les sélecteurs ne correspondent plus à rien sans que cela se remarque, et vous vous retrouvez avec une semaine de fichiers vides. Faites de l’analyseur une fonction pure que vous pouvez tester, et faites en sorte qu’il signale une erreur lorsqu’il ne trouve rien.

Enregistrez une copie d’une page réelle en tant que fixture :

mkdir fixtures
curl -s https://books.toscrape.com/catalogue/page-1.html -o fixtures/books-page-1.html

Déplacez l’analyse dans son propre module. Il prend en charge le code HTML et une URL, sans se soucier de la provenance du code HTML :

// parse-books.mjs: keep parsing separate from fetching so it can be tested offline.
import { load } from 'cheerio';

const RATINGS = { One: 1, Two: 2, Three: 3, Four: 4, Five: 5 };

export function parseBooks(html, pageUrl) {
  const $ = load(html);
  const books = $('article.product_pod')
    .map((_, el) => {
      const card = $(el);
      const link = card.find('h3 a');
      const ratingWord = card.find('p.star-rating').attr('class').replace('star-rating', '').trim();
      return {
        title: link.attr('title'),
        price: Number(card.find('.price_color').text().replace(/[^0-9.]/g, '')),
        rating: RATINGS[ratingWord] ?? null,
        inStock: card.find('.availability').text().includes('In stock'),
        url: new URL(link.attr('href'), pageUrl).href,
      };
    })
    .get();

  // Fail loudly when the layout changes instead of saving an empty file
  if (books.length === 0) throw new Error(`No books found on ${pageUrl}; check the selectors`);
  return books;
}

Testez-le ensuite avec l’exécuteur de tests intégré à Node, sans aucune dépendance supplémentaire :

// Run with: node --test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { parseBooks } from './parse-books.mjs';

const PAGE_URL = 'https://books.toscrape.com/catalogue/page-1.html';

test('parses every book card on a saved page', async () => {
  const html = await readFile(new URL('./fixtures/books-page-1.html', import.meta.url), 'utf8');
  const books = parseBooks(html, PAGE_URL);

  assert.equal(books.length, 20);
  for (const book of books) {
    assert.ok(book.title.length > 0);
    assert.ok(book.price > 0);
    assert.ok(book.rating >= 1 && book.rating <= 5);
    assert.match(book.url, /^https:\/\/books\.toscrape\.com\/catalogue\//);
  }
});

test('throws when the layout changes', () => {
  assert.throws(() => parseBooks('<html><body><p>Redesigned</p></body></html>', PAGE_URL), /No books found/);
});

Exécutez node --test, et le résumé se termine par :

# tests 2
# pass 2
# fail 0

Le premier test vérifie que l'analyseur lit les 20 cartes avec des valeurs cohérentes ; le second, qu'une page remaniée lève une exception au lieu de renvoyer un tableau vide. Comme le fixture réside sur le disque, les tests s'exécutent hors ligne. Lorsque le site change, enregistrez un nouveau fixture et mettez à jour les sélecteurs jusqu'à ce que les tests réussissent.

Quelques bonnes pratiques permettent de prolonger la durée de vie des sélecteurs :

  • Privilégiez les identifiants (id) et les data-* aux attributs et itemprop les attributs. Ils ont une signification et changent moins souvent que les classes de style.
  • Évitez les classes utilitaires et les noms de classes générés, qui peuvent changer à chaque déploiement.
  • Basez-vous sur la structure plutôt que sur la position. « Le prix indiqué dans chaque fiche produit » résiste à l’apparition d’une nouvelle bannière ; « la troisième div », non.

Si la maintenance de la couche de récupération coûte plus cher que l’analyse, comparez les API de web scraping en fonction de la tâche et du coût.

Foire aux questions

Cheerio est-il plus rapide que Puppeteer ?

En général, pour les pages statiques. Cheerio ne fait qu’analyser du texte, tandis que Puppeteer lance un navigateur complet, charge les ressources et exécute des scripts ; Cheerio effectue donc beaucoup moins de travail par page. Mesurez les performances sur votre propre cible plutôt que de vous fier à des benchmarks génériques. Si les données nécessitent JavaScript pour s’afficher, Cheerio seul ne pourra pas y accéder, quelle que soit sa vitesse.

Cheerio peut-il exécuter du JavaScript ou extraire des pages dynamiques ?

Non. Cheerio analyse la chaîne HTML que vous lui fournissez et n’exécute jamais de scripts. Pour les pages dynamiques, commencez par rechercher les données dans les scripts intégrés ou dans les requêtes JSON de la page. Si aucune de ces méthodes ne fonctionne, récupérez le code HTML rendu à partir d’un navigateur sans interface graphique ou d’une API de rendu, puis transmettez-le à cheerio.load().

Ai-je besoin d’Axios pour utiliser Cheerio ?

Non. Les versions actuelles de Node.js incluent une fonction fetch globale ; vous pouvez donc appeler await fetch(url) et transmettre await res.text() à cheerio.load(). La méthode fromURL() de Cheerio peut également récupérer les données pour vous. Si votre projet utilise déjà Axios, cela fonctionne également : transmettez response.data à cheerio.load() de la même manière.

Cheerio prend-il en charge XPath ?

Non. Cheerio sélectionne les éléments à l’aide de sélecteurs CSS ainsi que de quelques extensions de type jQuery telles que :contains() et :first, et ne dispose pas de moteur XPath. La plupart des expressions XPath se traduisent par un sélecteur CSS associé à des méthodes de parcours telles que .parent(), .next() ou .filter(). Si vous avez besoin d’utiliser XPath, optez pour un analyseur syntaxique qui le prend en charge.

Cheerio fonctionne-t-il avec TypeScript ?

Oui. Cheerio fournit ses propres définitions de types ; il n’est donc pas nécessaire d’installer un package @types séparé. Importez load et le type CheerioAPI, annotez le type de retour de votre analyseur, puis exécutez le fichier avec tsx, ou ajoutez @types/node et compilez avec tsc. La logique d’analyse reste identique à celle de la version JavaScript.

Pourquoi `cheerio.load` renvoie-t-il un résultat vide ?

En général, les éléments ne se trouvent pas dans le code HTML brut. Vous avez peut-être copié des sélecteurs depuis DevTools, qui affiche la page après l’exécution des scripts, ou bien le serveur a peut-être renvoyé une page vide ou une erreur. Enregistrez le code d’état et les premières lignes du code HTML, puis vérifiez votre sélecteur en comparant avec la vue « Afficher la source ».

Comment extraire du texte brut à partir d’un code HTML avec Cheerio ?

Appelez la méthode .text() sur la sélection, puis normalisez les espaces avec .replace(/\s+/g, ' ').trim(). La méthode .text() concatène le texte de chaque élément imbriqué, en conservant les sauts de ligne et les indentations de la source. Pour une page entière, sélectionnez d’abord l’élément de contenu principal afin d’exclure de votre résultat les éléments de navigation et le texte du pied de page.

Cheerio peut-il analyser des flux XML et RSS ?

Oui. Passez { xml: true } comme option : cheerio.load(xml, { xml: true }). En mode HTML par défaut, les balises telles que `link` sont traitées comme des éléments HTML vides et perdent leur texte, ce qui empêche l'analyse des flux RSS. Le mode XML conserve également la casse des noms de balises et ignore les balises d'encapsulation `html`, `head` et `body`.

Comment éviter d’être bloqué lors d’un scraping avec Cheerio ?

Le blocage se produit au niveau de la requête, et non dans Cheerio. Envoyez un User-Agent valide, ajoutez un délai entre les requêtes, réessayez les réponses 429 et 5xx avec un délai de réessai, respectez l’en-tête Retry-After et suivez le fichier robots.txt ainsi que les conditions d’utilisation du site. Si les pages que vous êtes autorisé à collecter renvoient toujours des codes 403, changez la couche de récupération tout en conservant votre analyseur.

Le scraping Web avec Cheerio est-il légal ?

Cheerio n’est qu’un analyseur syntaxique ; la légalité dépend donc de ce que vous collectez et de la manière dont vous le faites. Les données publiques recueillies de manière courtoise et dans le respect des conditions d’utilisation d’un site présentent généralement moins de risques. Les données personnelles, les contenus protégés par une connexion et les œuvres protégées par le droit d’auteur impliquent des obligations supplémentaires. Les règles varient d’un pays à l’autre ; demandez donc un avis juridique pour les projets commerciaux. Ceci ne constitue pas un avis juridique.

Quand faut-il confier la récupération à une API ?

Utilisez Cheerio pour ce qu’il fait le mieux, à savoir transformer du code HTML en données propres. Lorsque les pages nécessitent du JavaScript ou renvoient systématiquement des codes d’erreur 403, confiez leur récupération à un autre outil. Les forfaits de l’API Scraper commencent par un essai gratuit de 5 000 requêtes. Le rendu JavaScript et le ciblage géographique ne coûtent pas de crédits supplémentaires, et les requêtes échouées ne sont pas facturées. Pour les flux nécessitant des clics, du défilement ou des temps d’attente, demandez un devis pour l’API Browser adapté à votre charge de travail. Dans tous les cas, votre analyseur reste le même.

À propos de l'auteur

Raluca Penciuc, Développeur full-stack @ WebScrapingAPI

Raluca Penciuc

Développeur full-stack

Raluca Penciuc est développeuse Full Stack chez WebScrapingAPI ; elle conçoit des robots de collecte de données, améliore les techniques de contournement et recherche des moyens fiables de réduire le risque de détection sur les sites cibles.

Web scraping avec AWS Lambda : guide pour Python et Java 2026
Guides

Web scraping avec AWS Lambda : guide pour Python et Java 2026

TL;DR: En bref : le web scraping avec AWS Lambda fonctionne mieux lorsque chaque invocation est courte, bien délimitée et peut faire l'objet d'une nouvelle tentative de manière indépendante. Commencez par utiliser les requêtes HTTP directes, AWS SAM et S3, puis ajoutez SQS, des conteneurs, le rendu dans un navigateur, des proxys ou une couche de récupération gérée uniquement lorsque la charge de travail démontre qu'elle en a besoin.

Suciu Dan33 min read
Lire l'article
Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données
Guides

Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données

TL;DR: En bref : GoSpider est un robot d'exploration en ligne de commande destiné à découvrir des URL, et non un outil complet de collecte de données structurées. Ce guide d'utilisation de GoSpider présente comment effectuer une exploration limitée, gérer correctement les résultats, transférer les données de Colly vers un fichier CSV, ainsi qu'une procédure de diagnostic pour les réponses 403 ou les pages nécessitant un rendu JavaScript.

Suciu Dan24 min read
Lire l'article
Comment récupérer les données de Redfin : Guide Python des données immobilières
Guides

Comment récupérer les données de Redfin : Guide Python des données immobilières

TL;DR: Redfin expose des points d'extrémité d'API cachés qui renvoient du JSON structuré pour les listes de propriétés, ce qui permet d'ignorer complètement l'analyse HTML fragile. Ce guide vous accompagne dans la construction d'un scraper Python qui extrait les données de location et de vente, effectue des recherches par emplacement, surveille les nouvelles inscriptions via des sitemaps XML et exporte des résultats propres au format CSV ou JSON.

Suciu Dan15 min read
Lire l'article

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.