Scraping por lenguaje 18 min de lectura

Web scraping en C++: guía completa

Web scraping en C++: libcurl, parsers de HTML, cuándo se justifica el desarrollo a bajo nivel y qué aporta en velocidad.

EW
Equipo Web-Scraping.es
Recopilación de datos para las necesidades del negocio
Publicado: 24 marzo 2025

Índice

  1. Introducción: por qué hacer scraping en C++ y qué conviene recordar
  2. Cómo obtenemos la página: clientes HTTP
  3. Trabajo con HTTPS / SSL
  4. Obtención del estado de la respuesta y las cabeceras
  5. Bibliotecas para parsear el contenido
  6. Solución de problemas con codificaciones y caracteres especiales
  7. Trabajo con cookies y sesiones
  8. Uso de proxies
  9. Scraping a través de TOR
  10. Multihilo y curl_multi
  11. Almacenamiento de URL y colas (panorama)
  12. Qué más hay que tener en cuenta
  13. Principales ventajas y desventajas de la implementación en C++
  14. Configuración final recomendada

1. Introducción

El web scraping es la obtención automática de páginas web y la extracción de datos estructurados a partir de ellas. Técnicamente, la tarea se divide en dos etapas independientes:

  1. Etapa de red — descargar el HTML/JSON por HTTP(S).
  2. Etapa de parseo — convertir el texto «en crudo» en un árbol (DOM) y extraer los nodos necesarios.

C++ no se elige para esto por comodidad (en Python o Go un scraper se escribe varias veces más rápido), sino por rendimiento y control: decenas de miles de conexiones en un solo núcleo, consumo mínimo de memoria, latencia predecible, ausencia de pausas del GC y una integración sencilla en un backend C/C++ existente.

Antes de escribir código, recuerde el lado legal y ético: respete robots.txt, no genere una carga excesiva (rate limiting), lea las condiciones de uso del sitio y la legislación sobre datos personales. Las técnicas para sortear bloqueos (proxies, TOR) se tratan más abajo como una herramienta, no como una invitación a incumplir las reglas de las plataformas.


2. Cómo obtenemos la página

Este es el cimiento de todo el scraper. En C++ hay varias opciones, desde el clásico de bajo nivel hasta envoltorios cómodos «al estilo Python».

2.1. libcurl — el estándar del sector

libcurl es el estándar de facto de las peticiones de red en C/C++. Soporta HTTP/1.1, HTTP/2, HTTP/3, HTTPS, proxies, SOCKS5, cookies, compresión y timeouts: literalmente todo lo que necesita un scraper, en una sola biblioteca. Su pega: una API en C basada en callbacks, bastante verbosa.

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

// Callback: curl lo invoca a medida que llegan los datos; los acumulamos en una cadena.
static size_t write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) {
    auto* out = static_cast<std::string*>(userdata);
    out->append(ptr, size * nmemb);
    return size * nmemb;
}

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

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);     // seguir las redirecciones
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");    // gzip/deflate/br automático
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; MyBot/1.0)");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

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

Fíjese en CURLOPT_ACCEPT_ENCODING, "": activa la descompresión transparente de gzip/deflate/brotli; sin ella recibirá basura binaria en lugar de HTML.

2.2. cpr — «Curl for People»

cpr (C++ Requests) es un envoltorio moderno sobre libcurl en el espíritu de Python Requests. Requiere C++17 y se mantiene activamente (documentación). La misma funcionalidad, pero con un código varias veces más corto:

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

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

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

Para la mayoría de los proyectos es el mejor punto de partida: obtiene toda la potencia de libcurl (proxies, cookies, SSL, peticiones asíncronas) con una API cómoda. Se integra mediante FetchContent de CMake, vcpkg o Conan.

2.3. cpp-httplib — header-only

cpp-httplib: un único archivo de cabecera, sin dependencias externas (para HTTPS hace falta OpenSSL). Ideal cuando no quiere arrastrar curl. Desventajas: solo HTTP/1.1, sin soporte integrado de proxies SOCKS ni de compresión brotli.

c++
#include <httplib.h>

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

2.4. Boost.Beast — bajo nivel y asincronía

Boost.Beast está construida sobre Boost.Asio y da control total sobre HTTP/WebSocket a nivel de sockets con un modelo asíncrono (corrutinas, futures, callbacks). Es el camino para quien necesita decenas de miles de conexiones simultáneas y una lógica de red a medida. El precio: bastante más código; además, el HTTPS habrá que configurarlo a mano mediante el SSL-stream de Asio.

Qué elegir

Escenario Recomendación
Empezar rápido, scraper típico cpr
Máximo control y funciones, el «estándar» asentado libcurl directamente
Mínimo de dependencias, cliente sencillo cpp-httplib
Decenas de miles de conexiones asíncronas Boost.Beast / Asio

3. Trabajo con HTTPS / SSL

Hoy prácticamente toda la web es HTTPS, así que TLS no es una opción, sino la norma.

3.1. Con libcurl/cpr

libcurl usa por sí mismo una capa TLS (por defecto OpenSSL, aunque hay compilaciones con GnuTLS, mbedTLS, BoringSSL, Schannel en Windows o Secure Transport en macOS). Lo esencial es la verificación de certificados:

c++
// ACTIVADO POR DEFECTO — cámbielo solo de forma consciente.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // verificar la cadena de certificados
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // verificar el nombre del host
// Indicar un CA-bundle propio si no se encuentra el del sistema:
curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/cacert.pem");

Nunca desactive VERIFYPEER/VERIFYHOST en producción para «arreglar» errores de certificado: eso abre la puerta a los ataques MITM. Si el conjunto de certificados raíz del sistema no está disponible (habitual en Windows o en contenedores), descargue un cacert.pem actualizado (lo publica el proyecto curl) e indíquelo con CURLOPT_CAINFO.

En cpr, el comportamiento por defecto es seguro; si hace falta, se ajusta mediante cpr::SslOptions.

3.2. Detalles finos

  • SNI (Server Name Indication) viene activado por defecto; es necesario para los hosts virtuales.
  • Versión de TLS: tiene sentido forzar como mínimo TLS 1.2 (CURLOPT_SSLVERSION = CURL_SSLVERSION_TLSv1_2).
  • Fingerprinting TLS: los sistemas anti-bots avanzados saben distinguir a los clientes por la huella JA3/JA4 del handshake TLS. La huella habitual de libcurl difiere de la de un navegador; es otro gran tema aparte (que llega hasta compilaciones de curl parcheadas para imitar el ClientHello de un navegador).

4. Estado de la respuesta y cabeceras

Un scraper está obligado a reaccionar a los códigos HTTP: 200 — correcto, 301/302 — redirección, 403/429 — bloqueo o límite de peticiones, 5xx — error del servidor (reintentar más tarde).

4.1. Código de respuesta y cabeceras en libcurl

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

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

// El juego completo de cabeceras se captura con un callback aparte:
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers_map);

Campos útiles de getinfo: CURLINFO_RESPONSE_CODE, CURLINFO_CONTENT_TYPE, CURLINFO_EFFECTIVE_URL (URL final tras las redirecciones), CURLINFO_REDIRECT_COUNT, CURLINFO_TOTAL_TIME, CURLINFO_SIZE_DOWNLOAD.

4.2. En cpr todo llega ya parseado

c++
cpr::Response r = cpr::Get(cpr::Url{"https://example.com"});
r.status_code;               // 200
r.reason;                    // "OK"
r.header["content-type"];    // map de cabeceras, muy cómodo
r.url;                       // URL final
r.elapsed;                   // duración de la petición

Para qué le sirve esto al scraper: de la cabecera Content-Type: text/html; charset=iso-8859-1 se extrae la codificación (vea la sección sobre codificaciones), y de Retry-After, ante un 429, cuánto hay que esperar antes de reintentar.


5. Bibliotecas para parsear el contenido

Ya descargó el HTML: ahora toca construir el DOM y extraer los nodos con selectores. No parsee HTML con expresiones regulares: la maquetación real, con etiquetas sin cerrar, anidamiento y comentarios, rompe cualquier regex; use un parser completo.

5.1. lexbor — la opción moderna n.º 1

lexbor (GitHub) es un parser HTML5 rápido, conforme al estándar WHATWG, escrito en C puro y sin dependencias externas. Soporta selectores CSS, detección de la codificación a partir del flujo de bytes y trabajo con el DOM. Es el sucesor de facto de myhtml/Modest y hoy la mejor opción para código nuevo (lo usa, por cierto, el parser del DOM integrado en PHP 8.4).

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

lxb_html_document_t* doc = lxb_html_document_create();
lxb_html_document_parse(doc, (const lxb_char_t*)html.data(), html.size());
// después, recorrido del DOM o búsqueda con el módulo selectors (selectores CSS)
lxb_html_document_destroy(doc);

Para disponer de un envoltorio C++ cómodo sobre lexbor existen proyectos de terceros (por ejemplo, sprexer).

5.2. libxml2 + XPath

libxml2 es una biblioteca madura y probada por el tiempo. Su htmlReadMemory() digiere el HTML «sucio», y XPath ofrece selecciones potentes (no trae selectores CSS de serie, pero XPath es más expresivo). Una elección excelente si sus datos encajan bien en expresiones XPath.

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

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

5.3. Gumbo — el clásico, pero archivado

Gumbo, de Google, fue durante mucho tiempo el estándar del parseo HTML5 en C/C++, pero el repositorio está archivado (en solo lectura desde enero de 2026). El desarrollo lo continúa el fork de la comunidad en Codeberg. Para código nuevo es preferible lexbor; mencionamos Gumbo porque lo encontrará en multitud de proyectos existentes.

5.4. Otras herramientas

  • htmlcxx — un parser HTML/CSS sencillo en C++; útil para tareas ligeras, pero lleva mucho tiempo sin evolucionar.
  • pugixml y RapidXML — para XML estricto (RSS, sitemaps, SOAP), no para HTML arbitrario.
  • JSON: muchos sitios entregan los datos mediante una API o JSON incrustado. Elija nlohmann/json (comodidad) o RapidJSON (velocidad).

Qué elegir

Tarea Recomendación
Scraper HTML5 nuevo, hacen falta selectores CSS lexbor
Selecciones complejas, está habituado a XPath libxml2
Mantenimiento de código legacy Gumbo / fork en Codeberg
XML estricto (RSS/sitemap) pugixml
API JSON nlohmann/json o RapidJSON

6. Codificaciones y caracteres especiales

Un dolor clásico: la página llega con «caracteres raros» (mojibake). La causa es casi siempre la misma: un desajuste de codificaciones. En la web hispanohablante conviven UTF-8 y, en los sitios antiguos, windows-1252 (cp1252) o ISO-8859-1 (Latin-1). Dentro del programa mantenga todo en UTF-8, convirtiendo en la entrada.

6.1. Cómo detectar la codificación de origen

Fuentes (por orden de prioridad):

  1. La cabecera HTTP Content-Type: text/html; charset=iso-8859-1.
  2. El meta del HTML: <meta charset="..."> o <meta http-equiv="Content-Type" content="...; charset=...">.
  3. El BOM al comienzo del archivo (para UTF-8/16).
  4. Heurística sobre el contenido (si no hay nada más).

Una ventaja: lexbor sabe detectar la codificación a partir del flujo de bytes por sí mismo, lo que elimina buena parte del problema ya en la fase de parseo.

6.2. Conversión a UTF-8

Opción A — iconv (GNU libiconv), disponible casi en todas partes:

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

Opción B — ICU (International Components for Unicode) — el camino más potente y fiable: una lista enorme de codificaciones, normalización Unicode y un detector de codificación (ucsdet_*):

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

Opción C — UTF8-CPP (utfcpp) — una biblioteca ligera header-only para validar, iterar y convertir texto ya en UTF-8/UTF-16/UTF-32 (no transcodifica cp1252, pero es insustituible para trabajar correctamente con el propio UTF-8).

6.3. Trampas prácticas

  • No imprima UTF-8 en la consola de Windows sin SetConsoleOutputCP(CP_UTF8): verá basura aunque los datos sean correctos.
  • En Windows, para los nombres de archivo con tildes o eñes use la API wide (std::wstring/UTF-16).
  • std::string almacena bytes, no «caracteres»: para contar caracteres reales (con á, ñ, ¿) cuente por puntos de código (utfcpp/ICU), no con .size().
  • Fije siempre el invariante: «en la entrada, detección y conversión a UTF-8; a partir de ahí, solo UTF-8 en todo el pipeline».

Muchos sitios exigen sesión: login, carrito, verificaciones anti-bots, paginación tras la autenticación. Las cookies hay que aceptarlas, almacenarlas y reenviarlas.

libcurl trae un «cookie engine» integrado:

c++
// Activar el motor y conservar las cookies en un archivo entre ejecuciones:
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); // leer (cadena vacía: activar solo el motor en memoria)
curl_easy_setopt(curl, CURLOPT_COOKIEJAR,  "cookies.txt"); // escribir durante el cleanup

// Enviar una cookie concreta a mano:
curl_easy_setopt(curl, CURLOPT_COOKIE, "session=abc123; lang=es");

Dentro de una misma «sesión», reutilice el mismo handle CURL (o un único cpr::Session): así las cookies, las conexiones keep-alive y las sesiones TLS se conservan entre peticiones, lo que resulta más rápido y más correcto desde el punto de vista de la lógica del sitio.

En cpr:

c++
cpr::Session session;
session.SetUrl(cpr::Url{"https://example.com/login"});
session.SetCookies(cpr::Cookies{{"session", "abc123"}});
cpr::Response r = session.Get();
cpr::Cookies received = r.cookies; // cookies devueltas por el servidor

Un escollo: para que una redirección traslade correctamente las cookies entre subdominios, active el cookie engine antes de la petición y no recree el handle en cada paso.


8. Proxies

Los proxies sirven para sortear restricciones geográficas, repartir la carga y reducir el riesgo de bloqueo por IP. libcurl soporta proxies HTTP, HTTPS y SOCKS5 de serie.

c++
// Proxy HTTP con autenticación:
curl_easy_setopt(curl, CURLOPT_PROXY, "http://user:pass@proxyhost:8080");

// SOCKS5 (con resolución DNS en el lado del proxy — importante para el anonimato):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://proxyhost:1080");

En cpr:

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

Rotación de proxies

Para el scraping a gran escala se mantiene un pool de proxies que se va rotando: en círculo, al azar o según su «salud» (ban → exclusión temporal). La estrategia más simple: un diccionario {proxy → contador de errores/tiempo de reposo} y la elección de un proxy vivo antes de cada petición. Conviene asignar a cada proxy su propio cookie-jar y su User-Agent, para que las «identidades» no se crucen.

El prefijo socks5h:// (con la letra h) significa que la resolución DNS pasa por el proxy y no se hace en local; de lo contrario, su petición DNS real le delatará. Es crítico con los proxies y, sobre todo, con TOR (más abajo).


9. Scraping a través de TOR

TOR es un caso particular de proxy SOCKS5: el demonio local de Tor levanta un SOCKS5 normalmente en 127.0.0.1:9050 (Tor Browser, en el 9150). Basta con dirigir las peticiones allí:

c++
// Todo el tráfico por la red Tor; la 'h' resuelve el DNS dentro de Tor (¡obligatorio!):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");

Cambio de «identidad» (nuevo nodo de salida)

A través del ControlPort de Tor (puerto 9051) se puede obtener con la señal NEWNYM una nueva cadena de nodos, es decir, en la práctica una nueva IP. Para ello abrimos un socket TCP hacia el puerto de control y enviamos comandos según el Tor Control Protocol:

code
AUTHENTICATE "contrasena"
SIGNAL NEWNYM
QUIT

Esto permite cambiar la IP de salida entre «oleadas» de peticiones. En la práctica, tenga en cuenta:

  • Entre un NEWNYM y otro, Tor impone un pequeño período de enfriamiento: no dispare la señal demasiado a menudo.
  • Tor es lento: latencia alta y canal estrecho. Para el scraping masivo es más una herramienta de anonimato que de rendimiento.
  • Muchos sitios bloquean en bloque los nodos de salida conocidos de Tor (403/CAPTCHA).
  • No pase por Tor tareas en las que de todos modos se autentica con una cuenta real: el anonimato se pierde a nivel de aplicación.

Técnicamente, la capa de código es la misma que para los proxies (sección 8); solo cambia la dirección del proxy y se añade la lógica de diálogo con el puerto de control.


10. Multihilo

El scraping es una tarea I/O-bound: la mayor parte del tiempo se va en esperar a la red. El paralelismo multiplica el rendimiento. Hay dos enfoques radicalmente distintos.

10.1. Pool de hilos + un handle por hilo

El clásico: un pool de hilos de trabajo, una cola de URL compartida y protegida frente a hilos, y en cada hilo su propio handle CURL.

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

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

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

int main() {
    curl_global_init(CURL_GLOBAL_ALL); // ¡UNA sola vez, antes de arrancar los hilos!
    std::vector<std::thread> pool;
    for (int i = 0; i < 16; ++i) pool.emplace_back(worker);
    for (auto& t : pool) t.join();
    curl_global_cleanup();
}

Crítico: curl_global_init() se llama una sola vez en el hilo principal, antes de lanzar los workers; un handle CURL no puede compartirse entre hilos: cada hilo tiene el suyo (vea curl: threadsafe).

10.2. curl_multi — muchas conexiones en un solo hilo

curl_multi gestiona cientos o miles de transferencias paralelas en un solo hilo mediante multiplexación (epoll/poll). Es más eficiente en memoria que «un hilo por conexión» y escala de maravilla. Su pega: el código se complica (bucle curl_multi_perform + gestión de eventos). Se pueden combinar ambos enfoques: varios hilos, cada uno con su propia pila multi.

10.3. Primitivas de paralelismo listas para usar

  • std::thread / std::async — los medios básicos de la STL.
  • oneTBB (Intel Threading Building Blocks) — algoritmos paralelos de alto nivel, tuberías de procesamiento (parallel_pipeline encaja de lleno en el esquema «descargar → parsear → guardar»), contenedores seguros frente a hilos.
  • OpenMP — paralelización sencilla de bucles con pragmas; suele rendir más en el parseo intensivo en CPU que en la espera de red.

Recomendación

Para la mayoría de los proyectos: pool de hilos (16--64) + un handle por hilo. Cuando choque con la memoria o con el número de hilos al llegar a miles de conexiones, pase a curl_multi.


11. Almacenamiento de URL y colas

(Sección panorámica: esto ya es arquitectura de crawler.) En cuanto el scraper empieza a seguir enlaces, aparece la tarea de gestionar el frontier — la cola de URL aún no visitadas — y la deduplicación.

Subtareas clave:

  • Cola de trabajos (frontier). En el caso simple, una std::queue en memoria. Para la resiliencia y la distribución, un broker externo: RabbitMQ (cliente rabbitmq-c) o Redis como cola/conjunto (cliente hiredis).
  • Deduplicación de URL. Para no descargar lo mismo dos veces: un std::unordered_set de URL normalizadas en memoria; con grandes volúmenes, un filtro de Bloom (compacto, a cambio de raros falsos positivos) o claves en Redis.
  • Normalización de URL. Reducción a la forma canónica (esquema, host en minúsculas, orden de la query, eliminación del #fragment, resolución de enlaces relativos). Ayuda Boost.URL.
  • Persistencia. El estado del rastreo y los resultados, en una base de datos: SQLite para una sola máquina; PostgreSQL/ClickHouse, para la escala.
  • Política de rastreo. BFS/DFS, prioridades por dominio, rate limiting por host con pausas corteses, límite de profundidad.

Pipeline típico: frontier → descargador (pool/multi) → parser → extractor de enlaces → normalización → dedup → de vuelta al frontier, y los datos extraídos, al almacenamiento.


12. Qué más hay que tener en cuenta

Temas que a menudo se olvidan, pero sin los cuales un scraper «de combate» se rompe enseguida:

  • robots.txt y cortesía. Parsee y respete robots.txt (existe el parser oficial google/robotstxt); añada pausas y limite las RPS por dominio. Es lo ético y, además, reduce el riesgo de ban.

  • Protección anti-bots y huellas. Los sitios analizan el User-Agent, el conjunto y el orden de las cabeceras, la huella TLS (JA3/JA4), el framing HTTP/2 y el comportamiento. Como mínimo, envíe cabeceras verosímiles (Accept, Accept-Language, User-Agent) y no «llame a la puerta» siempre igual. Los CAPTCHA y los desafíos JS son otra capa de problemas.

  • Renderizado de JavaScript. libcurl/lexbor solo ven el HTML original, sin ejecutar el JS. Para los sitios SPA (React/Vue), donde el contenido lo cargan los scripts, hace falta o bien un navegador headless (controlar Chromium mediante el Chrome DevTools Protocol desde C++), o bien — lo que suele ser más sencillo — localizar y llamar directamente a la misma API JSON interna que utiliza el frontend.

  • Compresión del contenido. Active Accept-Encoding (gzip/deflate/brotli): ahorra varias veces el tráfico; libcurl descomprime de forma transparente con CURLOPT_ACCEPT_ENCODING. Para descomprimir a mano: zlib y brotli.

  • Reintentos y backoff. La red es inestable. Implemente reintentos con espera exponencial y jitter, respete la cabecera Retry-After en los 429/503 y fije timeouts razonables (CURLOPT_TIMEOUT, CURLOPT_CONNECTTIMEOUT).

  • Memoria y recursos. Cierre los handles, libere los árboles DOM (*_destroy), vigile Content-Length y el límite de tamaño de la respuesta (CURLOPT_MAXFILESIZE) para no descargar por accidente un archivo de gigabytes en memoria.

  • Logging y monitorización. Cuente los códigos de respuesta, la velocidad y la proporción de bans por dominio; de lo contrario, no sabrá cuándo su scraper «murió en silencio».


13. Ventajas y desventajas

Ventajas de la implementación en C++

  • Rendimiento y baja latencia. Velocidad cercana al hardware, sobrecoste mínimo por petición.
  • Eficiencia de memoria. Se pueden mantener miles de conexiones con recursos modestos (sobre todo con curl_multi); no hay pausas del recolector de basura.
  • Control total. Ajuste fino de TLS, sockets, timeouts y memoria allí donde los lenguajes de alto nivel esconden los detalles.
  • Un ecosistema maduro de bibliotecas C. libcurl, OpenSSL, libxml2, ICU, lexbor: un cimiento industrial y probado.
  • Integración sencilla en un backend C/C++ existente, sin puentes entre lenguajes.

Desventajas

  • Desarrollo más largo y más caro. Lo que en Python se escribe en una tarde, en C++ exige más código y más cuidado.
  • Gestión manual de recursos. Fugas de memoria, handles colgados, condiciones de carrera en multihilo: responsabilidad suya.
  • APIs en C basadas en callbacks (libcurl, libxml2), verbosas; los envoltorios (cpr) lo palian en parte.
  • Pocas «pilas incluidas» para los sitios con JS. No hay un equivalente nativo de Selenium/Playwright; un navegador headless desde C++ es un suplicio.
  • Menos frameworks de scraping listos que en Python (no hay un análogo de Scrapy «de serie»): se escribe más a mano.

Conclusión: C++ compensa cuando la escala, la velocidad y el consumo de recursos son críticos (crawlers de alta carga, un producto sobre stack C++). Para tareas puntuales y prototipos, es más rápido tirar de Python o Go.


14. Configuración final

Un conjunto equilibrado para un scraper de producción en C++:

Capa Biblioteca
Cliente HTTP cpr (sobre libcurl)
TLS OpenSSL (vía libcurl)
Parseo de HTML lexbor (o libxml2 para XPath)
JSON nlohmann/json / RapidJSON
Codificaciones ICU o iconv
Multihilo pool de std::thread + curl_multi, y oneTBB si hace falta
Proxies/TOR libcurl (socks5h://) + ControlPort de Tor
Cola/dedup Redis / RabbitMQ, filtro de Bloom
Almacenamiento SQLite / PostgreSQL
Normalización de URL Boost.URL

Empiece por lo pequeño — cpr + lexbor + un solo hilo — y añada complejidad (proxies, multihilo, colas) solo cuando la carga real lo exija.