Le scraping par langage 18 min de lecture

Web scraping en C++ : le guide complet

Web scraping en C++ : libcurl, parseurs HTML, quand le développement bas niveau se justifie et ce qu'il apporte en vitesse.

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

Sommaire

  1. Introduction : pourquoi scraper en C++ et ce qu'il faut garder à l'esprit
  2. Comment récupérer la page : les clients HTTP
  3. Travailler avec HTTPS / SSL
  4. Récupérer le statut de la réponse et les en-têtes
  5. Les bibliothèques pour parser le contenu
  6. Résoudre les problèmes de cyrillique et d'encodages
  7. Travailler avec les cookies et les sessions
  8. Utiliser des proxys
  9. Scraper via TOR
  10. Multithreading et curl_multi
  11. Stockage des URL et des files d'attente (panorama)
  12. Ce qu'il faut encore prendre en compte
  13. Principaux avantages et inconvénients d'une implémentation en C++
  14. La pile technologique finale recommandée

1. Introduction

Le web scraping, c'est la récupération automatique de pages web et l'extraction, à partir de celles-ci, de données structurées. Techniquement, la tâche se décompose en deux étapes indépendantes :

  1. L'étape réseau — télécharger le HTML/JSON via HTTP(S).
  2. L'étape d'analyse — transformer le texte « brut » en arbre (DOM) et en extraire les nœuds voulus.

On ne choisit pas C++ pour le confort (en Python ou en Go, un scraper s'écrit plusieurs fois plus vite), mais pour la performance et le contrôle : des dizaines de milliers de connexions sur un seul cœur, une consommation mémoire minimale, une latence prévisible, aucune pause de GC, une intégration facile dans un backend C/C++ existant.

Avant d'écrire du code, pensez au volet juridique et éthique : respectez robots.txt, ne générez pas de charge excessive (rate limiting), lisez les conditions d'utilisation du site et la législation sur les données personnelles. Les techniques de contournement des blocages (proxys, TOR) sont présentées ci-dessous comme un outil, pas comme une invitation à enfreindre les règles des plateformes.


2. Comment récupérer la page

C'est le fondement de tout le scraper. En C++, les options sont multiples — de la classique bas niveau aux wrappers confortables « façon Python ».

2.1. libcurl — le standard du secteur

libcurl est le standard de facto des requêtes réseau en C/C++. Il prend en charge HTTP/1.1, HTTP/2, HTTP/3, HTTPS, les proxys, SOCKS5, les cookies, la compression, les timeouts — littéralement tout ce qu'il faut pour le scraping, dans une seule bibliothèque. Son défaut — une API C sur callbacks, verbeuse.

c++
#include <curl/curl.h>
#include <string>

// Callback : curl l'appelle au fil de l'arrivée des données, on les accumule dans une chaîne.
static size_t write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) {
    auto* out = static_cast<std::string*>(userdata);
    out->append(ptr, size * nmemb);
    return size * nmemb;
}

std::string fetch(const std::string& url) {
    CURL* curl = curl_easy_init();
    std::string body;

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);     // suivre les redirections
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");    // gzip/deflate/br automatiquement
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; MyBot/1.0)");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        // traiter curl_easy_strerror(res)
    }
    curl_easy_cleanup(curl);
    return body;
}

Notez le CURLOPT_ACCEPT_ENCODING, "" — il active la décompression transparente gzip/deflate/brotli ; sans lui, vous obtiendrez du bruit binaire à la place du HTML.

2.2. cpr — « Curl for People »

cpr (C++ Requests) est une surcouche moderne de libcurl dans l'esprit de Python Requests. Elle requiert C++17 et est activement maintenue (documentation). Les mêmes fonctionnalités, mais un code plusieurs fois plus court :

c++
#include <cpr/cpr.h>

cpr::Response r = cpr::Get(
    cpr::Url{"https://example.com"},
    cpr::Header{{"User-Agent", "MyBot/1.0"}},
    cpr::Timeout{30000}
);

r.status_code;            // 200
r.header["content-type"]; // "text/html; charset=utf-8"
r.text;                   // corps de la réponse

Pour la plupart des projets, c'est le meilleur point de départ : vous obtenez toute la puissance de libcurl (proxys, cookies, SSL, requêtes asynchrones) avec une API agréable. Elle s'intègre via CMake FetchContent, vcpkg ou Conan.

2.3. cpp-httplib — header-only

cpp-httplib — un seul fichier d'en-tête, aucune dépendance externe (OpenSSL est requis pour HTTPS). Idéal quand on ne veut pas embarquer curl. Inconvénients : HTTP/1.1 uniquement, pas de prise en charge intégrée des proxys SOCKS ni de la compression brotli.

c++
#include <httplib.h>

httplib::Client cli("https://example.com");
auto res = cli.Get("/");
if (res && res->status == 200) {
    std::string body = res->body;
}

2.4. Boost.Beast — bas niveau et asynchrone

Boost.Beast est bâtie au-dessus de Boost.Asio et donne un contrôle total sur HTTP/WebSocket au niveau des sockets, avec un modèle asynchrone (coroutines, futures, callbacks). C'est la voie à suivre quand il faut des dizaines de milliers de connexions simultanées et une logique réseau sur mesure. Le prix à payer — nettement plus de code ; HTTPS devra se configurer à la main via le SSL-stream d'Asio.

Que choisir

Scénario Recommandation
Démarrer vite, scraper classique cpr
Contrôle et fonctionnalités au maximum, le « standard » établi libcurl en direct
Un minimum de dépendances, client simple cpp-httplib
Des dizaines de milliers de connexions asynchrones Boost.Beast / Asio

3. Travailler avec HTTPS / SSL

Aujourd'hui, la quasi-totalité du web est en HTTPS : TLS n'est donc pas une option, mais la norme.

3.1. Via libcurl/cpr

libcurl s'appuie lui-même sur une couche TLS (par défaut OpenSSL, mais il existe des builds avec GnuTLS, mbedTLS, BoringSSL, Schannel sous Windows, Secure Transport sous macOS). L'essentiel — la vérification des certificats :

c++
// ACTIVÉ PAR DÉFAUT — à ne modifier qu'en toute connaissance de cause.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // vérifier la chaîne de certificats
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // vérifier le nom d'hôte
// Indiquer son propre bundle CA si celui du système est introuvable :
curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/cacert.pem");

Ne désactivez jamais VERIFYPEER/VERIFYHOST en production pour « réparer » des erreurs de certificat — c'est une porte ouverte aux attaques MITM. Si le jeu de certificats racines du système est absent (fréquent sous Windows/en conteneur), téléchargez un cacert.pem à jour (publié par le projet curl) et déclarez-le via CURLOPT_CAINFO.

Dans cpr, le comportement par défaut est sûr ; au besoin, tout se règle via cpr::SslOptions.

3.2. Subtilités

  • SNI (Server Name Indication) est activé par défaut — indispensable pour les hôtes virtuels.
  • Version de TLS : il est judicieux de forcer au minimum TLS 1.2 (CURLOPT_SSLVERSION = CURL_SSLVERSION_TLSv1_2).
  • TLS-fingerprinting : les systèmes anti-bots avancés savent distinguer les clients à l'empreinte JA3/JA4 du handshake TLS. L'empreinte habituelle de libcurl diffère de celle d'un navigateur — c'est un vaste sujet à part entière (jusqu'aux builds de curl patchés pour reproduire le ClientHello d'un navigateur).

4. Statut de la réponse et en-têtes

Un scraper doit réagir aux codes HTTP : 200 — OK, 301/302 — redirection, 403/429 — blocage/rate limit, 5xx — erreur serveur (à réessayer plus tard).

4.1. Code de réponse et en-têtes dans libcurl

c++
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

char* content_type = nullptr;
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);

// On capture l'ensemble des en-têtes via un callback dédié :
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers_map);

Champs getinfo utiles : CURLINFO_RESPONSE_CODE, CURLINFO_CONTENT_TYPE, CURLINFO_EFFECTIVE_URL (l'URL finale après redirections), CURLINFO_REDIRECT_COUNT, CURLINFO_TOTAL_TIME, CURLINFO_SIZE_DOWNLOAD.

4.2. Dans cpr — tout est déjà parsé

c++
cpr::Response r = cpr::Get(cpr::Url{"https://example.com"});
r.status_code;               // 200
r.reason;                    // "OK"
r.header["content-type"];    // map pratique des en-têtes
r.url;                       // URL finale
r.elapsed;                   // durée de la requête

À quoi cela sert au scraper : l'en-tête Content-Type: text/html; charset=windows-1251 livre l'encodage (voir la section sur le cyrillique), et Retry-After, lors d'un 429, indique combien de temps attendre avant de réessayer.


5. Les bibliothèques pour parser le contenu

Le HTML est téléchargé — il faut maintenant construire le DOM et en extraire les nœuds par sélecteurs. Ne parsez pas le HTML avec des expressions régulières — le balisage réel, avec ses balises non fermées, son imbrication et ses commentaires, casse n'importe quelle regex ; utilisez un parseur digne de ce nom.

5.1. lexbor — le choix moderne n°1

lexbor (GitHub) est un parseur HTML5 rapide, conforme au standard WHATWG, écrit en C pur, sans dépendances externes. Il prend en charge les sélecteurs CSS, la détection de l'encodage à partir du flux d'octets et la manipulation du DOM. C'est le successeur de fait de myhtml/Modest et, à ce jour, la meilleure option pour du nouveau code (c'est lui, au passage, qu'utilise le parseur DOM intégré de PHP 8.4).

c
#include <lexbor/html/parser.h>
#include <lexbor/dom/interfaces/element.h>

lxb_html_document_t* doc = lxb_html_document_create();
lxb_html_document_parse(doc, (const lxb_char_t*)html.data(), html.size());
// ensuite : parcours du DOM ou recherche via le module selectors (sélecteurs CSS)
lxb_html_document_destroy(doc);

Pour disposer d'un wrapper C++ confortable au-dessus de lexbor, il existe des projets tiers (par exemple sprexer).

5.2. libxml2 + XPath

libxml2 est une bibliothèque mûre, éprouvée par les années. Sa fonction htmlReadMemory() digère le HTML « sale », et XPath offre des sélections puissantes (pas de sélecteurs CSS de série, mais XPath est plus expressif). Excellent choix si vos données se décrivent bien en expressions XPath.

c++
#include <libxml/HTMLparser.h>
#include <libxml/xpath.h>

htmlDocPtr doc = htmlReadMemory(html.data(), html.size(), nullptr, "UTF-8",
                                HTML_PARSE_RECOVER | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING);
xmlXPathContextPtr ctx = xmlXPathNewContext(doc);
xmlXPathObjectPtr res = xmlXPathEvalExpression((const xmlChar*)"//a/@href", ctx);
// parcourir res->nodesetval->nodeTab

5.3. Gumbo — un classique, mais archivé

Gumbo, de Google, a longtemps été le standard du parsing HTML5 en C/C++, mais le dépôt est archivé (en lecture seule depuis janvier 2026). Le développement se poursuit dans un fork communautaire, sur Codeberg. Pour du nouveau code, préférez lexbor ; Gumbo est cité parce que vous le rencontrerez dans une multitude de projets existants.

5.4. Autres outils

  • htmlcxx — un parseur HTML/CSS C++ simple ; pratique pour les tâches légères, mais plus maintenu depuis longtemps.
  • pugixml et RapidXML — pour le XML strict (RSS, sitemap, SOAP), pas pour du HTML arbitraire.
  • JSON : beaucoup de sites livrent leurs données via une API ou du JSON embarqué. Prenez nlohmann/json (le confort) ou RapidJSON (la vitesse).

Que choisir

Tâche Recommandation
Nouveau scraper HTML5, besoin de sélecteurs CSS lexbor
Sélections complexes, habitude de XPath libxml2
Maintenance de code legacy Gumbo / fork sur Codeberg
XML strict (RSS/sitemap) pugixml
API JSON nlohmann/json ou RapidJSON

6. Encodages et Unicode

Douleur classique : la page arrive en « caractères illisibles ». La cause est presque toujours la même — des encodages qui ne concordent pas. Les sites russophones se partagent entre UTF-8, windows-1251 (cp1251), plus rarement KOI8-R. À l'intérieur du programme, gardez tout en UTF-8, en convertissant dès l'entrée.

6.1. Comment déterminer l'encodage source

Les sources, par ordre de priorité :

  1. L'en-tête HTTP Content-Type: text/html; charset=windows-1251.
  2. La méta HTML : <meta charset="..."> ou <meta http-equiv="Content-Type" content="...; charset=...">.
  3. Le BOM en début de fichier (pour UTF-8/16).
  4. L'heuristique sur le contenu (à défaut d'autre chose).

Point appréciable : lexbor sait détecter lui-même l'encodage à partir du flux d'octets — cela règle l'essentiel du problème dès l'étape de parsing.

6.2. Conversion en UTF-8

Option A — iconv (GNU libiconv), disponible à peu près partout :

c++
#include <iconv.h>
// cp1251 -> UTF-8
iconv_t cd = iconv_open("UTF-8", "CP1251");
// ... iconv(cd, &in, &inleft, &out, &outleft) ...
iconv_close(cd);

Option B — ICU (International Components for Unicode) — la voie la plus puissante et la plus fiable : une liste d'encodages énorme, la normalisation Unicode, un détecteur d'encodage (ucsdet_*) :

c++
#include <unicode/ucnv.h>
icu::UnicodeString us(raw.data(), raw.size(), "windows-1251");
std::string utf8;
us.toUTF8String(utf8);

Option C — UTF8-CPP (utfcpp) — une bibliothèque header-only légère pour valider/itérer/convertir de l'UTF-8/UTF-16/UTF-32 (elle ne transcode pas le cp1251, mais elle est irremplaçable pour travailler correctement avec l'UTF-8 lui-même).

6.3. Pièges en pratique

  • N'affichez pas d'UTF-8 dans la console Windows sans SetConsoleOutputCP(CP_UTF8) — vous verrez des caractères parasites alors que les données sont correctes.
  • Sous Windows, pour des noms de fichiers en cyrillique, utilisez l'API wide (std::wstring/UTF-16).
  • std::string stocke des octets, pas des « caractères » ; pour compter de vrais caractères cyrilliques, comptez les points de code (utfcpp/ICU), pas .size().
  • Fixez l'invariant une fois pour toutes : « à l'entrée, détection et transcodage en UTF-8 ; ensuite, uniquement de l'UTF-8 sur tout le pipeline ».

Beaucoup de sites exigent une session : connexion, panier, vérifications anti-bots, pagination derrière authentification. Les cookies doivent être acceptés, conservés et renvoyés.

libcurl embarque un « cookie engine » :

c++
// Activer le moteur et conserver les cookies dans un fichier entre les exécutions :
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); // lire (chaîne vide — juste activer le moteur en mémoire)
curl_easy_setopt(curl, CURLOPT_COOKIEJAR,  "cookies.txt"); // écrire au cleanup

// Passer un cookie précis à la main :
curl_easy_setopt(curl, CURLOPT_COOKIE, "session=abc123; lang=ru");

Au sein d'une même « session », réutilisez le même handle CURL (ou une même cpr::Session) — cookies, connexions keep-alive et sessions TLS sont alors conservés d'une requête à l'autre, ce qui est à la fois plus rapide et plus juste vis-à-vis de la logique du site.

Dans cpr :

c++
cpr::Session session;
session.SetUrl(cpr::Url{"https://example.com/login"});
session.SetCookies(cpr::Cookies{{"session", "abc123"}});
cpr::Response r = session.Get();
cpr::Cookies received = r.cookies; // les cookies renvoyés par le serveur

Piège classique : pour qu'une redirection transfère correctement les cookies entre sous-domaines, activez le cookie engine avant la requête et ne recréez pas le handle à chaque étape.


8. Proxys

Les proxys servent à contourner les restrictions géographiques, à répartir la charge et à réduire le risque de blocage par IP. libcurl prend en charge les proxys HTTP, HTTPS et SOCKS5 « de série ».

c++
// Proxy HTTP avec authentification :
curl_easy_setopt(curl, CURLOPT_PROXY, "http://user:pass@proxyhost:8080");

// SOCKS5 (avec résolution DNS côté proxy — important pour l'anonymat) :
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://proxyhost:1080");

Dans cpr :

c++
cpr::Response r = cpr::Get(
    cpr::Url{"https://example.com"},
    cpr::Proxies{{"https", "http://user:pass@proxyhost:8080"}}
);

Rotation des proxys

Pour du scraping à grande échelle, on entretient un pool de proxys et on les fait tourner : en round-robin, au hasard ou selon leur « santé » (ban → mise à l'écart temporaire). La stratégie la plus simple — un dictionnaire {proxy → compteur d'erreurs/temps d'arrêt} et le choix d'un proxy vivant avant chaque requête. Il est sain d'attribuer à chaque proxy son propre cookie-jar et son propre User-Agent, pour que les « identités » ne se recoupent pas.

Le préfixe socks5h:// (avec la lettre h) signifie que la résolution DNS passe par le proxy, et non en local — sans quoi votre vraie requête DNS vous trahit. C'est critique pour les proxys, et plus encore pour TOR (ci-dessous).


9. Scraper via TOR

TOR est un cas particulier de proxy SOCKS5 : le démon Tor local ouvre un SOCKS5, en général sur 127.0.0.1:9050 (Tor Browser — sur le 9150). Il suffit d'y diriger les requêtes :

c++
// Tout le trafic passe par le réseau Tor ; 'h' — résolution DNS dans Tor (obligatoire !) :
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");

Changer d'« identité » (nouveau nœud de sortie)

Via le ControlPort de Tor (port 9051), le signal NEWNYM permet d'obtenir un nouveau circuit, c'est-à-dire, de fait, une nouvelle IP. Pour cela, on ouvre un socket TCP vers le port de contrôle et on envoie les commandes du Tor Control Protocol :

code
AUTHENTICATE "mot_de_passe"
SIGNAL NEWNYM
QUIT

Cela permet de changer d'IP de sortie entre deux « vagues » de requêtes. En pratique, gardez en tête :

  • Entre deux NEWNYM, Tor impose un petit temps de latence — ne tirez pas sur le signal trop souvent.
  • Tor est lent : latence élevée, débit étroit. Pour du scraping massif, c'est un outil d'anonymat plus que de performance.
  • Beaucoup de sites bloquent en bloc les nœuds de sortie Tor connus (403/CAPTCHA).
  • Ne faites pas passer par Tor des tâches où vous vous connectez de toute façon sous un compte réel — l'anonymat se perd au niveau applicatif.

Techniquement, la couche de code est la même que pour les proxys (section 8) ; seule l'adresse du proxy change, et s'y ajoute la logique de dialogue avec le port de contrôle.


10. Multithreading

Le scraping est une tâche I/O-bound : l'essentiel du temps se passe à attendre le réseau. Le parallélisme apporte un gain démultiplié. Deux approches fondamentalement différentes existent.

10.1. Pool de threads + un handle par thread

La méthode classique : un pool de threads de travail, une file d'URL partagée thread-safe et, pour chaque thread, son propre handle CURL.

c++
#include <thread>
#include <queue>
#include <mutex>

std::queue<std::string> urls;
std::mutex m;

void worker() {
    CURL* curl = curl_easy_init();   // un handle PROPRE par thread
    for (;;) {
        std::string url;
        {
            std::lock_guard<std::mutex> lk(m);
            if (urls.empty()) break;
            url = urls.front(); urls.pop();
        }
        std::string html = fetch_with(curl, url);
        // ... parsing ...
    }
    curl_easy_cleanup(curl);
}

int main() {
    curl_global_init(CURL_GLOBAL_ALL); // UNE SEULE fois, avant de démarrer les threads !
    std::vector<std::thread> pool;
    for (int i = 0; i < 16; ++i) pool.emplace_back(worker);
    for (auto& t : pool) t.join();
    curl_global_cleanup();
}

Point critique : curl_global_init() s'appelle une seule fois, dans le thread principal, avant le lancement des workers ; le handle CURL ne doit jamais être partagé entre threads — chacun a le sien (voir curl: threadsafe).

10.2. curl_multi — beaucoup de connexions dans un seul thread

curl_multi mène des centaines/milliers de transferts parallèles dans un seul thread grâce au multiplexage (epoll/poll). C'est plus efficace en mémoire que « un thread par connexion » et cela passe idéalement à l'échelle. Inconvénient — un code plus complexe (boucle curl_multi_perform + gestion des événements). On peut combiner : plusieurs threads, chacun avec sa propre pile multi.

10.3. Primitives de parallélisme prêtes à l'emploi

  • std::thread / std::async — les briques de base de la STL.
  • oneTBB (Intel Threading Building Blocks) — algorithmes parallèles de haut niveau, pipelines (parallel_pipeline épouse bien le schéma « télécharger → parser → sauvegarder »), conteneurs thread-safe.
  • OpenMP — parallélisation simple des boucles par pragmas ; plus utile pour un parsing lourd en CPU que pour l'attente réseau.

Recommandation

Pour la plupart des projets : un pool de threads (16–64) + un handle par thread. Quand la mémoire ou le nombre de threads devient la limite, à plusieurs milliers de connexions — passez à curl_multi.


11. Stockage des URL et files d'attente

(Section de synthèse — on touche ici à l'architecture d'un crawler.) Dès que le scraper se met à suivre des liens apparaît la gestion du frontier — la file des URL non encore visitées — et de la déduplication.

Les sous-tâches clés :

  • La file de tâches (frontier). Dans le cas simple — une std::queue en mémoire. Pour la robustesse et la distribution — un broker externe : RabbitMQ (client rabbitmq-c) ou Redis en guise de file/ensemble (client hiredis).
  • La déduplication des URL. Pour ne pas télécharger deux fois la même chose : un std::unordered_set d'URL normalisées en mémoire ; sur de gros volumes — un filtre de Bloom (compact, au prix de rares faux positifs) ou des clés dans Redis.
  • La normalisation des URL. Mise en forme canonique (schéma, casse de l'hôte, tri de la query, suppression du #fragment, résolution des liens relatifs). Boost.URL rendra service.
  • La persistance. L'état du crawl et les résultats vont en base : SQLite pour une machine seule, PostgreSQL/ClickHouse — pour l'échelle.
  • La politique d'exploration. BFS/DFS, priorités par domaine, rate limiting par hôte et délais de politesse, limitation de la profondeur.

Le pipeline type : frontier → téléchargeur (pool/multi) → parseur → extracteur de liens → normalisation → dédup → retour dans le frontier, les données extraites partant, elles, vers le stockage.


12. Ce qu'il faut encore prendre en compte

Des sujets qu'on oublie souvent, mais sans lesquels un scraper en production se casse vite :

  • robots.txt et politesse. Parsez et respectez robots.txt (il existe un parseur officiel : google/robotstxt) ; imposez des délais et limitez le RPS par domaine. C'est éthique, et cela réduit le risque de ban.

  • Protection anti-bot et empreintes. Les sites analysent le User-Agent, le jeu et l'ordre des en-têtes, l'empreinte TLS (JA3/JA4), le framing HTTP/2, le comportement. Au minimum — envoyez des en-têtes plausibles (Accept, Accept-Language, User-Agent) et évitez de « frapper » toujours au même rythme. Les CAPTCHA et défis JS forment une couche de problèmes à part.

  • Rendu JavaScript. libcurl/lexbor ne voient que le HTML d'origine, sans exécution du JS. Pour les sites SPA (React/Vue), dont le contenu est chargé par des scripts, il faut soit un navigateur headless (piloter Chromium via le Chrome DevTools Protocol depuis C++), soit — souvent plus simple — trouver et appeler la même API JSON interne que celle qu'utilise le frontend.

  • Compression du contenu. Activez Accept-Encoding (gzip/deflate/brotli) — le trafic est divisé par plusieurs ; libcurl décompresse de façon transparente avec CURLOPT_ACCEPT_ENCODING. Pour une décompression manuelle — zlib et brotli.

  • Nouvelles tentatives et backoff. Le réseau est instable. Implémentez des retries avec délai exponentiel et jitter, respectez l'en-tête Retry-After sur 429/503, fixez des timeouts raisonnables (CURLOPT_TIMEOUT, CURLOPT_CONNECTTIMEOUT).

  • Mémoire et ressources. Fermez les handles, libérez les arbres DOM (*_destroy), surveillez Content-Length/le plafond de taille de réponse (CURLOPT_MAXFILESIZE) pour ne pas aspirer par mégarde un fichier d'un gigaoctet en mémoire.

  • Journalisation et surveillance. Comptez les codes de réponse, la vitesse, la part de bans par domaine — sinon, impossible de savoir quand le scraper est « mort en silence ».


13. Avantages et inconvénients

Les avantages d'une implémentation en C++

  • Performance et faible latence. Une vitesse proche du « métal », un minimum de surcoût par requête.
  • Efficacité mémoire. On peut tenir des milliers de connexions sur des ressources modestes (surtout avec curl_multi), sans pauses de ramasse-miettes.
  • Contrôle total. Réglage fin du TLS, des sockets, des timeouts, de la mémoire — là où les langages de haut niveau masquent les détails.
  • Un écosystème mûr de bibliothèques C. libcurl, OpenSSL, libxml2, ICU, lexbor — un socle industriel éprouvé.
  • Intégration aisée dans un backend C/C++ existant, sans ponts entre langages.

Les inconvénients

  • Développement plus long et plus coûteux. Ce qui s'écrit en une soirée en Python demande en C++ plus de code et de rigueur.
  • Gestion manuelle des ressources. Fuites mémoire, handles qui traînent, races en multithreading — c'est votre responsabilité.
  • Des API C sur callbacks (libcurl, libxml2), verbeuses ; les wrappers (cpr) sauvent en partie la mise.
  • Peu de « batteries incluses » pour les sites JS. Pas d'équivalent natif de Selenium/Playwright ; un navigateur headless piloté depuis C++, c'est douloureux.
  • Moins de frameworks de scraping prêts à l'emploi qu'en Python (pas d'équivalent de Scrapy « de série ») — on écrit davantage à la main.

Conclusion : C++ se justifie quand l'échelle, la vitesse et la consommation de ressources sont critiques (crawlers à forte charge, produit bâti sur une stack C++). Pour des tâches ponctuelles et des prototypes, Python/Go restent plus rapides à mettre en œuvre.


14. La stack finale

Un ensemble équilibré pour un scraper C++ de production :

Couche Bibliothèque
Client HTTP cpr (au-dessus de libcurl)
TLS OpenSSL (via libcurl)
Parsing HTML lexbor (ou libxml2 pour XPath)
JSON nlohmann/json / RapidJSON
Encodages ICU ou iconv
Multithreading pool std::thread + curl_multi, oneTBB au besoin
Proxys/TOR libcurl (socks5h://) + Tor ControlPort
File/dédup Redis / RabbitMQ, filtre de Bloom
Stockage SQLite / PostgreSQL
Normalisation des URL Boost.URL

Commencez petit — cpr + lexbor + un seul thread — et n'ajoutez de la complexité (proxys, multithreading, files d'attente) qu'en réponse à une charge réelle.