Saltar al contenido
Empezar gratis
Volver al blog

Extracción de datos de la web en C y C++ con libcurl y lexbor

Raluca PenciucÚltima actualización el 29 min read
Extracción de datos de la web en C y C++ con libcurl y lexbor
En resumen: Para hacer web scraping en C y C++, usa libcurl para descargar páginas y lexbor para analizar HTML5 con selectores CSS; ambas son C puro, así que una sola pila sirve para los dos lenguajes. Elige C++ cuando el scraper forma parte de un código nativo, se ejecuta en hardware limitado o analiza grandes volúmenes de HTML. Para tareas rápidas y puntuales, Python se escribe más rápido.

Esta guía te muestra cómo extraer datos de sitios web en C y C++ (no en C#) con dos bibliotecas de C portátiles: libcurl para descargar páginas y lexbor para analizar HTML5 con selectores CSS. Ambas están escritas en C puro, por lo que una misma pila sirve para ambos lenguajes. Además, se instalan de la misma forma en Windows, macOS y Linux.

Crearás un rastreador en C++20 para una librería de práctica pública, Books to Scrape. Este rastreador sigue la paginación, visita cada página de producto con un pequeño grupo de subprocesos y guarda archivos CSV y JSON limpios. A continuación, procesarás una página de JavaScript en «Quotes to Scrape» de tres formas diferentes: leyendo los datos directamente desde una etiqueta `script`, renderizándola con Chrome sin interfaz gráfica y pasándola a una API de scraping. Por último, volverás a ver el scraper principal como un programa sencillo en C. CMake y vcpkg compilan todo a partir de un único manifiesto.

Lo que vas a crear:

  1. Un rastreador que sigue los enlaces «siguiente» a través de las páginas de listados, de forma respetuosa.
  2. Un paso de página de producto que añade códigos UPC y recuentos de existencias con cuatro subprocesos de trabajo.
  3. Exportación a CSV según RFC 4180 y a JSON ordenado con precios en números enteros.
  4. Tres formas de obtener datos de una página JavaScript.
  5. Un rastreador sencillo en C17 basado en las mismas dos bibliotecas.

¿Prefieres programar en C#? Nuestra guía de scraping web en C# abarca HttpClient, AngleSharp y Playwright para .NET.

¿Es C++ adecuado para el scraping web?

Sí, cuando el rastreador debe integrarse en un código base existente de C o C++, ejecutarse en hardware con recursos limitados o procesar grandes volúmenes de HTML con poca memoria. Para tareas puntuales, Python es más rápido de programar. C++ no tiene un enlace oficial con Playwright, por lo que los sitios con mucho JavaScript necesitan Chrome sin interfaz gráfica o una API de rastreo.

C++ es una buena opción para:

  • Dispositivos integrados y periféricos, donde un pequeño binario nativo es mejor que incluir un entorno de ejecución de Python.
  • Servicios nativos existentes, como sistemas de negociación, indexadores de búsqueda y agentes de monitorización, en los que el rastreador es un módulo más en lugar de un proceso independiente en otro lenguaje.
  • Análisis de gran volumen, donde un menor consumo de CPU y memoria por página se traduce en un ahorro real en los servidores.
  • Equipos que desean el mismo analizador en C y C++, ya que libcurl y lexbor son bibliotecas de C con envoltorios de C++.

C++ no es la mejor opción para:

  • Scripts rápidos y puntuales, en los que un cuaderno de Python te permite obtener un CSV más rápidamente.
  • Automatización intensiva de navegadores con inicios de sesión, clics y tiempos de espera. Playwright es compatible con JavaScript, Python, Java y .NET, pero no con C++.
  • Equipos en los que nadie revisa el código de C++ de forma habitual.

La gestión manual de la memoria es el principal riesgo en los rastreadores nativos, ya que todas las bibliotecas de C te proporcionan punteros sin procesar que debes liberar. Esta guía envuelve cada identificador de C en una pequeña clase RAII y muestra cómo comprobar el resultado con sanitizadores, de modo que cualquier limpieza omitida se detecte en una ejecución de prueba en lugar de provocar una fuga en producción.

¿Qué necesitas para el web scraping en C y C++?

Necesitas un compilador de C++20, CMake y tres bibliotecas: libcurl para descargar páginas, lexbor para analizar HTML5 y ejecutar selectores CSS, y nlohmann/json para generar JSON. libcurl y lexbor son bibliotecas de C puro, por lo que la misma pila funciona en C. vcpkg instala las tres en Windows, macOS y Linux.

Los tutoriales más antiguos siguen recomendando bibliotecas que están archivadas o que no pueden gestionar el HTML del mundo real.

Bibliotecas de scraping en C y C++ (versiones de vcpkg, noviembre de 2026)

Biblioteca

Lenguaje

Úsala para

Versión (vcpkg)

Estado

libcurl

C

Solicitudes HTTP(S), proxies, cookies

8.23.0

Activo; se incluye con la mayoría de los sistemas operativos

cpr

C++

Envoltura al estilo de «requests» para libcurl

1.15.1

Activo

cpp-httplib

C++ (solo encabezados)

Cliente y servidor HTTP sencillos

0.63.0

Activo

Boost.Beast

C++

HTTP asíncrono de bajo nivel y WebSocket

1.92.0

Activo; curva de aprendizaje pronunciada

lexbor

C

Análisis de HTML5 + selectores CSS

3.0.1

Activo; sin dependencias externas

libxml2

C

Análisis de HTML/XML + XPath

2.15.4

Activo; el analizador HTML no es un generador completo de árboles HTML5

Gumbo

C

Análisis de HTML5 (sin selectores)

0.14.1 (bifurcación de la comunidad)

Repositorio de Google archivado; bifurcación en Codeberg

pugixml

C++

Solo XML (entrada bien formada)

1.16

Activo; no apto para HTML real

nlohmann/json

C++ (solo encabezado)

Salida JSON y análisis de script-JSON

3.12.0

Activo

Esta guía utiliza libcurl, lexbor y nlohmann/json. Ambas bibliotecas principales están escritas en C, se mantienen activamente y están empaquetadas en vcpkg y Conan. lexbor no tiene ninguna dependencia externa, y libcurl ya viene incluido en casi todos los sistemas operativos. Obtienes un analizador sintáctico de nivel de navegador y un cliente HTTP probado en la práctica sin necesidad de instalar nada complicado. El código que escribes para C++ también se puede trasladar a C puro casi línea por línea.

Configura el proyecto con CMake y vcpkg

El proyecto utiliza un manifiesto de vcpkg, un pequeño archivo JSON que enumera las bibliotecas. Cuando CMake configura el proyecto con el archivo de la cadena de herramientas de vcpkg, vcpkg lee el manifiesto y compila todas las dependencias para tu plataforma. Consulta el modo de manifiesto de vcpkg para obtener más detalles. Esta es la estructura que obtendrás:

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)

El if(TARGET ...) bloque es importante. vcpkg compila lexbor como biblioteca estática en Linux de forma predeterminada, y la compilación estática exporta un nombre de destino de CMake diferente al de la versión compartida. Mantener ambas líneas te ahorra un error de enlace que puede llevar a confusión.

Clona e inicia vcpkg una vez; a continuación, configura, compila y prueba. En Windows, ejecuta bootstrap-vcpkg.bat y utiliza barras invertidas:

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

Salida (final)

-- 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 primera ejecución de «configure» compila las bibliotecas a partir del código fuente, por lo que tarda un minuto. Las ejecuciones posteriores de «configure» las reutilizan.

Si prefieres utilizar paquetes del sistema, Debian y Ubuntu tienen sudo apt install libcurl4-openssl-dev nlohmann-json3-dev. lexbor no está incluido en la versión estable de Debian, así que instálalo con vcpkg o compílalo desde el código fuente. En macOS, brew install curl lexbor nlohmann-json se instalan los tres.

Visual Studio: abre la carpeta del proyecto en Visual Studio 2022 o posterior, que lo detecta CMakeLists.txt automáticamente. En CMakePresets.json, configura CMAKE_TOOLCHAIN_FILE en la ruta de vcpkg scripts/buildsystems/vcpkg.cmake, y el manifiesto instalará libcurl, lexbor y nlohmann/json en la primera configuración. No es necesario configurar manualmente las rutas de inclusión, las rutas de las bibliotecas ni las variables de entorno.

¿Cómo se extrae información de una página web en C++?

Descarga la página con libcurl en un std::string, analízala con lexbor y selecciona los elementos con selectores CSS como article.product_pod. Lee el texto o los atributos de cada elemento y, a continuación, convierte los precios y los enlaces relativos en valores limpios. Envuelve los manejadores de C en pequeñas clases RAII para que cada analizador y cada manejador se liberen automáticamente.

Empieza por el analizador, ya que es la parte con la que más vas a trabajar. La API de lexbor es C puro: creas objetos, los inicializas, los utilizas y los destruyes. La Html clase que se muestra a continuación oculta ese ciclo de vida. Su destructor libera todo, incluso cuando se lanza una excepción a mitad de una página.

// 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_;
};

Hay tres detalles de lexbor que conviene conocer antes de modificar esta clase:

  1. El documento, el analizador CSS y el motor de selectores son objetos independientes. Se necesitan los tres para ejecutar una consulta, y la clase los crea y destruye en un único lugar, de modo que ninguno pueda sobrevivir a los demás.
  2. Los selectores compilados comparten un único grupo de memoria. La clase compila cada cadena de selector una vez y la almacena en caché durante toda la vida del documento, por lo que h3 a el contenido de 20 fichas de producto se analiza una sola vez. Este bloque de memoria también explica un fallo clásico de lexbor: si liberas la memoria de un selector y luego analizas otro selector con el mismo analizador, lexbor lee la memoria liberada. Un único bloque de memoria compartido, liberado en el destructor, evita que esto ocurra.
  3. El contenido de texto se asigna automáticamente. lxb_dom_node_text_content devuelve un nuevo búfer, y debes devolverlo con lxb_dom_document_destroy_text. El text() función auxiliar lo copia en un std::string y lo libera inmediatamente.

La documentación del módulo de selectores de lexbor enumera todos los selectores compatibles, incluidos :has() y :nth-child().

A continuación, convierte el HTML en registros tipados. Cada ficha de libro en Books to Scrape contiene un título, un precio, una valoración por estrellas, una línea de disponibilidad y un enlace a su página de producto.

// 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);
        }
    }
}

Los precios se mantienen como peniques enteros hasta el momento en que se imprimen. El sistema binario de coma flotante no puede representar 51,77 con exactitud, por lo que al sumar o comparar double los precios se produce una desviación de fracciones de penique. long Los valores no. El título procede del atributo title en lugar de su texto, ya que el texto visible se trunca con «...» en los títulos largos. parse_detail Lee la tabla de información de la página del producto más tarde, cuando el rastreador visita cada libro.

main.cpp vincula los encabezados entre sí. Comienza con las inclusiones, un User-Agent válido y un pequeño archivo auxiliar, y luego se ejecuta según el nombre del comando:

// 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;
}

Ejecuta primero el analizador sobre una copia guardada de la página. Si algo falla aquí, sabrás que se trata de un problema de análisis y no de red. Este es el fixture comando:

    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

Salida

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

¿Por qué lexbor en lugar de libxml2 o Gumbo?

lexbor sigue las reglas de análisis sintáctico de HTML de WHATWG que utilizan los navegadores, tiene un motor de selectores CSS integrado y se mantiene de forma activa. El analizador sintáctico de HTML de libxml2 es anterior a HTML5, por lo que su árbol puede diferir de lo que muestran las DevTools. Google archivó su repositorio de Gumbo. Existe una bifurcación de la comunidad que sigue en activo, pero ninguna de las dos versiones cuenta con un motor de selectores.

La diferencia se aprecia en el código desordenado que los sitios web reales generan constantemente. Aquí hay un elemento «input» mal anidado, <table><b>Sale</b><tr><td>9.99</td></tr></table>, analizada por lexbor, por libxml2 2.9 (la versión que incluyen la mayoría de las distribuciones de Linux) y por Chrome:

Salida

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 y Chrome construyen el mismo árbol: el elemento <b> elemento se desplaza delante de la tabla, y aparece un <tbody> aparece alrededor de la fila. libxml2 mantiene <b> dentro de la tabla y no añade ningún <tbody>. Por lo tanto, un selector que se copie de DevTools, como table > tbody > tr, coincide en lexbor y, en el árbol de libxml2, no devuelve nada sin avisar. Las reglas que sigue lexbor se recogen en la especificación de análisis sintáctico de HTML de WHATWG. Las versiones más recientes de libxml2 tokenizan de forma más parecida a HTML5, pero siguen sin construir el árbol HTML5 completo.

Usar libxml2 y XPath en su lugar

Si tu proyecto ya incluye libxml2, o si piensas en XPath, puedes analizar la misma página de listado con él. Envuelve cada objeto de libxml2 en un std::unique_ptr con su función libre correspondiente, pasa HTML_PARSE_NONET para que el analizador nunca recupere nada de la red, y libera cada cadena que te proporcione libxml2 con xmlFree, en lugar de free(). Si estás dudando entre los dos lenguajes de consulta, el artículo «XPath frente a selectores CSS» los compara uno al lado del otro.

// 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

Resultado

A Light in the Attic
Tipping the Velvet
Soumission
20 titles

¿Cómo encontrar los selectores CSS adecuados?

Abre la página en tu navegador, haz clic con el botón derecho sobre los datos y selecciona «Inspeccionar». Elige selectores cortos basados en clases o atributos estables, como «article.product_pod h3 a», y pruébalos en la consola de DevTools con «document.querySelectorAll». A continuación, comprueba también el HTML sin procesar, ya que DevTools muestra el DOM analizado, incluidos los elementos «tbody» que insertan los navegadores.

En «Books to Scrape», cada libro se encuentra en un <article class="product_pod">. Dentro de él, los selectores que necesitas son cortos y están vinculados a nombres de clase que describen el contenido, lo que hace que sea menos probable que dejen de funcionar que las rutas largas copiadas con «Copiar selector»:

Datos

Selector (dentro de la ficha)

Qué leer

Título

h3 a

El title atributo (el texto del enlace aparece truncado con «...»)

Precio

.price_color

El texto, como £51.77

Valoración

p.star-rating

La segunda palabra del class atributo, como Three

Página siguiente

li.next a (página completa)

El href atributo

Prueba cada selector en la consola antes de escribir código en C++: document.querySelectorAll("article.product_pod").length debería devolver 20. A continuación, pulsa Ctrl+U para ver el código fuente y confirmar que los mismos elementos existen en el HTML que envía el servidor, y no solo en el DOM tras la ejecución de los scripts. Esa comprobación permite detectar a tiempo las páginas creadas con JavaScript. Nuestra hoja de referencia de selectores CSS abarca los selectores de atributo, pseudoclase y combinadores para cuando los simples no son suficientes.

¿Cómo debes configurar libcurl para el web scraping?

Crea un identificador sencillo por cada hilo y reutilízalo para que las conexiones permanezcan abiertas. Establece un User-Agent descriptivo, tiempos de espera de conexión y totales, un límite de redireccionamientos y CURLOPT_ACCEPT_ENCODING en una cadena vacía para permitir la compresión. Comprueba tú mismo el estado HTTP, vuelve a intentar solo los errores 408, 429, 5xx y los errores de red, y respeta el encabezado Retry-After.

La HttpClient clase envuelve un identificador «easy» de libcurl con esos valores predeterminados. CurlGlobal se ejecuta curl_global_init una vez, en main, antes de que se inicie ningún hilo, ya que esa llamada no es segura para hilos.

// 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_;
};

Hay cuatro opciones importantes en esta clase:

  • No CURLOPT_FAILONERROR. Esa opción convierte los errores HTTP en errores de transferencia y descarta el cuerpo. El rastreador quiere tanto el código de estado como el cuerpo, por lo que comprueba el estado por sí mismo.
  • Solo vuelve a intentarlo cuando puede servir de ayuda. Un 404 no se soluciona por sí solo, así que el cliente vuelve a intentar los errores de red y las respuestas 408, 429 y 5xx, hasta un máximo de tres veces.
  • CURLINFO_RETRY_AFTER Se encarga del análisis. libcurl lee el encabezado «Retry-After» en cualquiera de sus formatos (segundos o una fecha HTTP) y te devuelve los segundos. El cliente limita la espera a 120 segundos, de modo que un encabezado malicioso o defectuoso no pueda paralizar el rastreo durante horas. Si no hay encabezado, espera 1, 2 y luego 4 segundos.
  • Se reutiliza el identificador. Cada get llamada mantiene abierta la misma conexión cuando el servidor lo permite, lo que resulta más rápido para ti y menos pesado para el sitio web.

Para ver cómo funciona la lógica de reintentos, dirige el cliente a un pequeño servidor local que responda con un 429 Retry-After: 2 dos veces, y luego con 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";
}

Salida (líneas del registro del servidor, seguidas del programa)

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

El cliente esperó dos segundos dos veces, tal y como pedía el servidor, y lo consiguió al tercer intento.

User-Agent: identifica tu rastreador y ofrece a los propietarios de los sitios web una forma de ponerse en contacto contigo, como bookscraper/1.0 (+https://example.com/contact). Fingir que eres un navegador de escritorio hace que tu tráfico sea más difícil de distinguir del abuso y no lo hace más bienvenido. Nuestra guía sobre encabezados HTTP para el scraping web trata los demás encabezados que vale la pena configurar.

Proxies: añade CURLOPT_PROXY (por ejemplo socks5h://host:1080) y CURLOPT_PROXYUSERPWD lee las variables de entorno para enrutar el tráfico a través de un proxy. Esta opción se corresponde con el indicador -x , por lo que se mantienen los mismos ajustes que al usar curl con un proxy.

cpr: si prefieres una API de C++ al estilo de `requests`, cpr envuelve libcurl, y el resto de esta guía funciona igual.

¿Cómo se extraen datos de varias páginas en C++?

Analiza el enlace a la página siguiente en cada página de listado, resuélvelo con respecto a la URL actual y repite el proceso hasta que no quede ningún enlace o se alcance el límite de páginas. La API de URL de libcurl resuelve correctamente los enlaces relativos. Haz una pausa entre solicitudes y recupera las páginas de detalles con un pequeño conjunto fijo de subprocesos de trabajo, cada uno con su propio identificador de libcurl.

El bucle de rastreo es corto porque el analizador ya devuelve la siguiente URL:

// 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;
}

El límite de páginas te protege de los bucles y de rastrear accidentalmente un catálogo de 1.000 páginas cuando tu intención era probar solo dos. La pausa de medio segundo mantiene la tasa de solicitudes moderada.

La parte complicada es resolve_url en books.hpp. Los enlaces de «Libros para rastrear» son relativos: una ficha en /catalogue/page-1.html enlaza a a-light-in-the-attic_1000/index.html, y el mismo href en la página de inicio se refiere a otra cosa, ya que la página de inicio se encuentra un directorio más arriba. Unir cadenas de caracteres falla en casos como ese y en ../ rutas. La API de URL de libcurl aplica, en cambio, las reglas de resolución estándar: establece la URL de la página como base, coloca la href encima de ella y se lee la URL absoluta. Las pruebas sin conexión que aparecen más adelante en esta guía comprueban tanto una ruta relativa simple como un ../ enlace.

El crawl comando comprueba el archivo robots.txt (tal y como se explica en la sección sobre rastreo responsable), rastrea las páginas de listados, visita cada página de producto y genera ambos archivos de exportación:

    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

Salida (unos 7 segundos para 2 páginas de listado, 40 páginas de productos y las pausas)

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

Cuando el propio rastreo se convierte en el proyecto, con miles de páginas, descubrimiento de enlaces y programación, la API de rastreo de WebScrapingAPI recopila las páginas relacionadas mediante un único rastreo delimitado.

¿Cómo se extraen páginas de forma simultánea?

Inicia un número fijo de subprocesos std::jthread, unos cuatro, y deja que cada uno reclame el siguiente índice de un contador std::atomic. Asigna a cada subproceso su propio identificador «easy» de libcurl, ya que un mismo identificador nunca debe ser utilizado por dos subprocesos a la vez. Escribe cada resultado en su propia ranura, de modo que no se necesite ningún mutex.

Las páginas de productos son independientes entre sí, por lo que se adaptan perfectamente a un grupo pequeño:

// 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

Cada trabajador crea su propio HttpClient dentro del hilo y, a continuación, entra en un bucle: toma el siguiente índice, recupera la página de ese libro y rellena upc y stock_count en esa Book. Como ningún par de trabajadores toca nunca el mismo elemento, y el vector nunca cambia de tamaño durante el bucle, no hay nada que bloquear. std::jthread Se une automáticamente cuando se destruye, por lo que la función no puede devolver ningún valor mientras un trabajador siga escribiendo. No hay join llamadas que olvidar.

Cuatro trabajadores es un valor predeterminado adecuado para un solo sitio web. Aumentar el número de subprocesos suele suponer una mayor carga para el destino. Si alguna vez necesitas cientos de transferencias en paralelo, echa un vistazo a la interfaz «multi» de libcurl, que gestiona muchas transferencias desde un solo subproceso, en lugar de añadir subprocesos.

¿Cómo se guardan los datos extraídos en CSV o JSON?

Almacena los precios como centavos o peniques en formato entero y, a continuación, genera un archivo CSV que cumpla con la norma RFC 4180: encierra entre comillas los campos que contengan comas, comillas o saltos de línea, duplica las comillas internas y termina las filas con CRLF. Para JSON, la función `ordered_json` de nlohmann/json mantiene los campos en el orden en que los añades. Ambos formatos se abren correctamente en hojas de cálculo y herramientas de datos.

Los títulos de los libros hacen que un CSV sencillo falle rápidamente. Books to Scrape tiene títulos con comas y paréntesis en las primeras páginas, y unir los campos con comas simples desplazaría todas las columnas posteriores a ellos. El código que se muestra a continuación sigue la norma 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);
}

El archivo se abre en modo binario a propósito. En modo texto, Windows convierte cada \n en \r\n, por lo que los \r\n los finales de fila aparecerían como \r\r\n. El modo binario escribe exactamente los bytes solicitados en todas las plataformas.

El generador de JSON utiliza ordered_json, por lo que los campos aparecen en el mismo orden que las columnas del CSV, en lugar de por orden alfabético:

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';
}

Salida: books.csv (primeras filas, más la fila que hay que poner entre comillas)

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

Resultado: books.json (primer registro)

{
  "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"
}

El título «Starving Hearts» se mantiene en una sola columna porque la coma está entre comillas. Los precios se han analizado con std::from_chars, que ignora la configuración regional del usuario, por lo que un sistema configurado para utilizar comas como separadores decimales no puede alterar el resultado. El precio se analiza como dos números enteros, libras y peniques, ya que el soporte para números de coma flotante from_chars sigue variando entre las distintas bibliotecas estándar.

¿Cómo se extrae información de una página web en C puro?

Utiliza las mismas dos bibliotecas sin los envoltorios de C++: libcurl escribe la respuesta en un búfer ampliable mediante una función de llamada de retorno de escritura, y lexbor analiza el búfer y ejecuta selectores CSS. Libera todos los identificadores en cada ruta. Una única etiqueta de limpieza con «goto» permite que todo sea manejable. El programa completo en C tiene menos de 80 líneas.

Todo lo anterior también funciona en C17. El programa siguiente recupera la primera página de «Books to Scrape» y muestra todos los títulos:

/* 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

Salida (las 3 primeras de 20 líneas)

A Light in the Attic
Tipping the Velvet
Soumission
…

Si has instalado las bibliotecas con vcpkg, apunta PKG_CONFIG_PATH primero vcpkg_installed/<triplet>/lib/pkgconfig primero, ya que vcpkg incluye libcurl.pc y lexbor.pc los archivos allí.

Hay dos trampas en las que caen la mayoría de los rastreadores de C:

  • realloc puede fallar. Asigna su resultado a un puntero temporal y compruébalo antes de sobrescribir buf->data. Al escribir buf->data = realloc(buf->data, …) provoca una fuga del búfer antiguo cuando se agota la memoria.
  • La función de llamada de retorno de escritura debe devolver el número de bytes que ha gestionado. Cualquier otro valor indica a libcurl que algo ha salido mal, y esta interrumpe la transferencia. Así es también como la función de llamada de retorno informa de un fallo de asignación: devuelve 0.

Las goto etiquetas proporcionan a la función una ruta de salida por cada recurso, de modo que los objetos de libcurl y lexbor se liberan en orden inverso, independientemente de dónde se produzca el error.

¿Cómo sabes si una página necesita JavaScript?

Carga la página con libcurl y cuenta los elementos que esperas encontrar. Si el recuento es cero pero tu navegador muestra datos, la página genera su contenido con JavaScript. Antes de recurrir al navegador, busca las etiquetas `script` del HTML sin procesar y consulta la pestaña «Red». A menudo, los datos ya están ahí en formato JSON, que puedes analizar directamente.

La /js/ versión de «Quotes to Scrape» tiene un aspecto idéntico al de la página normal en un navegador. Sin embargo, si la recuperas con libcurl, los elementos de las citas no aparecen. Las citas se encuentran en un array de JavaScript dentro de una <script> etiqueta, y el propio script de la página genera el HTML a partir de él tras la carga.

Esto significa que, a menudo, puedes prescindir por completo del navegador. Esta función busca el script que define el array y se lo pasa a 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';
    }

Resultado

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

Cero elementos en el HTML, diez citas en el script. La extracción busca var data = [, y luego toma todo lo que hay hasta el primer ]; que le sigue. Se trata de un patrón específico de esta página, no de un analizador de JavaScript general. En otros sitios, busca application/json o application/ld+json etiquetas script, o abre la pestaña de red y llama al punto final JSON que utiliza la propia página.

¿Cómo se extraen datos de páginas renderizadas con JavaScript en C++?

Ejecuta Chrome sin interfaz gráfica con la opción --dump-dom para ejecutar los scripts de la página e imprimir el HTML final; a continuación, analiza esa salida con lexbor. Para clics, inicios de sesión o tiempos de espera, controla Chrome a través de WebDriver o del protocolo DevTools sobre HTTP. Playwright no tiene un enlace oficial para C++. A gran escala, una API de renderizado elimina por completo la flota de navegadores.

Cuando los datos no se encuentran en una etiqueta script, deja que un navegador real construya la página. Un navegador sin interfaz gráfica se ejecuta sin ventana, y la opción --dump-dom imprime el DOM renderizado en la salida estándar, que C++ puede leer a través de una tubería:

// 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';
    }

Salida (unos segundos, incluido el inicio de Chrome)

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

La misma Html clase y los mismos selectores CSS funcionan en la salida de Chrome, ya que lexbor y Chrome analizan el HTML de la misma manera. La documentación del modo sin interfaz gráfica de Chrome explica el resto de indicadores.

Ten en cuenta tres limitaciones:

  • Nunca construyas el comando a partir de entradas no fiables. La URL aquí es una constante. Una URL tomada de una página o proporcionada por un usuario podría contener sintaxis de shell, y popen la ejecutaría. En Windows, utiliza _popen y _pclose con comillas dobles alrededor de la URL.
  • --dump-dom imprime el DOM tras el evento «load». No espera a los datos que llegan posteriormente a través de llamadas de red adicionales. Cuando una página carga contenido después de eso, o si necesitas clics, inicios de sesión o desplazamiento, controla Chrome mediante WebDriver o el Protocolo Chrome DevTools, ya que ambos utilizan HTTP o WebSocket, a los que libcurl puede acceder.
  • Establece la CHROME variable de entorno con la ruta del navegador en macOS y Windows, donde google-chrome no está en el PATH.

Para sesiones de navegador numerosas con inicios de sesión e interacciones, la API de navegador de WebScrapingAPI ejecuta los navegadores por ti.

¿Cómo se utiliza una API de web scraping desde C++?

Envía una solicitud GET con libcurl al punto final de la API de scraping de WebScrapingAPI, pasando tu clave de API, la URL de destino codificada en formato URL y opciones como render_js=1 y country=us. El cuerpo de la respuesta es el código HTML de la página, por lo que tu analizador lexbor actual funcionará sin necesidad de modificaciones. Lee la clave de la variable de entorno WSA_API_KEY.

Ejecutar Chrome tú mismo está bien para unas pocas páginas. Cuando necesites renderización de JavaScript, un país específico o acceso a sitios que bloquean los rastreadores tras miles de solicitudes, una API de scraping se encarga de los navegadores y los proxies. Como devuelve HTML, el único cambio que tienes que hacer por tu parte es una 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 codifica la URL de destino, de modo que su propio ? y & no se puedan confundir con los parámetros de la API. El cliente tiene un tiempo de espera de 90 segundos porque renderizar una página lleva más tiempo que recuperarla, y solo un reintento, por lo que una clave incorrecta falla rápidamente. Mantén la clave fuera del código fuente y de los archivos de historial del shell que compartas:

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

Sin la variable, el comando muestra la solicitud que enviaría:

Salida sin 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

Con una clave incorrecta, la API responde con un error 401 en formato JSON, y el programa lo muestra en pantalla en lugar de intentar analizarlo:

Salida con una clave no válida

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

Ejemplo de salida con una clave válida (simulada a efectos ilustrativos)

div.quote elements via Scraper API: 10

La página generada debería contener las mismas diez citas que mostró Chrome, y el mismo selector debería encontrarlas. WebScrapingAPI también indica en los encabezados de respuesta los créditos utilizados en cada solicitud, precedidos por WSA, que puedes leer con CURLOPT_HEADERFUNCTION si quieres realizar un seguimiento del uso desde C++.

¿Qué método de scraping en C++ deberías utilizar?

Utiliza libcurl y lexbor para páginas estáticas, y revisa las etiquetas de script antes de añadir un navegador. Utiliza Chrome sin interfaz gráfica cuando la página requiera JavaScript y el volumen sea reducido. Cambia a una API de scraping cuando necesites renderización de JavaScript, geolocalización o rotación de proxies a gran escala sin tener que ejecutar navegadores tú mismo.

Página o necesidad

Utiliza

Por qué

Coste

HTML estático (libros para extraer)

libcurl + lexbor

Es la opción más rápida y la que menos componentes móviles tiene

Solo tu ancho de banda

Datos en un <script> (Citas /js/)

libcurl + nlohmann/json

No se necesita navegador

Solo tu ancho de banda

Contenido generado tras la carga, pocas páginas

Chrome sin interfaz gráfica --dump-dom + lexbor

Representación real del navegador

Una sola instalación de Chrome; unos segundos por página

Clics, inicios de sesión, tiempos de espera

WebDriver o protocolo DevTools sobre HTTP

Control total del navegador

Flota de navegadores para ejecutar y actualizar

JavaScript, geolocalización o bloqueos a gran escala

API de scraper (render_js, country)

Renderización y proxies gestionados por nosotros

Créditos por solicitud

¿Cómo se prueba y depura un scraper en C++?

Guarda páginas reales como plantillas y prueba tu analizador con ellas sin conexión mediante CTest, de modo que los fallos se deban al análisis y no a la red. Compila binarios de prueba con AddressSanitizer y UndefinedBehaviorSanitizer para detectar fugas y errores de memoria en las llamadas a la biblioteca C, y ejecuta ThreadSanitizer en el rastreador concurrente. Actualiza los fixtures cuando el sitio cambie.

Guarda las páginas de las que depende tu analizador con curl -o en fixtures/: en este caso bastan una página de listado y una página de producto. La prueba que se muestra a continuación comprueba el analizador, la resolución de URL, el análisis de precios, las comillas en el CSV y las reglas del archivo robots.txt, sin necesidad de conectarse a la red:

// 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;
}

Ejecútala a través de CTest y, a continuación, compila la misma prueba con los sanitizadores. ThreadSanitizer necesita su propia compilación, ya que no se puede combinar con 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

Salida

all parser checks passed

100% tests passed, 0 tests failed out of 1

Una ejecución limpia del sanitizador solo muestra la salida del propio programa. Cualquier fuga, lectura fuera de límites o carrera de datos genera un informe con la línea exacta.

Hay dos hábitos de depuración que ahorran mucho tiempo. En primer lugar, configura CURLOPT_VERBOSE a 1 en el identificador para ver todos los encabezados de solicitud y respuesta, lo que muestra rápidamente redireccionamientos, compresión y códigos de estado que no esperabas. En segundo lugar, cuando un selector no devuelve nada, escribe r.body en un archivo y ábrelo. El código HTML que has recibido suele ser una página de consentimiento, una página de error o un shell de JavaScript, no la página que has visto en el navegador.

¿Cómo se realiza un scraping responsable con C o C++?

Lee el archivo robots.txt antes de rastrear y omite las rutas no permitidas, identifica tu rastreador con un User-Agent veraz que incluya una URL de contacto y espacia las solicitudes. Recopila solo los datos públicos que necesites, evita los datos personales a menos que tengas una base legal para ello y respeta las condiciones del sitio. Retírate inmediatamente cuando el servidor responda con un código 429.

El rastreador ya hace la mayor parte de esto: envía un User-Agent identificativo, hace pausas entre el rastreo de páginas, se limita a cuatro procesos de trabajo y espera el tiempo que indica Retry-After. La última pieza es el archivo robots.txt. Este pequeño verificador sigue la norma 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_;
};

Selecciona el grupo correspondiente a tu agente de usuario o, si no hay ninguno, recurre al * grupo cuando no hay ninguno, y gana la regla coincidente más larga, con Allow empates. Un archivo robots.txt que devuelve un 404 significa que no hay reglas, como ocurre con Books to Scrape:

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

Este verificador es deliberadamente sencillo. No gestiona el * y $ comodines que permite el RFC 9309 dentro de las rutas, así que utiliza un analizador completo para los rastreos en producción de sitios que no controles.

El archivo robots.txt es una convención de rastreo, no una licencia. Lo que puedas recopilar también depende de los datos, de las condiciones del sitio y de la legislación del lugar en el que operes. Lee «¿Es legal el web scraping?» antes de extraer datos para un proyecto comercial.

Preguntas frecuentes

¿Es C++ más rápido que Python para el web scraping?

Para el análisis y el procesamiento, normalmente sí: el C++ compilado con lexbor consume menos CPU y memoria por página que la mayoría de las pilas de Python. Sin embargo, en la mayoría de los rastreadores, la latencia de la red y los retrasos por cortesía son los factores que más influyen en el tiempo total, por lo que la ganancia real es menor de lo que sugieren las pruebas de rendimiento. Elige C++ cuando el volumen de análisis, los límites de memoria o una base de código existente lo justifiquen.

¿Cuál es la diferencia entre C, C++ y C# para el web scraping?

C y C++ se compilan en código nativo y utilizan bibliotecas de C como libcurl y lexbor, por lo que una misma pila sirve para ambos. C# se ejecuta en .NET y cuenta con sus propias herramientas, entre las que se incluyen HttpClient, AngleSharp y Playwright para .NET. Los nombres se parecen, pero las bibliotecas no se solapan. Los desarrolladores de C# deben seguir nuestra guía específica para C#.

¿Cuál es el mejor analizador HTML para C++?

lexbor es la mejor opción por defecto en 2026: implementa el algoritmo de análisis sintáctico HTML5 de WHATWG, incluye un motor de selectores CSS, no tiene dependencias externas y se instala a través de vcpkg en Windows, macOS y Linux. Utiliza libxml2 cuando necesites XPath o ya dependas de él, y pugixml solo para XML bien formado, no para HTML del mundo real.

¿Debería utilizar cpr o libcurl para el web scraping?

Cualquiera de los dos funciona, ya que cpr es una envoltura en C++ de libcurl con una API más intuitiva, al estilo de requests. La biblioteca libcurl sin envolver te ofrece todas las opciones, como el análisis integrado de Retry-After y la API de URL, con una dependencia menos, y las mismas llamadas funcionan en C puro. Elige cpr si prefieres un código más breve y solo escribes en C++.

¿Funciona lexbor en Windows y en Visual Studio?

Sí. La adaptación de lexbor para vcpkg se compila en el escritorio de Windows, macOS y Linux. Solo quedan excluidas las aplicaciones de la Plataforma Universal de Windows. En Visual Studio, abre la carpeta del proyecto, configura CMAKE_TOOLCHAIN_FILE para que apunte al archivo de la cadena de herramientas de vcpkg, y el manifiesto instalará libcurl, lexbor y nlohmann/json en la primera configuración. No es necesario especificar rutas manuales a las bibliotecas.

¿Puede C++ extraer datos de sitios web en JavaScript?

Sí, pero no solo con libcurl, ya que libcurl no ejecuta JavaScript. Comprueba primero si los datos ya se encuentran en una etiqueta `script` en formato JSON. De lo contrario, ejecuta Chrome sin interfaz gráfica con la opción --dump-dom, controla un navegador mediante WebDriver o el protocolo DevTools, o llama a una API de extracción con la representación de JavaScript habilitada y analiza el HTML devuelto.

¿Cómo evito que me bloqueen al extraer datos con C++?

Envía menos solicitudes, más lentamente, con un User-Agent válido, y respeta el Retry-After cuando un servidor responda con un 429. Reutiliza las conexiones en lugar de abrir otras nuevas, y no ejecutes más procesos de fondo de los que el sitio pueda gestionar. En el caso de sitios protegidos, redirige las solicitudes a través de proxies rotativos con CURLOPT_PROXY, o utiliza una API de scraping que gestione proxies y navegadores.

El scraping de datos públicos es legal en muchas jurisdicciones, pero la respuesta depende de lo que recopiles, cómo lo recopiles y de las condiciones del sitio web. Evita los datos personales sin una base legal, nunca eludas los inicios de sesión ni los muros de pago, respeta el archivo robots.txt y busca asesoramiento legal para proyectos comerciales. El lenguaje de programación que utilices no cambia las normas.

¿Cómo se utiliza un proxy con libcurl?

Establece CURLOPT_PROXY con la URL del proxy, como http://host:port o socks5h://host:port, y CURLOPT_PROXYUSERPWD con las credenciales, leídas desde variables de entorno en lugar de desde el código fuente. libcurl admite proxies HTTP, HTTPS y SOCKS. Para rotar direcciones IP, configura la opción apuntando a la puerta de enlace rotativa de un proveedor y mantén el resto de tu rastreador sin cambios.

¿Cuántos subprocesos debe utilizar un rastreador en C++?

Empieza con unas cuatro solicitudes simultáneas por sitio y ve evaluando los resultados. Son las esperas de red, y no la CPU, las que limitan a la mayoría de los rastreadores, por lo que los hilos adicionales principalmente añaden carga al sitio de destino y aumentan las tasas de bloqueo. Asigna a cada hilo su propio identificador de libcurl. Para miles de transferencias paralelas, utiliza la interfaz «multi» de libcurl en unos pocos hilos, en lugar de un hilo por solicitud.

Extrae datos de sitios con mucho JavaScript y protegidos desde C++

Mantén tu código de libcurl y lexbor y deja que WebScrapingAPI se encargue de la representación y la geolocalización. Añade render_js=1 y country a una solicitud GET y analiza el HTML que ya conoces. La representación de JavaScript y la geolocalización no suponen ningún coste adicional. Las solicitudes fallidas no se facturan, y la prueba gratuita incluye 5.000 solicitudes. Prueba la API de scraping

Acerca del autor

Raluca Penciuc, Desarrollador full-stack @ WebScrapingAPI

Raluca Penciuc

Desarrollador full-stack

Raluca Penciuc es desarrolladora full stack en WebScrapingAPI, donde se dedica a crear rastreadores, mejorar las técnicas de evasión y buscar formas fiables de reducir la detección en los sitios web de destino.

Extracción de datos web con AWS Lambda: guía para Python y Java 2026
Guías

Extracción de datos web con AWS Lambda: guía para Python y Java 2026

TL;DR: En resumen: el web scraping con AWS Lambda funciona mejor cuando cada invocación es breve, está delimitada y se puede reintentar de forma independiente. Empieza con HTTP directo, AWS SAM y S3, y añade SQS, contenedores, renderización en navegador, proxies o una capa de recuperación gestionada solo cuando la carga de trabajo demuestre que los necesita.

Suciu Dan33 min read
Leer artículo
Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos
Guías

Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos

TL;DR: En resumen: GoSpider es un rastreador de línea de comandos diseñado para descubrir URL, no un extractor completo de datos estructurados. Esta guía sobre cómo utilizar GoSpider muestra cómo realizar un rastreo limitado, gestionar correctamente los resultados, pasar los datos de Colly a CSV y seguir una ruta de diagnóstico para respuestas 403 o páginas que requieren renderización con JavaScript.

Suciu Dan24 min read
Leer artículo
Cómo raspar Redfin: Guía Python de Datos Inmobiliarios
Guías

Cómo raspar Redfin: Guía Python de Datos Inmobiliarios

TL;DR: Redfin expone puntos finales de API ocultos que devuelven JSON estructurado para los listados de propiedades, lo que permite omitir por completo el frágil análisis HTML. Esta guía te guía a través de la construcción de un raspador de Python que extrae datos de alquiler y venta, busca por ubicación, supervisa los nuevos listados a través de mapas de sitio XML y exporta resultados limpios a CSV o JSON.

Suciu Dan14 min read
Leer artículo

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.