Aller au contenu
Retour au blog

Extraction de données sur le Web en C et C++ avec libcurl et lexbor

Raluca PenciucDernière mise à jour le 30 min read
Extraction de données sur le Web en C et C++ avec libcurl et lexbor
En bref : Pour le web scraping en C et C++, utilisez libcurl pour télécharger les pages et lexbor pour analyser le HTML5 avec des sélecteurs CSS ; les deux sont écrits en C pur, donc une seule pile sert les deux langages. Choisissez C++ lorsque le scraper s'intègre à une base de code native, tourne sur du matériel contraint ou analyse de gros volumes de HTML. Pour des tâches ponctuelles et rapides, Python s'écrit plus vite.

Ce guide vous explique comment extraire des données de sites web en C et C++ (et non en C#) à l’aide de deux bibliothèques C portables : libcurl pour télécharger des pages et lexbor pour analyser le code HTML5 à l’aide de sélecteurs CSS. Toutes deux sont écrites en C pur, ce qui permet d’utiliser une seule pile pour les deux langages. Elles s’installent également de la même manière sous Windows, macOS et Linux.

Vous allez créer un scraper C++20 pour une librairie publique d’entraînement, Books to Scrape. Il suit la pagination, visite chaque page produit à l’aide d’un petit pool de threads et enregistre des fichiers CSV et JSON propres. Vous traiterez ensuite une page JavaScript sur Quotes to Scrape de trois manières différentes : en lisant les données directement à partir d’une balise script, en les rendant avec Chrome headless, et en les transmettant à une API de scraping. Enfin, vous reverrez le scraper de base sous la forme d’un simple programme C. CMake et vcpkg se chargent de tout compiler à partir d’un seul manifeste.

Ce que vous allez créer :

  1. Un robot d’indexation qui suit poliment les liens « Suivant » sur les pages de listes de produits.
  2. Une étape dédiée aux pages de produits qui ajoute les codes UPC et les quantités en stock à l’aide de quatre threads de travail.
  3. Exportation au format CSV RFC 4180 et JSON ordonné avec des prix sous forme d’entiers.
  4. Trois méthodes pour extraire des données d’une page JavaScript.
  5. Un scraper C17 simple, basé sur les deux mêmes bibliothèques.

Vous préférez écrire en C# ? Notre guide de scraping web en C# couvre HttpClient, AngleSharp et Playwright pour .NET.

Le C++ est-il adapté au scraping web ?

Oui, lorsque le scraper doit s'intégrer dans une base de code C ou C++ existante, fonctionner sur du matériel limité ou traiter de grands volumes de code HTML avec peu de mémoire. Pour les tâches ponctuelles, Python est plus rapide à écrire. Le C++ ne dispose pas de liaison officielle avec Playwright ; les sites utilisant beaucoup de JavaScript nécessitent donc Chrome en mode headless ou une API de scraping.

Le C++ est particulièrement adapté aux :

  • Les appareils embarqués et en périphérie, où un petit binaire natif est préférable à l'intégration d'un runtime Python.
  • Les services natifs existants, tels que les systèmes de trading, les indexeurs de recherche et les agents de surveillance, où le scraper est un module supplémentaire plutôt qu’un processus distinct écrit dans un autre langage.
  • L’analyse de volumes importants, où une utilisation réduite du processeur et de la mémoire par page se traduit par de réelles économies sur les serveurs.
  • Les équipes qui souhaitent disposer du même analyseur en C et en C++, car libcurl et lexbor sont des bibliothèques C dotées d’enveloppes C++.

Le C++ est peu adapté aux cas suivants :

  • Les scripts ponctuels rapides, pour lesquels un notebook Python permet d’obtenir un fichier CSV plus rapidement.
  • L’automatisation intensive des navigateurs impliquant des connexions, des clics et des temps d’attente. Playwright prend en charge JavaScript, Python, Java et .NET, mais pas le C++.
  • Les équipes ne comptant aucun membre capable de réviser régulièrement du code C++.

La gestion manuelle de la mémoire est le principal risque des scrapers natifs, car chaque bibliothèque C vous fournit des pointeurs bruts à libérer. Ce guide encapsule chaque descripteur C dans une petite classe RAII et montre comment vérifier le résultat à l’aide de sanitizers, afin qu’un oubli de nettoyage soit détecté lors d’un test plutôt que sous la forme d’une fuite en production.

De quoi avez-vous besoin pour le web scraping en C et C++ ?

Il vous faut un compilateur C++20, CMake et trois bibliothèques : libcurl pour télécharger les pages, lexbor pour analyser le HTML5 et exécuter les sélecteurs CSS, et nlohmann/json pour générer du JSON. libcurl et lexbor sont des bibliothèques C classiques ; la même pile fonctionne donc en C. vcpkg installe ces trois bibliothèques sous Windows, macOS et Linux.

Les anciens tutoriels recommandent encore des bibliothèques qui sont désormais archivées ou qui ne prennent pas en charge le HTML tel qu’il est utilisé en pratique.

Bibliothèques de scraping en C et C++ (versions vcpkg, novembre 2026)

Bibliothèque

Langage

À utiliser pour

Version (vcpkg)

Statut

libcurl

C

Requêtes HTTP(S), proxys, cookies

8.23.0

Actif ; fourni avec la plupart des systèmes d'exploitation

cpr

C++

Enveloppe de type « requests » autour de libcurl

1.15.1

Actif

cpp-httplib

C++ (en-têtes uniquement)

Client et serveur HTTP simples

0.63.0

Actif

Boost.Beast

C++

HTTP asynchrone et WebSocket de bas niveau

1.92.0

Actif ; courbe d'apprentissage abrupte

lexbor

C

Analyse HTML5 + sélecteurs CSS

3.0.1

Actif ; aucune dépendance externe

libxml2

C

Analyse HTML/XML + XPath

2.15.4

Actif ; l'analyseur HTML ne permet pas de construire un arbre HTML5 complet

Gumbo

C

Analyse HTML5 (sans sélecteurs)

0.14.1 (fork communautaire)

Dépôt Google archivé ; fork sur Codeberg

pugixml

C++

XML uniquement (données d'entrée bien formées)

1.16

Actif ; ne convient pas au HTML réel

nlohmann/json

C++ (en-tête uniquement)

Sortie JSON et analyse de script-JSON

3.12.0

Actif

Ce guide utilise libcurl, lexbor et nlohmann/json. Ces deux bibliothèques principales sont écrites en C, font l’objet d’une maintenance active et sont disponibles sous forme de paquets vcpkg et Conan. lexbor ne présente aucune dépendance externe, et libcurl est déjà fourni avec la quasi-totalité des systèmes d’exploitation. Vous disposez ainsi d’un analyseur de niveau navigateur et d’un client HTTP éprouvé, sans avoir à installer de composants exotiques. Le code que vous écrivez en C++ est également transposable en C pur, presque ligne par ligne.

Configurer le projet avec CMake et vcpkg

Le projet utilise un manifeste vcpkg, un petit fichier JSON qui répertorie les bibliothèques. Lorsque CMake configure le projet à l’aide du fichier de chaîne d’outils de vcpkg, vcpkg lit le manifeste et compile toutes les dépendances pour votre plateforme. Consultez la section « Mode manifeste vcpkg » pour plus de détails. Voici la structure que vous obtiendrez :

bookscraper/
├── CMakeLists.txt
├── vcpkg.json
├── src/
│   ├── http.hpp      # libcurl client: timeouts, retries, Retry-After
│   ├── html.hpp      # lexbor RAII wrapper: parse + CSS selectors
│   ├── books.hpp     # Book struct, listing and detail parsers, URL resolution
│   ├── csv.hpp       # RFC 4180 CSV writer
│   ├── robots.hpp    # robots.txt check
│   └── main.cpp      # crawl, JavaScript, and Scraper API commands
├── tests/parser_test.cpp
└── fixtures/         # saved pages for offline tests

vcpkg.json

{
  "name": "bookscraper",
  "version": "1.0.0",
  "dependencies": ["curl", "lexbor", "nlohmann-json"]
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.21)
project(bookscraper CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(CURL REQUIRED)
find_package(lexbor CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
# lexbor exports lexbor::lexbor for shared builds and lexbor::lexbor_static for static ones
if(TARGET lexbor::lexbor)
  set(LEXBOR_TARGET lexbor::lexbor)
else()
  set(LEXBOR_TARGET lexbor::lexbor_static)
endif()

add_executable(bookscraper src/main.cpp)
target_link_libraries(bookscraper PRIVATE CURL::libcurl ${LEXBOR_TARGET} nlohmann_json::nlohmann_json)

enable_testing()
add_executable(parser_test tests/parser_test.cpp)
target_include_directories(parser_test PRIVATE src)
target_compile_definitions(parser_test PRIVATE FIXTURES="${CMAKE_CURRENT_SOURCE_DIR}/fixtures")
target_link_libraries(parser_test PRIVATE CURL::libcurl ${LEXBOR_TARGET})
add_test(NAME parser COMMAND parser_test)

Le if(TARGET ...) bloc est important. Par défaut, vcpkg compile lexbor en bibliothèque statique sous Linux, et la compilation statique exporte un nom de cible CMake différent de celui de la version partagée. En conservant les deux lignes, vous éviterez une erreur de liaison source de confusion.

Clonez et lancez vcpkg une fois, puis configurez, compilez et testez. Sous Windows, exécutez bootstrap-vcpkg.bat et utilisez des barres obliques inversées :

git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build

Sortie (extrait)

-- Found nlohmann_json: …/vcpkg_installed/x64-linux/share/nlohmann_json/nlohmann_jsonConfig.cmake (found version "3.12.0")
-- Configuring done (67.9s)
[100%] Built target bookscraper
1/1 Test #1: parser ...........................   Passed    0.00 sec
100% tests passed, 0 tests failed out of 1

La première commande `configure` compile les bibliothèques à partir du code source, ce qui prend une minute. Les commandes `configure` suivantes les réutilisent.

Si vous préférez utiliser les paquets système, Debian et Ubuntu proposent sudo apt install libcurl4-openssl-dev nlohmann-json3-dev. lexbor n'est pas inclus dans la version stable de Debian ; installez-le donc avec vcpkg ou compilez-le à partir du code source. Sous macOS, brew install curl lexbor nlohmann-json installe les trois.

Visual Studio : ouvrez le dossier du projet dans Visual Studio 2022 ou une version ultérieure, qui détecte CMakeLists.txt automatiquement. Dans CMakePresets.json, définissez CMAKE_TOOLCHAIN_FILE le chemin d’installation de vcpkg scripts/buildsystems/vcpkg.cmake, et le manifeste installe libcurl, lexbor et nlohmann/json lors de la première configuration. Vous n'avez pas besoin de définir manuellement les chemins d'inclusion, les chemins des bibliothèques ou les variables d'environnement.

Comment extraire le contenu d’une page web en C++ ?

Téléchargez la page à l’aide de libcurl dans un std::string, analysez-la avec lexbor, puis sélectionnez les éléments à l’aide de sélecteurs CSS tels que article.product_pod. Lisez le texte ou les attributs de chaque élément, puis convertissez les prix et les liens relatifs en valeurs propres. Encapsulez les descripteurs C dans de petites classes RAII afin que chaque analyseur et chaque descripteur soit libéré automatiquement.

Commencez par l’analyseur, car c’est la partie que vous manipulerez le plus. L’API de lexbor est en C pur : vous créez des objets, vous les initialisez, vous les utilisez, puis vous les détruisez. La Html classe ci-dessous masque ce cycle de vie. Son destructeur libère tout, même lorsqu’une exception est levée en cours de traitement d’une page.

// html.hpp: a small RAII layer over lexbor's HTML5 parser and CSS selector engine
#pragma once
#include <lexbor/html/html.h>
#include <lexbor/css/css.h>
#include <lexbor/selectors/selectors.h>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>

using Node = lxb_dom_node_t*;

class Html {
public:
    explicit Html(std::string_view html)
        : doc_(lxb_html_document_create()), css_(lxb_css_parser_create()), mem_(lxb_css_memory_create()),
          css_sel_(lxb_css_selectors_create()), sel_(lxb_selectors_create()) {
        if (!doc_ || !css_ || !mem_ || !css_sel_ || !sel_
            || lxb_html_document_parse(doc_, reinterpret_cast<const lxb_char_t*>(html.data()), html.size()) != LXB_STATUS_OK
            || lxb_css_parser_init(css_, nullptr) != LXB_STATUS_OK
            || lxb_css_memory_init(mem_, 128) != LXB_STATUS_OK
            || lxb_css_selectors_init(css_sel_) != LXB_STATUS_OK
            || lxb_selectors_init(sel_) != LXB_STATUS_OK) {
            release();
            throw std::runtime_error("lexbor: could not parse the document");
        }
        // Every compiled selector lives in one memory pool that is freed with the document.
        lxb_css_parser_memory_set(css_, mem_);
        lxb_css_parser_selectors_set(css_, css_sel_);
    }
    ~Html() { release(); }
    Html(const Html&) = delete;
    Html& operator=(const Html&) = delete;

    // All elements under `scope` (the whole document by default) that match a CSS selector.
    std::vector<Node> select(std::string_view css, Node scope = nullptr) const {
        std::vector<Node> found;
        lxb_css_selector_list_t* list = compile(css);
        lxb_selectors_find(sel_, scope ? scope : lxb_dom_interface_node(doc_), list,
            [](Node node, lxb_css_selector_specificity_t, void* out) -> lxb_status_t {
                static_cast<std::vector<Node>*>(out)->push_back(node);
                return LXB_STATUS_OK;
            }, &found);
        return found;
    }

    Node first(std::string_view css, Node scope = nullptr) const {
        auto nodes = select(css, scope);
        return nodes.empty() ? nullptr : nodes.front();
    }

    std::string text(Node node) const {
        if (!node) return {};
        size_t len = 0;
        lxb_char_t* raw = lxb_dom_node_text_content(node, &len);   // allocated by lexbor
        std::string out(reinterpret_cast<char*>(raw), len);
        lxb_dom_document_destroy_text(lxb_dom_interface_document(doc_), raw);
        return out;
    }

    static std::string attr(Node node, std::string_view name) {
        if (!node) return {};
        size_t len = 0;
        const lxb_char_t* value = lxb_dom_element_get_attribute(lxb_dom_interface_element(node),
            reinterpret_cast<const lxb_char_t*>(name.data()), name.size(), &len);
        return value ? std::string(reinterpret_cast<const char*>(value), len) : std::string{};
    }

private:
    // Compile each selector once per document; "h3 a" inside 20 cards is parsed a single time.
    lxb_css_selector_list_t* compile(std::string_view css) const {
        auto it = cache_.find(std::string(css));
        if (it != cache_.end()) return it->second;
        auto* list = lxb_css_selectors_parse(css_, reinterpret_cast<const lxb_char_t*>(css.data()), css.size());
        if (!list || css_->status != LXB_STATUS_OK) throw std::invalid_argument("bad CSS selector: " + std::string(css));
        return cache_.emplace(std::string(css), list).first->second;
    }
    void release() {
        if (sel_) lxb_selectors_destroy(sel_, true);
        if (css_sel_) lxb_css_selectors_destroy(css_sel_, true);
        if (css_) lxb_css_parser_destroy(css_, true);
        if (mem_) lxb_css_memory_destroy(mem_, true);     // frees every compiled selector list
        if (doc_) lxb_html_document_destroy(doc_);
        sel_ = nullptr; css_sel_ = nullptr; css_ = nullptr; mem_ = nullptr; doc_ = nullptr;
    }
    lxb_html_document_t* doc_;
    lxb_css_parser_t* css_;
    lxb_css_memory_t* mem_;
    lxb_css_selectors_t* css_sel_;
    lxb_selectors_t* sel_;
    mutable std::unordered_map<std::string, lxb_css_selector_list_t*> cache_;
};

Il est utile de connaître trois détails concernant lexbor avant de modifier cette classe :

  1. Le document, l’analyseur CSS et le moteur de sélection sont des objets distincts. Vous avez besoin des trois pour exécuter une requête, et la classe les crée et les détruit en un seul endroit, de sorte qu’aucun ne puisse survivre aux autres.
  2. Les sélecteurs compilés partagent un pool de mémoire. La classe compile chaque chaîne de sélecteur une seule fois et la met en cache pour toute la durée de vie du document ; ainsi, h3 a le contenu de 20 fiches produit n’est analysé qu’une seule fois. Ce pool explique également un plantage classique de lexbor : si vous libérez la mémoire d’un sélecteur puis analysez un autre sélecteur avec le même analyseur, lexbor lit la mémoire libérée. Un pool partagé, libéré dans le destructeur, permet d’éviter cela.
  3. Le contenu textuel est alloué automatiquement. lxb_dom_node_text_content renvoie un nouveau tampon, et vous devez le restituer avec lxb_dom_document_destroy_text. La text() fonction d’aide le copie dans un std::string et le libère immédiatement.

La documentation du module « lexbor selectors » répertorie tous les sélecteurs pris en charge, y compris :has() et :nth-child().

Ensuite, convertissez le code HTML en enregistrements typés. Chaque fiche de livre sur Books to Scrape contient un titre, un prix, une note par étoiles, une ligne d’indication de disponibilité et un lien vers la page du produit.

// books.hpp: turn Books to Scrape HTML into typed records
#pragma once
#include "html.hpp"
#include <curl/curl.h>
#include <charconv>
#include <memory>
#include <optional>
#include <string>
#include <vector>

struct Book {
    std::string title;
    long price_pence = 0;     // integer money: 51.77 GBP is stored as 5177
    int rating = 0;           // 1 to 5 stars
    bool in_stock = false;
    std::string url;          // absolute product URL
    std::string upc;          // filled from the product page
    int stock_count = 0;      // filled from the product page
};

// Resolve an href against the URL of the page it came from, using libcurl's URL API.
inline std::string resolve_url(const std::string& page_url, const std::string& href) {
    std::unique_ptr<CURLU, decltype(&curl_url_cleanup)> u(curl_url(), &curl_url_cleanup);
    if (!u || curl_url_set(u.get(), CURLUPART_URL, page_url.c_str(), 0) != CURLUE_OK
           || curl_url_set(u.get(), CURLUPART_URL, href.c_str(), 0) != CURLUE_OK)  // relative to the page
        return {};
    char* full = nullptr;
    if (curl_url_get(u.get(), CURLUPART_URL, &full, 0) != CURLUE_OK) return {};
    std::string out(full);
    curl_free(full);
    return out;
}

// "£51.77" -> 5177. Locale-independent: std::from_chars ignores the user's decimal separator.
inline std::optional<long> parse_pence(std::string_view text) {
    const auto start = text.find_first_of("0123456789");
    if (start == std::string_view::npos) return std::nullopt;
    text.remove_prefix(start);
    long whole = 0, frac = 0;
    auto [p, ec] = std::from_chars(text.data(), text.data() + text.size(), whole);
    if (ec != std::errc{}) return std::nullopt;
    if (p != text.data() + text.size() && *p == '.') {
        const char* f = p + 1;
        auto [q, ec2] = std::from_chars(f, text.data() + text.size(), frac);
        if (ec2 != std::errc{} || q - f != 2) return std::nullopt;
    }
    return whole * 100 + frac;
}

// <p class="star-rating Three"> -> 3
inline int parse_rating(const std::string& css_class) {
    static constexpr const char* words[] = {"One", "Two", "Three", "Four", "Five"};
    for (int i = 0; i < 5; ++i)
        if (css_class.find(words[i]) != std::string::npos) return i + 1;
    return 0;
}

struct ListingPage {
    std::vector<Book> books;
    std::string next_url;     // empty on the last page
};

inline ListingPage parse_listing(std::string_view html, const std::string& page_url) {
    Html doc(html);
    ListingPage page;
    for (Node card : doc.select("article.product_pod")) {
        Node link = doc.first("h3 a", card);
        Book b;
        b.title = Html::attr(link, "title");                   // the visible text is truncated with "..."
        b.url = resolve_url(page_url, Html::attr(link, "href"));
        b.price_pence = parse_pence(doc.text(doc.first(".price_color", card))).value_or(-1);
        b.rating = parse_rating(Html::attr(doc.first("p.star-rating", card), "class"));
        b.in_stock = doc.text(doc.first(".availability", card)).find("In stock") != std::string::npos;
        page.books.push_back(std::move(b));
    }
    if (Node next = doc.first("li.next a")) page.next_url = resolve_url(page_url, Html::attr(next, "href"));
    return page;
}

// Product page: UPC and "In stock (22 available)" from the information table.
inline void parse_detail(std::string_view html, Book& b) {
    Html doc(html);
    for (Node row : doc.select("table.table-striped tr")) {
        const std::string key = doc.text(doc.first("th", row));
        const std::string value = doc.text(doc.first("td", row));
        if (key == "UPC") b.upc = value;
        if (key == "Availability") {
            const auto open = value.find('(');
            if (open != std::string::npos) std::from_chars(value.data() + open + 1, value.data() + value.size(), b.stock_count);
        }
    }
}

Les prix restent exprimés en pence entiers jusqu’au moment où vous les affichez. La virgule flottante binaire ne peut pas représenter exactement 51,77 ; par conséquent, l’addition ou la comparaison double entraîne un écart de quelques fractions de penny. long Ce n’est pas le cas des valeurs. Le titre provient de l’attribut title plutôt que de son texte, car le texte visible est tronqué par « ... » pour les titres longs. parse_detail lit ensuite le tableau d’informations de la page produit, lorsque le robot explore chaque livre.

main.cpp relie les en-têtes entre eux. Il commence par les inclusions, un User-Agent valide et un petit fichier d'aide, puis s'exécute en fonction du nom de la commande :

// main.cpp: Books to Scrape crawler (libcurl + lexbor), plus the JavaScript and Scraper API paths
#include "books.hpp"
#include "csv.hpp"
#include "http.hpp"
#include "robots.hpp"
#include <nlohmann/json.hpp>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <thread>

static const std::string kUserAgent = "bookscraper/1.0 (+https://example.com/contact)";

static std::string read_file(const std::string& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in) throw std::runtime_error("cannot open " + path);
    std::ostringstream ss; ss << in.rdbuf(); return ss.str();
}
int main(int argc, char** argv) try {
    CurlGlobal curl;
    const std::string cmd = argc > 1 ? argv[1] : "crawl";
    // if (cmd == "fixture") { … } else if (cmd == "crawl") { … } … : the commands in this guide
    return 0;
} catch (const std::exception& e) {
    std::cerr << "error: " << e.what() << '\n';
    return 1;
}

Lancez d’abord l’analyseur sur une copie enregistrée de la page. Si quelque chose ne fonctionne pas à ce stade, vous savez qu’il s’agit d’un problème d’analyse et non d’un problème réseau. Il s’agit de la fixture commande :

    if (cmd == "fixture") {                       // offline: parse saved HTML
        auto page = parse_listing(read_file(argv[2]), "https://books.toscrape.com/catalogue/page-1.html");
        for (size_t i = 0; i < 3 && i < page.books.size(); ++i) {
            const Book& b = page.books[i];
            std::cout << b.title << " | " << format_price(b.price_pence) << " | " << b.rating << " stars | " << b.url << '\n';
        }
        std::cout << page.books.size() << " books, next: " << page.next_url << '\n';
    }
curl -s https://books.toscrape.com/catalogue/page-1.html -o page1.html
./build/bookscraper fixture page1.html

Sortie

A Light in the Attic | 51.77 | 3 stars | https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Tipping the Velvet | 53.74 | 1 stars | https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html
Soumission | 50.10 | 1 stars | https://books.toscrape.com/catalogue/soumission_998/index.html
20 books, next: https://books.toscrape.com/catalogue/page-2.html

Pourquoi lexbor plutôt que libxml2 ou Gumbo ?

lexbor respecte les règles d’analyse HTML du WHATWG utilisées par les navigateurs, intègre un moteur de sélecteurs CSS et fait l’objet d’une maintenance active. L’analyseur HTML de libxml2 est antérieur à HTML5 ; son arbre peut donc différer de celui affiché par DevTools. Google a archivé son dépôt Gumbo. Un fork communautaire se poursuit, mais aucune des deux versions ne dispose d’un moteur de sélecteurs.

La différence apparaît sur les balises mal structurées, que les sites réels produisent tout le temps. Voici un élément `input` mal imbriqué, <table><b>Sale</b><tr><td>9.99</td></tr></table>, analysée par lexbor, par libxml2 2.9 (la version fournie par la plupart des distributions Linux) et par Chrome :

Résultat

lexbor : <body><b>Sale</b><table><tbody><tr><td>9.99</td></tr></tbody></table></body>
libxml2: <body><table>
<b>Sale</b><tr><td>9.99</td></tr>
</table></body>
chrome  : <html><head></head><body><b>Sale</b><table><tbody><tr><td>9.99</td></tr></tbody></table></body></html>

lexbor et Chrome construisent le même arbre : l’élément <b> élément se place devant le tableau, et un <tbody> apparaît autour de la ligne. libxml2 conserve <b> à l’intérieur de la table et n’ajoute pas de <tbody>. Ainsi, un sélecteur que vous copiez depuis DevTools, tel que table > tbody > tr, correspondra sur lexbor mais ne renverra rien (sans message d’erreur) sur cet arbre libxml2. Les règles suivies par lexbor sont définies dans la spécification d’analyse syntaxique HTML du WHATWG. Les versions plus récentes de libxml2 effectuent une tokenisation plus proche de celle de HTML5, mais elles ne construisent toujours pas l’arbre HTML5 complet.

Utiliser libxml2 et XPath à la place

Si votre projet lie déjà libxml2, ou si vous raisonnez en XPath, vous pouvez analyser la même page de liste avec cet outil. Enveloppez chaque objet libxml2 dans un std::unique_ptr avec sa fonction libre correspondante, passez HTML_PARSE_NONET de sorte que l’analyseur ne récupère jamais rien depuis le réseau, et libérez chaque chaîne fournie par libxml2 avec xmlFree, et non free(). Si vous hésitez entre ces deux langages de requête, l’article « XPath vs sélecteurs CSS » les compare côte à côte.

// libxml2 alternative: the same listing page through libxml2's HTML parser and XPath.
#include <libxml/HTMLparser.h>
#include <libxml/xpath.h>
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>

int main(int argc, char** argv) {
    std::ifstream in(argc > 1 ? argv[1] : "fixtures/books-page1.html", std::ios::binary);
    std::ostringstream ss; ss << in.rdbuf(); const std::string html = ss.str();

    std::unique_ptr<xmlDoc, decltype(&xmlFreeDoc)> doc(
        htmlReadMemory(html.data(), static_cast<int>(html.size()), "https://books.toscrape.com/catalogue/page-1.html",
                       "UTF-8", HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING | HTML_PARSE_NONET),
        &xmlFreeDoc);
    if (!doc) { std::cerr << "parse failed\n"; return 1; }
    std::unique_ptr<xmlXPathContext, decltype(&xmlXPathFreeContext)> ctx(xmlXPathNewContext(doc.get()), &xmlXPathFreeContext);
    std::unique_ptr<xmlXPathObject, decltype(&xmlXPathFreeObject)> res(
        xmlXPathEvalExpression(BAD_CAST "//article[contains(@class,'product_pod')]//h3/a/@title", ctx.get()),
        &xmlXPathFreeObject);
    if (!res || !res->nodesetval) return 1;
    for (int i = 0; i < res->nodesetval->nodeNr && i < 3; ++i) {
        xmlChar* title = xmlNodeGetContent(res->nodesetval->nodeTab[i]);
        std::cout << reinterpret_cast<char*>(title) << '\n';
        xmlFree(title);                         // libxml2 memory goes back through xmlFree, not free()
    }
    std::cout << res->nodesetval->nodeNr << " titles\n";
}
g++ -std=c++20 libxml2_xpath.cpp $(pkg-config --cflags --libs libxml-2.0) -o libxml2_xpath
./libxml2_xpath page1.html

Résultat

A Light in the Attic
Tipping the Velvet
Soumission
20 titles

Comment trouver les bons sélecteurs CSS ?

Ouvrez la page dans votre navigateur, faites un clic droit sur les données, puis sélectionnez « Inspecter ». Choisissez des sélecteurs courts basés sur des classes ou des attributs stables, tels que `article.product_pod h3 a`, et testez-les dans la console DevTools avec `document.querySelectorAll`. Vérifiez ensuite le code HTML brut, car DevTools affiche le DOM analysé, y compris les éléments `tbody` insérés par les navigateurs.

Sur Books to Scrape, chaque livre se trouve dans un <article class="product_pod">. À l’intérieur, les sélecteurs dont vous avez besoin sont courts et liés à des noms de classes qui décrivent le contenu, ce qui les rend moins susceptibles de ne plus fonctionner que les chemins d’accès longs copiés avec « Copier le sélecteur » :

Données

Sélecteur (à l’intérieur de la carte)

À lire

Titre

h3 a

L' title (le texte du lien est tronqué par « ... »)

Prix

.price_color

Le texte, tel que £51.77

Note

p.star-rating

Le deuxième mot de l’ class , par exemple Three

Page suivante

li.next a (page entière)

L' href attribut

Testez chaque sélecteur dans la console avant d’écrire du code C++ : document.querySelectorAll("article.product_pod").length devrait renvoyer 20. Appuyez ensuite sur Ctrl+U pour afficher le code source et vérifier que les mêmes éléments existent bien dans le code HTML envoyé par le serveur, et pas seulement dans le DOM après l'exécution des scripts. Cette simple vérification permet de détecter rapidement les pages construites en JavaScript. Notre aide-mémoire sur les sélecteurs CSS couvre les sélecteurs d'attribut, de pseudo-classe et combinatoires lorsque les sélecteurs simples ne suffisent pas.

Comment configurer libcurl pour le web scraping ?

Créez un identifiant unique par thread et réutilisez-le pour que les connexions restent ouvertes. Définissez un User-Agent descriptif, des délais d’attente de connexion et globaux, une limite de redirections, et réglez CURLOPT_ACCEPT_ENCODING sur une chaîne vide pour activer la compression. Vérifiez vous-même le statut HTTP, ne réessayez qu’en cas d’erreurs 408, 429, 5xx et d’erreurs réseau, et respectez l’en-tête Retry-After.

La HttpClient classe encapsule un descripteur « easy » de libcurl avec ces paramètres par défaut. CurlGlobal s'exécute curl_global_init une seule fois, dans main, avant le démarrage de tout thread, car cet appel n’est pas thread-safe.

// http.hpp: one reusable libcurl handle with timeouts, an honest User-Agent, and polite retries
#pragma once
#include <curl/curl.h>
#include <algorithm>
#include <chrono>
#include <stdexcept>
#include <string>
#include <thread>

// Call curl_global_init() once, in main(), before any thread starts.
struct CurlGlobal {
    CurlGlobal() {
        if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
            throw std::runtime_error("curl_global_init failed");
    }
    ~CurlGlobal() { curl_global_cleanup(); }
    CurlGlobal(const CurlGlobal&) = delete;
    CurlGlobal& operator=(const CurlGlobal&) = delete;
};

struct Response {
    long status = 0;
    std::string body;
};

class HttpClient {
public:
    explicit HttpClient(const std::string& user_agent, long timeout_seconds = 30)
        : curl_(curl_easy_init()) {
        if (!curl_) throw std::runtime_error("curl_easy_init failed");
        curl_easy_setopt(curl_, CURLOPT_USERAGENT, user_agent.c_str());  // libcurl copies the string
        curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L);
        curl_easy_setopt(curl_, CURLOPT_MAXREDIRS, 5L);
        curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L);
        curl_easy_setopt(curl_, CURLOPT_TIMEOUT, timeout_seconds);
        curl_easy_setopt(curl_, CURLOPT_ACCEPT_ENCODING, "");  // accept every compression this libcurl supports
        curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, &HttpClient::append);
    }
    ~HttpClient() { curl_easy_cleanup(curl_); }
    HttpClient(const HttpClient&) = delete;             // one handle per thread; never share it
    HttpClient& operator=(const HttpClient&) = delete;

    // GET a URL. Retries network errors, 408, 429, and 5xx; waits for Retry-After when the server sends it.
    Response get(const std::string& url, int max_retries = 3) {
        for (int attempt = 0;; ++attempt) {
            Response r;
            curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &r.body);
            const CURLcode rc = curl_easy_perform(curl_);
            if (rc == CURLE_OK) curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &r.status);

            const bool retryable = rc != CURLE_OK || r.status == 408 || r.status == 429 || r.status >= 500;
            if (!retryable || attempt == max_retries) {
                if (rc != CURLE_OK)
                    throw std::runtime_error(std::string("GET ") + url + " failed: " + curl_easy_strerror(rc));
                return r;
            }

            curl_off_t retry_after = 0;                   // seconds, parsed by libcurl; 0 when absent
            curl_easy_getinfo(curl_, CURLINFO_RETRY_AFTER, &retry_after);
            const auto backoff = std::chrono::seconds(1LL << attempt);   // 1 s, 2 s, 4 s
            const auto wait = retry_after > 0 ? std::chrono::seconds(std::min<curl_off_t>(retry_after, 120)) : backoff;
            std::this_thread::sleep_for(wait);
        }
    }

private:
    static size_t append(char* data, size_t size, size_t count, void* out) {
        static_cast<std::string*>(out)->append(data, size * count);
        return size * count;
    }
    CURL* curl_;
};

Quatre paramètres de cette classe sont importants :

  • Non CURLOPT_FAILONERROR. Cette option transforme les erreurs HTTP en erreurs de transfert et ignore le corps de la requête. Le scraper a besoin à la fois du code d’état et du corps de la requête ; il vérifie donc lui-même le statut.
  • Il ne réessaie que lorsque cela est utile. Une erreur 404 ne se résoudra pas d’elle-même ; le client réessaie donc en cas d’erreurs réseau, de réponses 408, 429 et 5xx, jusqu’à trois fois.
  • CURLINFO_RETRY_AFTER effectue l’analyse. libcurl lit l’en-tête Retry-After sous l’une ou l’autre forme (en secondes ou sous forme de date HTTP) et vous fournit la durée en secondes. Le client limite le temps d’attente à 120 secondes, afin qu’un en-tête malveillant ou corrompu ne puisse pas bloquer l’exploration pendant des heures. En l’absence d’en-tête, il attend respectivement 1, 2 puis 4 secondes.
  • Le descripteur est réutilisé. Chaque get appel maintient la même connexion ouverte lorsque le serveur le permet, ce qui est plus rapide pour vous et moins gourmand en ressources pour le site.

Pour voir la logique de réessai à l’œuvre, dirigez le client vers un petit serveur local qui renvoie un code 429 avec Retry-After: 2 deux fois, puis un 200 :

# Local test server: the first two requests get 429 + Retry-After: 2, then 200.
import http.server, sys
hits = 0
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        global hits; hits += 1
        if hits <= 2:
            self.send_response(429); self.send_header("Retry-After", "2"); self.end_headers(); self.wfile.write(b"slow down")
        else:
            self.send_response(200); self.end_headers(); self.wfile.write(b"ok after %d tries" % hits)
    def log_message(self, fmt, *a): sys.stderr.write("server: " + fmt % a + "\n")
http.server.HTTPServer(("127.0.0.1", 8429), H).serve_forever()
#include "../src/http.hpp"
#include <iostream>
int main() {
    CurlGlobal g;
    HttpClient http("bookscraper/1.0");
    auto t0 = std::chrono::steady_clock::now();
    Response r = http.get("http://127.0.0.1:8429/");
    auto s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
    std::cout << "status " << r.status << ", body \"" << r.body << "\", waited " << s << " s\n";
}

Sortie (lignes du journal du serveur, puis le programme)

server: "GET / HTTP/1.1" 429 -
server: "GET / HTTP/1.1" 429 -
server: "GET / HTTP/1.1" 200 -
status 200, body "ok after 3 tries", waited 4.00422 s

Le client a attendu deux secondes à deux reprises, exactement comme le serveur l’a demandé, et a réussi à la troisième tentative.

User-Agent : identifiez votre robot de collecte de données et donnez aux propriétaires de sites un moyen de vous contacter, par exemple bookscraper/1.0 (+https://example.com/contact). Se faire passer pour un navigateur de bureau rend votre trafic plus difficile à distinguer d’un abus et ne le rend pas plus bienvenu. Notre guide sur les en-têtes HTTP pour le scraping web aborde les autres en-têtes qu’il vaut la peine de configurer.

Proxys : ajoutez CURLOPT_PROXY (par exemple socks5h://host:1080) et CURLOPT_PROXYUSERPWD utilisez les variables d’environnement pour passer par un proxy. Cette option correspond au drapeau -x , ce qui permet de conserver les mêmes paramètres que lors de l’utilisation de curl avec un proxy.

cpr : si vous préférez une API C++ de type « requests », cpr encapsule libcurl, et le reste de ce guide fonctionne de la même manière.

Comment extraire plusieurs pages en C++ ?

Analysez le lien « page suivante » sur chaque page de liste, résolvez-le par rapport à l’URL actuelle, puis effectuez une boucle jusqu’à ce qu’il ne reste plus de lien ou que la limite de pages soit atteinte. L’API URL de libcurl résout correctement les liens relatifs. Prévoyez une pause entre chaque requête et récupérez les pages de détail à l’aide d’un petit pool fixe de threads de travail, chacun disposant de son propre descripteur libcurl.

La boucle de crawl est courte car le parseur renvoie déjà l’URL suivante :

// 1. Follow "next" links until the last page or a page limit, pausing between requests.
static std::vector<Book> crawl(HttpClient& http, std::string url, int max_pages) {
    std::vector<Book> all;
    for (int page = 1; !url.empty() && page <= max_pages; ++page) {
        Response r = http.get(url);
        if (r.status != 200) { std::cerr << "HTTP " << r.status << " for " << url << '\n'; break; }
        ListingPage lp = parse_listing(r.body, url);
        std::cerr << "page " << page << ": " << lp.books.size() << " books\n";
        all.insert(all.end(), lp.books.begin(), lp.books.end());
        url = lp.next_url;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    return all;
}

La limite de pages vous protège des boucles infinies et vous évite de parcourir accidentellement un catalogue de 1 000 pages alors que vous aviez l’intention de tester seulement deux pages. La pause d’une demi-seconde permet de maintenir un taux de requêtes modéré.

La partie délicate consiste à resolve_url dans books.hpp. Les liens « Books to Scrape » sont relatifs : une fiche sur /catalogue/page-1.html renvoie vers a-light-in-the-attic_1000/index.html, et ce même href sur la page d’accueil renvoie à autre chose, car la page d’accueil se trouve un répertoire plus haut. L’assemblage de chaînes de caractères ne fonctionne pas dans ce genre de cas ni avec certains ../ . L’API URL de libcurl applique plutôt les règles de résolution standard : elle définit l’URL de la page comme base, ajoute le href par-dessus, puis récupère l’URL absolue. Les tests hors ligne présentés plus loin dans ce guide vérifient à la fois un chemin relatif simple et un ../ lien.

La crawl commande vérifie le fichier robots.txt (comme expliqué dans la section consacrée au scraping responsable), explore les pages de listes, visite chaque page de produit et génère les deux fichiers d’exportation :

    if (cmd == "crawl") {                  // live: listing pages, product pages, CSV + JSON
        const int max_pages = argc > 2 ? std::atoi(argv[2]) : 2;
        HttpClient http(kUserAgent);
        Response robots_txt = http.get("https://books.toscrape.com/robots.txt");
        Robots robots(robots_txt.status == 200 ? robots_txt.body : "", kUserAgent);
        std::cerr << "robots.txt: HTTP " << robots_txt.status << ", /catalogue/ allowed: "
                  << std::boolalpha << robots.allowed("/catalogue/page-1.html") << '\n';
        if (!robots.allowed("/catalogue/page-1.html")) return 1;
        auto books = crawl(http, "https://books.toscrape.com/catalogue/page-1.html", max_pages);
        enrich(books, 4);
        write_csv("books.csv", books);
        write_json("books.json", books);
        std::cout << "saved " << books.size() << " books to books.csv and books.json\n";
    }
cd build && ./bookscraper crawl 2

Sortie (environ 7 secondes pour 2 pages de listes, 40 pages de produits et les pauses)

robots.txt: HTTP 404, /catalogue/ allowed: true
page 1: 20 books
page 2: 20 books
saved 40 books to books.csv and books.json

Lorsque l’exploration elle-même devient le projet, avec des milliers de pages, la découverte de liens et la planification, l’API Crawl de WebScrapingAPI collecte les pages associées via une seule exploration délimitée.

Comment extraire des pages en parallèle ?

Lancez un nombre fixe de threads std::jthread (environ quatre) et laissez chacun d’entre eux récupérer l’index suivant à partir d’un compteur std::atomic. Attribuez à chaque thread son propre handle « easy » libcurl, car un même handle ne doit jamais être utilisé par deux threads simultanément. Enregistrez chaque résultat dans son propre emplacement, ce qui évite d’avoir recours à un mutex.

Les pages de produits étant indépendantes les unes des autres, elles se prêtent naturellement à un petit pool :

// 2. Visit product pages with a small, fixed number of workers. Each worker owns its own
//    libcurl handle, and each result goes into its own slot, so no mutex is needed.
static void enrich(std::vector<Book>& books, unsigned workers) {
    std::atomic<size_t> next{0};
    std::vector<std::jthread> pool;
    for (unsigned w = 0; w < workers; ++w)
        pool.emplace_back([&] {
            HttpClient http(kUserAgent);
            for (size_t i = next++; i < books.size(); i = next++) {
                try {
                    Response r = http.get(books[i].url);
                    if (r.status == 200) parse_detail(r.body, books[i]);
                } catch (const std::exception& e) { std::cerr << e.what() << '\n'; }
            }
        });
}   // jthread joins on destruction

Chaque thread crée son propre HttpClient à l’intérieur du thread, puis effectue une boucle : récupérer l’index suivant, récupérer la page du livre correspondant et remplir upc et stock_count sur celui-ci Book. Comme aucun travailleur ne touche jamais le même élément et que la taille du vecteur ne change jamais pendant la boucle, il n’y a rien à verrouiller. std::jthread joins s’exécute automatiquement lors de sa destruction, de sorte que la fonction ne peut pas retourner tant qu’un worker est encore en train d’écrire. Il n’y a aucun join appels à ne pas oublier.

Quatre workers constituent une valeur par défaut raisonnable pour un site unique. Ajouter davantage de threads alourdit généralement la charge sur la cible. Si vous avez un jour besoin d’effectuer des centaines de transferts en parallèle, tournez-vous vers l’interface « multi » de libcurl, qui gère de nombreux transferts à partir d’un seul thread, au lieu d’ajouter des threads.

Comment enregistrer les données extraites au format CSV ou JSON ?

Stockez les prix sous forme d’entiers en cents ou en pence, puis générez un fichier CSV conforme à la norme RFC 4180 : mettez entre guillemets les champs contenant des virgules, des guillemets ou des sauts de ligne, doublez les guillemets internes et terminez les lignes par CRLF. Pour le format JSON, la fonction `ordered_json` de nlohmann/json conserve les champs dans l’ordre où vous les avez ajoutés. Ces deux formats s’ouvrent sans problème dans les tableurs et les outils de traitement de données.

Les titres de livres perturbent rapidement un fichier CSV standard. Books to Scrape contient des titres comportant des virgules et des parenthèses dès les premières pages ; si l’on séparait les champs par de simples virgules, toutes les colonnes suivantes seraient décalées. L’exemple ci-dessous respecte la norme RFC 4180 :

// csv.hpp: RFC 4180 output, so titles with commas or quotes stay in one column
#pragma once
#include "books.hpp"
#include <cstdio>
#include <fstream>
#include <string>
#include <vector>

inline std::string csv_field(const std::string& s) {
    if (s.find_first_of(",\"\r\n") == std::string::npos) return s;
    std::string out = "\"";
    for (char c : s) {
        if (c == '"') out += '"';   // a quote inside a field is written twice
        out += c;
    }
    return out + '"';
}

inline std::string format_price(long pence) {
    char buf[32];
    std::snprintf(buf, sizeof buf, "%ld.%02ld", pence / 100, pence % 100);
    return buf;
}

inline void write_csv(const std::string& path, const std::vector<Book>& books) {
    std::ofstream out(path, std::ios::binary);       // binary: "\r\n" stays exactly as written on Windows
    out << "title,price_gbp,rating,in_stock,stock_count,upc,url\r\n";
    for (const Book& b : books)
        out << csv_field(b.title) << ',' << format_price(b.price_pence) << ',' << b.rating << ','
            << (b.in_stock ? "true" : "false") << ',' << b.stock_count << ',' << csv_field(b.upc) << ','
            << csv_field(b.url) << "\r\n";
    if (!out) throw std::runtime_error("could not write " + path);
}

Le fichier s'ouvre volontairement en mode binaire. En mode texte, Windows transforme chaque \n en \r\n, de sorte que les \r\n fin de ligne s’afficherait sous la forme \r\r\n. Le mode binaire écrit exactement les octets demandés sur toutes les plateformes.

L'éditeur JSON utilise ordered_json, de sorte que les champs apparaissent dans le même ordre que les colonnes du fichier CSV, et non par ordre alphabétique :

static void write_json(const std::string& path, const std::vector<Book>& books) {
    nlohmann::ordered_json arr = nlohmann::ordered_json::array();   // keeps field order
    for (const Book& b : books)
        arr.push_back({{"title", b.title}, {"price_gbp", b.price_pence / 100.0}, {"rating", b.rating},
                       {"in_stock", b.in_stock}, {"stock_count", b.stock_count}, {"upc", b.upc}, {"url", b.url}});
    std::ofstream(path) << arr.dump(2) << '\n';
}

Sortie : books.csv (premières lignes, plus la ligne qui doit être mise entre guillemets)

title,price_gbp,rating,in_stock,stock_count,upc,url
A Light in the Attic,51.77,3,true,22,a897fe39b1053632,https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Tipping the Velvet,53.74,1,true,20,90fa61229261140a,https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html
…
"Starving Hearts (Triangular Trade Trilogy, #1)",13.99,2,true,19,0312262ecafa5a40,https://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html

Sortie : books.json (premier enregistrement)

{
  "title": "A Light in the Attic",
  "price_gbp": 51.77,
  "rating": 3,
  "in_stock": true,
  "stock_count": 22,
  "upc": "a897fe39b1053632",
  "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
}

Le titre « Starving Hearts » reste dans une seule colonne car la virgule se trouve entre guillemets. Les prix ont été analysés avec std::from_chars, qui ignore les paramètres régionaux de l’utilisateur ; ainsi, un système configuré pour utiliser des virgules comme séparateurs décimaux ne peut pas modifier le résultat. Le prix est analysé en deux entiers, les livres et les pence, car la prise en charge des nombres à virgule flottante from_chars diffère encore d’une bibliothèque standard à l’autre.

Comment extraire les données d’un site web en C pur ?

Utilisez les deux mêmes bibliothèques sans les wrappers C++ : libcurl écrit la réponse dans un tampon extensible via une fonction de rappel d’écriture, et lexbor analyse le tampon et exécute les sélecteurs CSS. Libérez chaque descripteur à chaque chemin d’accès. Une seule étiquette de nettoyage avec goto permet de garder cela gérable. Le programme C complet fait moins de 80 lignes.

Tout ce qui précède fonctionne également en C17. Le programme ci-dessous récupère la première page « Books to Scrape » et affiche chaque titre :

/* scrape.c: plain C17. Fetch one Books to Scrape page with libcurl, print titles and prices with lexbor. */
#include <curl/curl.h>
#include <lexbor/html/html.h>
#include <lexbor/css/css.h>
#include <lexbor/selectors/selectors.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct { char *data; size_t len; } buffer;

static size_t on_data(char *ptr, size_t size, size_t n, void *userdata) {
    buffer *buf = userdata;
    size_t add = size * n;
    char *grown = realloc(buf->data, buf->len + add + 1);
    if (!grown) return 0;                       /* returning less than add makes libcurl abort the transfer */
    buf->data = grown;
    memcpy(buf->data + buf->len, ptr, add);
    buf->len += add;
    buf->data[buf->len] = '\0';
    return add;
}

static lxb_status_t on_match(lxb_dom_node_t *node, lxb_css_selector_specificity_t spec, void *ctx) {
    (void) spec; (void) ctx;
    size_t len = 0;
    const lxb_char_t *title = lxb_dom_element_get_attribute(lxb_dom_interface_element(node),
                                                            (const lxb_char_t *) "title", 5, &len);
    if (title) printf("%.*s\n", (int) len, (const char *) title);
    return LXB_STATUS_OK;
}

int main(void) {
    const char *url = "https://books.toscrape.com/catalogue/page-1.html";
    static const char selector[] = "article.product_pod h3 a";
    buffer page = {0};
    long status = 0;
    int rc = EXIT_FAILURE;

    if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return EXIT_FAILURE;
    CURL *curl = curl_easy_init();
    if (!curl) goto cleanup_global;
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "bookscraper-c/1.0 (+https://example.com/contact)");
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_data);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &page);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) { fprintf(stderr, "request failed: %s\n", curl_easy_strerror(res)); goto cleanup_curl; }
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    if (status != 200) { fprintf(stderr, "HTTP %ld\n", status); goto cleanup_curl; }

    lxb_html_document_t *doc = lxb_html_document_create();
    lxb_css_parser_t *parser = lxb_css_parser_create();
    lxb_selectors_t *sel = lxb_selectors_create();
    lxb_css_selector_list_t *list = NULL;
    if (doc && parser && sel
        && lxb_html_document_parse(doc, (const lxb_char_t *) page.data, page.len) == LXB_STATUS_OK
        && lxb_css_parser_init(parser, NULL) == LXB_STATUS_OK
        && lxb_selectors_init(sel) == LXB_STATUS_OK
        && (list = lxb_css_selectors_parse(parser, (const lxb_char_t *) selector, sizeof selector - 1)) != NULL) {
        lxb_selectors_find(sel, lxb_dom_interface_node(doc), list, on_match, NULL);
        rc = EXIT_SUCCESS;
    }
    if (list) lxb_css_selector_list_destroy_memory(list);
    if (sel) lxb_selectors_destroy(sel, true);
    if (parser) lxb_css_parser_destroy(parser, true);
    if (doc) lxb_html_document_destroy(doc);
cleanup_curl:
    curl_easy_cleanup(curl);
cleanup_global:
    curl_global_cleanup();
    free(page.data);
    return rc;
}
gcc -std=c17 -Wall -Wextra scrape.c $(pkg-config --cflags --libs libcurl lexbor) -o scrape
./scrape

Sortie (3 premières lignes sur 20)

A Light in the Attic
Tipping the Velvet
Soumission
…

Si vous avez installé les bibliothèques avec vcpkg, pointez PKG_CONFIG_PATH vers vcpkg_installed/<triplet>/lib/pkgconfig d’abord, car vcpkg fournit libcurl.pc et lexbor.pc fichiers à cet emplacement.

Deux pièges font trébucher la plupart des scrapers en C :

  • realloc peut échouer. Attribuez son résultat à un pointeur temporaire et vérifiez-le avant de réécrire buf->data. L'écriture buf->data = realloc(buf->data, …) entraîne une fuite de l’ancien tampon lorsque la mémoire vient à manquer.
  • La fonction de rappel d’écriture doit renvoyer le nombre d’octets qu’elle a traités. Toute autre valeur indique à libcurl qu’un problème est survenu, et celle-ci interrompt le transfert. C’est également ainsi que la fonction de rappel signale un échec d’allocation : elle renvoie 0.

Les goto étiquettes fournissent à la fonction un chemin de sortie par ressource, de sorte que les objets libcurl et lexbor sont libérés dans l’ordre inverse, quel que soit l’endroit où une erreur se produit.

Comment savoir si une page nécessite du JavaScript ?

Récupérez la page avec libcurl et comptez les éléments attendus. Si le nombre est égal à zéro mais que votre navigateur affiche des données, cela signifie que la page génère son contenu à l’aide de JavaScript. Avant de vous tourner vers un navigateur, recherchez les balises `script` dans le code HTML brut et consultez l’onglet « Réseau ». Les données s’y trouvent souvent déjà au format JSON, que vous pouvez analyser directement.

La /js/ version de « Quotes to Scrape » semble identique à celle affichée normalement dans un navigateur. Cependant, si vous la récupérez avec libcurl, les éléments de citation ne sont pas présents. Les citations se trouvent dans un tableau JavaScript à l’intérieur d’une balise <script> balise, et le script de la page elle-même construit le code HTML à partir de celui-ci après le chargement.

Cela signifie que vous pouvez souvent vous passer complètement du navigateur. Cette fonction recherche le script qui définit le tableau et transmet ce dernier à nlohmann/json :

// 3. JavaScript pages, option A: the data is often already in the HTML, inside a <script>.
static nlohmann::json quotes_from_script(const std::string& html) {
    Html doc(html);
    for (Node script : doc.select("script")) {
        const std::string js = doc.text(script);
        const auto start = js.find("var data = [");
        if (start == std::string::npos) continue;
        const auto open = js.find('[', start);
        const auto close = js.find("];", open);   // first "];" after the array start ends the literal
        return nlohmann::json::parse(js.substr(open, close - open + 1));
    }
    return nlohmann::json::array();
}
    if (cmd == "js") {                     // live: plain fetch of a JavaScript page
        HttpClient http(kUserAgent);
        const std::string html = http.get("https://quotes.toscrape.com/js/").body;
        Html doc(html);
        std::cout << "div.quote elements in raw HTML: " << doc.select("div.quote").size() << '\n';
        auto quotes = quotes_from_script(html);
        std::cout << "quotes found in <script> JSON: " << quotes.size() << '\n'
                  << "first: " << quotes[0]["text"].get<std::string>() << " - "
                  << quotes[0]["author"]["name"].get<std::string>() << '\n';
    }

Résultat

div.quote elements in raw HTML: 0
quotes found in <script> JSON: 10
first: “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.” - Albert Einstein

Zéro élément dans le code HTML, dix citations dans le script. L’extraction recherche var data = [, puis récupère tout ce qui se trouve jusqu’à la première balise ]; qui le suit. Il s’agit d’un modèle spécifique à cette page, et non d’un analyseur JavaScript général. Sur d’autres sites, recherchez application/json ou application/ld+json script, ou ouvrez l'onglet « Réseau » et appelez le point de terminaison JSON utilisé par la page elle-même.

Comment extraire des données de pages rendues en JavaScript en C++ ?

Lancez Chrome en mode sans interface graphique avec l’option --dump-dom pour exécuter les scripts de la page et afficher le code HTML final, puis analysez cette sortie avec lexbor. Pour les clics, les connexions ou les délais d’attente, contrôlez Chrome via WebDriver ou le protocole DevTools sur HTTP. Playwright ne dispose pas de liaison officielle en C++. À grande échelle, une API de rendu permet de se passer complètement d’une flotte de navigateurs.

Lorsque les données ne se trouvent pas dans une balise script, laissez un véritable navigateur construire la page. Un navigateur sans interface graphique s’exécute sans fenêtre, et le drapeau --dump-dom affiche le DOM rendu sur la sortie standard, que le C++ peut lire via un tube :

// 3. JavaScript pages, option B: let headless Chrome run the scripts, then parse the DOM it prints.
//    POSIX popen; the URL is a constant here. Never paste untrusted input into a shell command.
static std::string render_with_chrome(const std::string& url) {
    const char* chrome = std::getenv("CHROME");
    const std::string cmd = std::string(chrome ? chrome : "google-chrome") +
        " --headless --disable-gpu --dump-dom '" + url + "' 2>/dev/null";
    std::unique_ptr<FILE, int (*)(FILE*)> pipe(popen(cmd.c_str(), "r"), pclose);
    if (!pipe) throw std::runtime_error("could not start Chrome");
    std::string dom; char buf[8192];
    for (size_t n; (n = std::fread(buf, 1, sizeof buf, pipe.get())) > 0;) dom.append(buf, n);
    return dom;
}
    if (cmd == "render") {                 // live: headless Chrome, then lexbor
        Html doc(render_with_chrome("https://quotes.toscrape.com/js/"));
        auto quotes = doc.select("div.quote");
        std::cout << "div.quote elements after rendering: " << quotes.size() << '\n';
        if (!quotes.empty())
            std::cout << "first author: " << doc.text(doc.first("small.author", quotes[0])) << '\n';
    }

Sortie (quelques secondes, démarrage de Chrome compris)

div.quote elements after rendering: 10
first author: Albert Einstein

La même Html classe et les mêmes sélecteurs CSS fonctionnent sur la sortie de Chrome, car lexbor et Chrome analysent le HTML de la même manière. La documentation sur le mode sans interface graphique de Chrome couvre les autres indicateurs.

Gardez trois limites à l’esprit :

  • Ne construisez jamais la commande à partir d’une entrée non fiable. L’URL utilisée ici est une constante. Une URL extraite d’une page ou fournie par un utilisateur pourrait contenir de la syntaxe shell, et popen l'exécuterait. Sous Windows, utilisez _popen et _pclose en plaçant l’URL entre guillemets doubles.
  • --dump-dom affiche le DOM après l’événement « load ». Il n’attend pas les données qui arrivent ultérieurement via des appels réseau supplémentaires. Lorsqu’une page charge du contenu après cela, ou si vous avez besoin de clics, de connexions ou de défilement, contrôlez Chrome via WebDriver ou le protocole Chrome DevTools, qui utilisent tous deux HTTP ou WebSocket, accessibles par libcurl.
  • Définissez la CHROME variable d’environnement sur le chemin d’accès au navigateur sous macOS et Windows, où google-chrome ne figure pas dans le PATH.

Pour les sessions de navigation multiples impliquant des connexions et des interactions, l’API navigateur de WebScrapingAPI gère les navigateurs à votre place.

Comment utiliser une API de web scraping depuis C++ ?

Envoyez une requête GET avec libcurl vers le point de terminaison de l’API Scraper de WebScrapingAPI, en transmettant votre clé API, l’URL cible encodée en URL et des options telles que render_js=1 et country=us. Le corps de la réponse correspond au code HTML de la page ; votre analyseur lexbor existant fonctionne donc sans modification. Lisez la clé à partir de la variable d’environnement WSA_API_KEY.

Lancer Chrome vous-même convient pour quelques pages. Lorsque vous avez besoin du rendu JavaScript, d’un pays spécifique ou d’un accès à des sites qui bloquent les scrapers après des milliers de requêtes, une API de scraping prend le relais des navigateurs et des proxys. Comme elle renvoie du code HTML, la seule modification de votre côté consiste à utiliser une URL :

// 4. Hand-off: the same libcurl client, pointed at WebScrapingAPI's Scraper API.
static std::string scraper_api_url(const std::string& api_key, const std::string& target,
                                   bool render_js, const std::string& country) {
    std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> c(curl_easy_init(), &curl_easy_cleanup);
    auto escape = [&](const std::string& s) {
        char* e = curl_easy_escape(c.get(), s.c_str(), static_cast<int>(s.size()));
        std::string out(e); curl_free(e); return out;
    };
    std::string url = "https://api.webscrapingapi.com/v2?api_key=" + escape(api_key) + "&url=" + escape(target);
    if (render_js) url += "&render_js=1";
    if (!country.empty()) url += "&country=" + escape(country);
    return url;
}
    if (cmd == "api") {                    // Scraper API: needs WSA_API_KEY; prints a dry run without it
        const char* key = std::getenv("WSA_API_KEY");
        const std::string url = scraper_api_url(key ? key : "YOUR_API_KEY",
                                                "https://quotes.toscrape.com/js/", true, "us");
        if (!key) { std::cout << "dry run (set WSA_API_KEY to send it):\n" << url << '\n'; return 0; }
        HttpClient http(kUserAgent, 90);          // rendered requests take longer
        Response r = http.get(url, 1);
        if (r.status != 200) { std::cerr << "Scraper API returned HTTP " << r.status << ": " << r.body << '\n'; return 1; }
        Html doc(r.body);
        std::cout << "div.quote elements via Scraper API: " << doc.select("div.quote").size() << '\n';
    }

curl_easy_escape encode l’URL cible, de sorte que sa propre ? et & ne puissent pas être confondus avec les paramètres de l’API. Le client dispose d’un délai d’expiration de 90 secondes, car le rendu d’une page prend plus de temps que sa récupération, et d’une seule tentative de réessai, ce qui permet de détecter rapidement une clé erronée. Ne mentionnez pas la clé dans le code source ni dans les fichiers d’historique du shell que vous partagez :

export WSA_API_KEY="your-key-from-the-dashboard"   # PowerShell: $env:WSA_API_KEY = "..."
./bookscraper api

Sans cette variable, la commande affiche la requête qu’elle enverrait :

Sortie sans WSA_API_KEY

dry run (set WSA_API_KEY to send it):
https://api.webscrapingapi.com/v2?api_key=YOUR_API_KEY&url=https%3A%2F%2Fquotes.toscrape.com%2Fjs%2F&render_js=1&country=us

Avec une clé incorrecte, l’API renvoie un code d’erreur 401 accompagné d’un message d’erreur au format JSON, et le programme l’affiche au lieu d’essayer de l’analyser :

Sortie avec une clé non valide

Scraper API returned HTTP 401: {"error":"Invalid API key"}

Exemple de sortie avec une clé valide (simulé à titre d’illustration)

div.quote elements via Scraper API: 10

La page affichée devrait contenir les dix citations générées par Chrome, et le même sélecteur devrait les trouver. WebScrapingAPI indique également les crédits utilisés par chaque requête dans les en-têtes de réponse, préfixés par WSA, que vous pouvez lire avec CURLOPT_HEADERFUNCTION si vous souhaitez suivre l'utilisation depuis C++.

Quelle approche de scraping en C++ devriez-vous utiliser ?

Utilisez libcurl et lexbor pour les pages statiques, et examinez les balises script avant d’ajouter un navigateur. Utilisez Chrome sans interface graphique lorsque la page nécessite du JavaScript et que le volume est faible. Passez à une API de scraping lorsque vous avez besoin d’un rendu JavaScript, d’un ciblage géographique ou d’une rotation de proxys à grande échelle sans avoir à exécuter vous-même des navigateurs.

Page ou besoin

Utilisez

Pourquoi

Coût

HTML statique (livres à extraire)

libcurl + lexbor

Le plus rapide, le moins de composants mobiles

Uniquement votre bande passante

Données dans un <script> (Citations /js/)

libcurl + nlohmann/json

Aucun navigateur requis

Uniquement votre bande passante

Contenu généré après le chargement, peu de pages

Chrome sans interface graphique --dump-dom + lexbor

Rendu en temps réel du navigateur

Une seule installation de Chrome ; quelques secondes par page

Clics, connexions, temps d'attente

WebDriver ou protocole DevTools via HTTP

Contrôle total du navigateur

Parc de navigateurs à exécuter et à mettre à jour

JavaScript, géolocalisation ou blocage à grande échelle

API de scraping (render_js, country)

Rendu et proxys gérés pour vous

Crédits par requête

Comment tester et déboguer un scraper C++ ?

Enregistrez des pages réelles en tant que fixtures et testez votre analyseur par rapport à celles-ci hors ligne avec CTest, afin que les échecs soient attribués à l’analyse et non au réseau. Compilez des binaires de test avec AddressSanitizer et UndefinedBehaviorSanitizer pour détecter les fuites et les erreurs de mémoire liées aux appels aux bibliothèques C, et exécutez ThreadSanitizer sur le robot d’indexation concurrent. Actualisez les fixtures lorsque le site change.

Enregistrez les pages dont dépend votre analyseur à l’aide de curl -o : fixtures/: une page de liste et une page de produit suffisent ici. Le test ci-dessous vérifie le parseur, la résolution des URL, l’analyse des prix, les guillemets dans les fichiers CSV et les règles du fichier robots.txt, sans toucher au réseau :

// parser_test.cpp: offline checks against saved pages, run with ctest
#include "books.hpp"
#include "csv.hpp"
#include "robots.hpp"
#include <fstream>
#include <iostream>
#include <sstream>

static int failures = 0;
#define CHECK(cond) do { if (!(cond)) { std::cerr << "FAIL line " << __LINE__ << ": " #cond "\n"; ++failures; } } while (0)

static std::string slurp(const std::string& p) { std::ifstream f(p, std::ios::binary); std::ostringstream s; s << f.rdbuf(); return s.str(); }

int main() {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    const std::string base = "https://books.toscrape.com/catalogue/page-1.html";
    auto page = parse_listing(slurp(FIXTURES "/books-page1.html"), base);
    CHECK(page.books.size() == 20);
    CHECK(page.books[0].title == "A Light in the Attic");
    CHECK(page.books[0].price_pence == 5177);
    CHECK(page.books[0].rating == 3);
    CHECK(page.books[0].in_stock);
    CHECK(page.books[0].url == "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html");
    CHECK(page.next_url == "https://books.toscrape.com/catalogue/page-2.html");

    // The same href means a different URL on the home page: resolve against the page you fetched.
    CHECK(resolve_url("https://books.toscrape.com/index.html", "catalogue/page-2.html")
          == "https://books.toscrape.com/catalogue/page-2.html");
    CHECK(resolve_url("https://books.toscrape.com/catalogue/page-2.html", "../index.html")
          == "https://books.toscrape.com/index.html");

    Book b; parse_detail(slurp(FIXTURES "/book-detail.html"), b);
    CHECK(b.upc == "a897fe39b1053632");
    CHECK(b.stock_count == 22);

    CHECK(parse_pence("£0.99") == 99);
    CHECK(!parse_pence("free").has_value());
    CHECK(csv_field("Starving Hearts (Triangular Trade Trilogy, #1)") == "\"Starving Hearts (Triangular Trade Trilogy, #1)\"");
    CHECK(csv_field("6\" ruler") == "\"6\"\" ruler\"");

    // robots.txt: a 404 means no rules; otherwise the most specific matching rule wins.
    CHECK(Robots("", "bookscraper").allowed("/catalogue/page-1.html"));
    const std::string txt = "User-agent: *\nDisallow: /private/\nAllow: /private/public-page\n\n"
                            "User-agent: bookscraper\nDisallow: /search\n";
    CHECK(Robots(txt, "bookscraper/1.0").allowed("/private/x"));          // our own group replaces "*"
    CHECK(!Robots(txt, "bookscraper/1.0").allowed("/search?q=c"));
    CHECK(!Robots(txt, "otherbot").allowed("/private/x"));
    CHECK(Robots(txt, "otherbot").allowed("/private/public-page"));

    curl_global_cleanup();
    std::cout << (failures ? "FAILED" : "all parser checks passed") << '\n';
    return failures ? 1 : 0;
}

Exécutez-le via CTest, puis compilez le même test avec les sanitizers. ThreadSanitizer nécessite sa propre compilation, car il ne peut pas être combiné avec AddressSanitizer :

ctest --test-dir build --output-on-failure

# Memory and undefined-behavior checks (GCC or Clang)
g++ -std=c++20 -g -fsanitize=address,undefined -Isrc -DFIXTURES='"fixtures"' tests/parser_test.cpp $(pkg-config --cflags --libs libcurl lexbor) -o parser_test_asan && ./parser_test_asan

# Data races in the crawler (separate build; TSan can't be combined with ASan)
g++ -std=c++20 -g -fsanitize=thread src/main.cpp $(pkg-config --cflags --libs libcurl lexbor) -o bookscraper_tsan && ./bookscraper_tsan crawl 1

Sortie

all parser checks passed

100% tests passed, 0 tests failed out of 1

Une exécution propre des sanitizers n’affiche que la sortie propre au programme. Toute fuite de mémoire, lecture hors limites ou conflit d’accès génère un rapport indiquant la ligne exacte concernée.

Deux habitudes de débogage permettent de gagner le plus de temps. Tout d’abord, définissez CURLOPT_VERBOSE sur 1 sur le descripteur pour voir tous les en-têtes de requête et de réponse, ce qui permet de repérer rapidement les redirections, la compression et les codes d'état inattendus. Ensuite, lorsqu'un sélecteur ne renvoie rien, redirigez r.body dans un fichier et ouvrez-le. Le code HTML que vous avez reçu correspond souvent à une page de consentement, une page d’erreur ou un shell JavaScript, et non à la page que vous avez vue dans le navigateur.

Comment effectuer un scraping de manière responsable avec C ou C++ ?

Lisez le fichier robots.txt avant de lancer l’exploration et ignorez les chemins interdits, identifiez votre robot de scraping à l’aide d’un User-Agent honnête comprenant une URL de contact, et espacez vos requêtes. Ne collectez que les données publiques dont vous avez besoin, évitez les données à caractère personnel sauf si vous disposez d’une base légale, et respectez les conditions d’utilisation du site. Arrêtez-vous immédiatement lorsque le serveur renvoie un code 429.

Le crawler s’occupe déjà de la plupart de ces points : il envoie un User-Agent d’identification, marque une pause entre chaque page, se limite à quatre threads et attend le temps indiqué par l’en-tête Retry-After. Il ne reste plus que le fichier robots.txt. Ce petit vérificateur respecte la norme RFC 9309 :

// robots.hpp: a small RFC 9309 check for one user agent (longest matching rule wins; Allow wins ties)
#pragma once
#include <algorithm>
#include <cctype>
#include <sstream>
#include <string>
#include <vector>

class Robots {
public:
    // body = robots.txt contents; pass an empty string when the file returns 404 (everything is allowed)
    Robots(const std::string& body, std::string agent) {
        std::transform(agent.begin(), agent.end(), agent.begin(), tolower_ascii);
        std::istringstream in(body);
        std::string line;
        bool in_group = false, group_has_rules = false, matched_specific = false;
        std::vector<Rule> star, mine;
        std::vector<Rule>* target = nullptr;
        while (std::getline(in, line)) {
            line = trim(line.substr(0, line.find('#')));
            const auto colon = line.find(':');
            if (colon == std::string::npos) continue;
            std::string key = trim(line.substr(0, colon)), value = trim(line.substr(colon + 1));
            std::transform(key.begin(), key.end(), key.begin(), tolower_ascii);
            if (key == "user-agent") {
                if (!in_group || group_has_rules) { target = nullptr; group_has_rules = false; }
                in_group = true;
                std::transform(value.begin(), value.end(), value.begin(), tolower_ascii);
                if (value == "*") { if (!target) target = &star; }
                else if (agent.find(value) != std::string::npos) { target = &mine; matched_specific = true; }
            } else if (key == "allow" || key == "disallow") {
                group_has_rules = true;
                if (target && !value.empty()) target->push_back({value, key == "allow"});
            }
        }
        rules_ = matched_specific ? mine : star;
    }

    bool allowed(const std::string& path) const {
        size_t best = 0; bool allow = true;
        for (const Rule& r : rules_)
            if (path.compare(0, r.prefix.size(), r.prefix) == 0 &&
                (r.prefix.size() > best || (r.prefix.size() == best && r.allow))) {
                best = r.prefix.size(); allow = r.allow;
            }
        return allow;
    }

private:
    struct Rule { std::string prefix; bool allow; };
    static char tolower_ascii(char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }
    static std::string trim(const std::string& s) {
        const auto b = s.find_first_not_of(" \t\r"), e = s.find_last_not_of(" \t\r");
        return b == std::string::npos ? "" : s.substr(b, e - b + 1);
    }
    std::vector<Rule> rules_;
};

il sélectionne le groupe correspondant à votre User-Agent, ou se rabat sur le * groupe s’il n’y en a pas, et la règle correspondante la plus longue l’emporte, en cas Allow les ex aequo étant pris en compte. Un fichier robots.txt renvoyant un code 404 signifie qu’il n’y a pas de règles, ce qui est le cas pour Books to Scrape :

robots.txt: HTTP 404, /catalogue/ allowed: true

Ce vérificateur est volontairement simple. Il ne gère pas les * et $ caractères génériques autorisés par la RFC 9309 dans les chemins d’accès ; utilisez donc un analyseur complet pour les explorations en production de sites que vous ne contrôlez pas.

Le fichier robots.txt est une convention d’exploration, pas une licence. Ce que vous pouvez collecter dépend également des données, des conditions d’utilisation du site et de la législation en vigueur là où vous opérez. Lisez « Le web scraping est-il légal ? » avant de procéder à tout scraping dans le cadre d’un projet commercial.

Foire aux questions

Le C++ est-il plus rapide que Python pour le web scraping ?

Pour l’analyse et le traitement, généralement oui : le C++ compilé avec lexbor utilise moins de CPU et de mémoire par page que la plupart des piles Python. Pour la plupart des scrapers, cependant, la latence réseau et les délais de politesse dominent le temps total, de sorte que le gain réel est inférieur à ce que suggèrent les benchmarks. Optez pour le C++ lorsque le volume d’analyse, les limites de mémoire ou une base de code existante le justifient.

Quelle est la différence entre le C, le C++ et le C# pour le web scraping ?

Le C et le C++ se compilent en code natif et utilisent des bibliothèques C telles que libcurl et lexbor ; une même pile suffit donc pour les deux. Le C# s’exécute sur .NET et dispose de ses propres outils, notamment HttpClient, AngleSharp et Playwright pour .NET. Les noms se ressemblent, mais les bibliothèques ne se recoupent pas. Les développeurs C# doivent suivre notre guide dédié au C#.

Quel est le meilleur analyseur HTML pour le C++ ?

lexbor est le choix par défaut le plus performant en 2026 : il implémente l’algorithme d’analyse syntaxique HTML5 du WHATWG, intègre un moteur de sélecteurs CSS, ne présente aucune dépendance externe et s’installe via vcpkg sous Windows, macOS et Linux. Utilisez libxml2 si vous avez besoin d’XPath ou si vous en dépendez déjà, et pugixml uniquement pour du XML bien formé, pas pour du HTML tel qu’il est utilisé en pratique.

Dois-je utiliser cpr ou libcurl pour le web scraping ?

Les deux fonctionnent, car cpr est un wrapper C++ autour de libcurl doté d’une API plus conviviale, de type requests. libcurl « brut » vous offre toutes les options, telles que l’analyse intégrée de Retry-After et l’API URL, avec une dépendance en moins, et les mêmes appels fonctionnent en C pur. Choisissez cpr si vous préférez un code plus court et n’écrivez qu’en C++.

Lexbor fonctionne-t-il sous Windows et dans Visual Studio ?

Oui. Le portage vcpkg de lexbor se compile sur Windows, macOS et Linux. Seules les applications Universal Windows Platform sont exclues. Dans Visual Studio, ouvrez le dossier du projet, pointez CMAKE_TOOLCHAIN_FILE vers le fichier de chaîne d’outils de vcpkg, et le manifeste installera libcurl, lexbor et nlohmann/json lors de la première configuration. Aucun chemin d’accès manuel aux bibliothèques n’est nécessaire.

Le C++ permet-il d’extraire des données de sites web en JavaScript ?

Oui, mais pas avec libcurl seul, car libcurl n’exécute pas de JavaScript. Vérifiez d’abord si les données se trouvent déjà dans une balise script au format JSON. Sinon, lancez Chrome en mode sans interface graphique avec l’option --dump-dom, contrôlez un navigateur via WebDriver ou le protocole DevTools, ou appelez une API de scraping avec le rendu JavaScript activé, puis analysez le code HTML renvoyé.

Comment éviter d’être bloqué lors d’un scraping en C++ ?

Envoyez moins de requêtes, plus lentement, avec un User-Agent valide, et respectez l’en-tête Retry-After lorsqu’un serveur renvoie un code 429. Réutilisez les connexions au lieu d’en ouvrir de nouvelles, et n’exécutez pas plus de workers que le site ne peut en gérer. Pour les sites protégés, acheminez les requêtes via des proxys tournants avec CURLOPT_PROXY, ou utilisez une API de scraping qui gère les proxys et les navigateurs.

Le scraping Web avec C++ est-il légal ?

Le scraping de données publiques est légal dans de nombreuses juridictions, mais la réponse dépend de ce que vous collectez, de la manière dont vous le collectez et des conditions d’utilisation du site. Évitez les données à caractère personnel sans fondement légal, ne contournez jamais les identifiants de connexion ou les barrières de paiement, respectez le fichier robots.txt et demandez un avis juridique pour les projets commerciaux. Le langage de programmation que vous utilisez ne change rien aux règles.

Comment utiliser un proxy avec libcurl ?

Définissez CURLOPT_PROXY sur l’URL du proxy, par exemple http://host:port ou socks5h://host:port, et CURLOPT_PROXYUSERPWD sur les identifiants, lus à partir des variables d’environnement plutôt que du code source. libcurl prend en charge les proxys HTTP, HTTPS et SOCKS. Pour faire tourner les adresses IP, pointez cette option vers la passerelle rotative d’un fournisseur et ne modifiez pas le reste de votre scraper.

Combien de threads un scraper C++ doit-il utiliser ?

Commencez par environ quatre requêtes simultanées par site, puis évaluez les résultats. Ce sont les temps d’attente réseau, et non le CPU, qui limitent la plupart des scrapers ; ainsi, des threads supplémentaires alourdissent principalement la charge sur la cible et augmentent les taux de blocage. Attribuez à chaque thread son propre descripteur libcurl. Pour des milliers de transferts parallèles, utilisez l’interface multi de libcurl sur quelques threads plutôt que d’utiliser un thread par requête.

Extraire des sites riches en JavaScript et protégés depuis C++

Conservez votre code libcurl et lexbor et laissez WebScrapingAPI se charger du rendu et du ciblage géographique. Ajoutez render_js=1 et country à une seule requête GET, puis analysez le code HTML que vous connaissez déjà. Le rendu JavaScript et le ciblage géographique ne coûtent pas de crédits supplémentaires. Les requêtes ayant échoué ne sont pas facturées, et l’essai gratuit comprend 5 000 requêtes. Essayez l’API Scraper

À 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.