Scraping per linguaggio 18 min di lettura

Web scraping in C++: guida completa

Web scraping in C++: libcurl, parser HTML, quando ha senso lo sviluppo a basso livello e che cosa offre in velocità.

TW
Team Web-Scraping.it
Raccolta dati per le esigenze del business
Pubblicato il 24 marzo 2025

Indice

  1. Introduzione: perché fare scraping in C++ e cosa tenere a mente
  2. Come scarichiamo la pagina: i client HTTP
  3. Lavorare con HTTPS / SSL
  4. Leggere lo stato della risposta e gli header
  5. Librerie per il parsing del contenuto
  6. Risolvere i problemi di codifiche e caratteri speciali
  7. Lavorare con cookie e sessioni
  8. Uso dei proxy
  9. Scraping attraverso TOR
  10. Multithreading e curl_multi
  11. Archiviazione di URL e code (panoramica)
  12. Cos’altro tenere presente
  13. Pro e contro principali dell’implementazione in C++
  14. Lo stack finale consigliato

1. Introduzione

Il web scraping è il recupero automatico di pagine web e l’estrazione da esse di dati strutturati. Tecnicamente il compito si divide in due fasi indipendenti:

  1. Fase di rete — scaricare l’HTML/JSON via HTTP(S).
  2. Fase di parsing — trasformare il testo «grezzo» in un albero (DOM) ed estrarre i nodi necessari.

C++ non si sceglie per comodità (in Python o Go uno scraper si scrive in una frazione del tempo), ma per prestazioni e controllo: decine di migliaia di connessioni su un solo core, consumo di memoria minimo, latenza prevedibile, nessuna pausa del GC e un’integrazione semplice in un backend C/C++ esistente.

Prima di scrivere codice, ricorda il lato legale ed etico: rispetta il robots.txt, non generare un carico eccessivo (rate limiting), leggi le condizioni d’uso del sito e la normativa sui dati personali. Le tecniche per aggirare i blocchi (proxy, TOR) sono trattate più avanti come strumenti, non come un invito a violare le regole delle piattaforme.


2. Come scarichiamo la pagina

È il fondamento dell’intero scraper. In C++ le opzioni sono diverse — dai classici a basso livello ai comodi wrapper «in stile Python».

2.1. libcurl — lo standard del settore

libcurl è lo standard de facto delle richieste di rete in C/C++. Supporta HTTP/1.1, HTTP/2, HTTP/3, HTTPS, proxy, SOCKS5, cookie, compressione e timeout: letteralmente tutto ciò che serve per lo scraping, in un’unica libreria. Il difetto: un’API in C basata su callback, piuttosto verbosa.

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

// Callback: curl la invoca man mano che arrivano i dati; li accumuliamo in una stringa.
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);     // seguire i redirect
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");    // gzip/deflate/br in automatico
    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) {
        // gestire curl_easy_strerror(res)
    }
    curl_easy_cleanup(curl);
    return body;
}

Fai attenzione a CURLOPT_ACCEPT_ENCODING, "": attiva la decompressione trasparente di gzip/deflate/brotli; senza, riceverai spazzatura binaria al posto dell’HTML.

2.2. cpr — «Curl for People»

cpr (C++ Requests) è un wrapper moderno sopra libcurl nello spirito di Python Requests. Richiede C++17 ed è mantenuto attivamente (documentazione). Stesse funzionalità, ma con un codice parecchie volte più 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;                   // corpo della risposta

Per la maggior parte dei progetti è il punto di partenza migliore: ottieni tutta la potenza di libcurl (proxy, cookie, SSL, richieste asincrone) con un’API comoda. Si integra tramite FetchContent di CMake, vcpkg o Conan.

2.3. cpp-httplib — header-only

cpp-httplib: un solo file header, nessuna dipendenza esterna (per HTTPS serve OpenSSL). Ideale quando non vuoi trascinarti dietro curl. I contro: solo HTTP/1.1, nessun supporto integrato per i proxy SOCKS né per la compressione 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 — basso livello e asincronia

Boost.Beast è costruita sopra Boost.Asio e offre il controllo totale su HTTP/WebSocket a livello di socket, con un modello asincrono (coroutine, future, callback). È la strada per chi ha bisogno di decine di migliaia di connessioni simultanee e di una logica di rete su misura. Il prezzo: parecchio codice in più; e l’HTTPS andrà configurato a mano tramite l’SSL-stream di Asio.

Cosa scegliere

Scenario Raccomandazione
Partire in fretta, scraper tipico cpr
Massimo controllo e funzionalità, lo «standard» libcurl direttamente
Minime dipendenze, client semplice cpp-httplib
Decine di migliaia di connessioni asincrone Boost.Beast / Asio

3. Lavorare con HTTPS / SSL

Oggi praticamente tutto il web è in HTTPS, quindi il TLS non è un’opzione ma la norma.

3.1. Con libcurl/cpr

libcurl usa a sua volta un layer TLS (di default OpenSSL, ma esistono build con GnuTLS, mbedTLS, BoringSSL, Schannel su Windows, Secure Transport su macOS). La cosa essenziale è la verifica dei certificati:

c++
// ATTIVO DI DEFAULT — da modificare solo con cognizione di causa.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // verificare la catena di certificati
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // verificare il nome dell'host
// Indicare un CA-bundle proprio se quello di sistema non viene trovato:
curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/cacert.pem");

Non disattivare mai VERIFYPEER/VERIFYHOST in produzione per «riparare» gli errori di certificato: apre la porta agli attacchi MITM. Se il set di certificati radice di sistema manca (capita spesso su Windows o nei container), scarica un cacert.pem aggiornato (lo pubblica il progetto curl) e indicalo con CURLOPT_CAINFO.

In cpr il comportamento di default è sicuro; se serve, si regola tramite cpr::SslOptions.

3.2. Punti delicati

  • SNI (Server Name Indication) è attivo di default — serve per gli host virtuali.
  • Versione TLS: ha senso imporre come minimo TLS 1.2 (CURLOPT_SSLVERSION = CURL_SSLVERSION_TLSv1_2).
  • Fingerprinting TLS: i sistemi anti-bot avanzati sanno distinguere i client dall’impronta JA3/JA4 dell’handshake TLS. L’impronta tipica di libcurl è diversa da quella di un browser — un altro grande tema a parte (fino alle build di curl con patch per imitare il ClientHello di un browser).

4. Stato della risposta e header

Uno scraper deve reagire ai codici HTTP: 200 — tutto bene, 301/302 — redirect, 403/429 — blocco o rate limit, 5xx — errore del server (riprovare più tardi).

4.1. Codice di risposta e header in 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);

// L'insieme completo degli header si cattura con un callback separato:
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers_map);

Campi utili di getinfo: CURLINFO_RESPONSE_CODE, CURLINFO_CONTENT_TYPE, CURLINFO_EFFECTIVE_URL (l’URL finale dopo i redirect), CURLINFO_REDIRECT_COUNT, CURLINFO_TOTAL_TIME, CURLINFO_SIZE_DOWNLOAD.

4.2. In cpr è già tutto pronto

c++
cpr::Response r = cpr::Get(cpr::Url{"https://example.com"});
r.status_code;               // 200
r.reason;                    // "OK"
r.header["content-type"];    // comoda map degli header
r.url;                       // URL finale
r.elapsed;                   // durata della richiesta

A cosa serve allo scraper: dall’header Content-Type: text/html; charset=iso-8859-1 si ricava la codifica (vedi la sezione sulle codifiche), e da Retry-After, con un 429, quanto aspettare prima di riprovare.


5. Librerie per il parsing del contenuto

HTML scaricato: ora bisogna costruire il DOM ed estrarre i nodi con i selettori. Non fare il parsing dell’HTML con le espressioni regolari: il markup reale, con tag non chiusi, annidamenti e commenti, rompe qualsiasi regex; usa un parser completo.

5.1. lexbor — la scelta moderna n. 1

lexbor (GitHub) è un parser HTML5 veloce, conforme allo standard WHATWG, scritto in C puro e senza dipendenze esterne. Supporta i selettori CSS, il rilevamento della codifica dal flusso di byte e il lavoro con il DOM. È il successore di fatto di myhtml/Modest e oggi la scelta migliore per il codice nuovo (lo usa, tra l’altro, il parser del DOM integrato in 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());
// poi attraversamento del DOM o ricerca con il modulo selectors (selettori CSS)
lxb_html_document_destroy(doc);

Per avere un comodo wrapper C++ sopra lexbor esistono progetti di terze parti (ad esempio sprexer).

5.2. libxml2 + XPath

libxml2 è una libreria matura e collaudata dal tempo. La sua htmlReadMemory() digerisce l’HTML «sporco», e XPath offre selezioni potenti (i selettori CSS non ci sono di serie, ma XPath è più espressivo). Una scelta eccellente se i dati si prestano bene alle espressioni 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);
// scorrere res->nodesetval->nodeTab

5.3. Gumbo — un classico, ma archiviato

Gumbo, di Google, è stato a lungo lo standard del parsing HTML5 in C/C++, ma il repository è archiviato (in sola lettura da gennaio 2026). Lo sviluppo prosegue nel fork della community su Codeberg. Per il codice nuovo è preferibile lexbor; Gumbo va citato perché lo incontrerai in moltissimi progetti esistenti.

5.4. Altri strumenti

  • htmlcxx — un parser HTML/CSS semplice in C++; comodo per compiti leggeri, ma fermo da tempo.
  • pugixml e RapidXML — per l’XML rigoroso (RSS, sitemap, SOAP), non per l’HTML arbitrario.
  • JSON: molti siti forniscono i dati via API o JSON incorporato. Scegli nlohmann/json (comodità) o RapidJSON (velocità).

Cosa scegliere

Compito Raccomandazione
Scraper HTML5 nuovo, servono i selettori CSS lexbor
Selezioni complesse, dimestichezza con XPath libxml2
Manutenzione di codice legacy Gumbo / fork su Codeberg
XML rigoroso (RSS/sitemap) pugixml
API JSON nlohmann/json o RapidJSON

6. Codifiche e caratteri speciali

Un classico dolore: la pagina arriva piena di «caratteri strani» (mojibake). La causa è quasi sempre una sola: una discordanza di codifiche. Nel web italiano convivono UTF-8 e, nei siti più datati, windows-1252 (cp1252) o ISO-8859-1 (Latin-1). All’interno del programma tieni tutto in UTF-8, convertendo in ingresso.

6.1. Come individuare la codifica di origine

Le fonti (in ordine di priorità):

  1. L’header HTTP Content-Type: text/html; charset=iso-8859-1.
  2. Il meta dell’HTML: <meta charset="..."> o <meta http-equiv="Content-Type" content="...; charset=...">.
  3. Il BOM all’inizio del file (per UTF-8/16).
  4. L’euristica sul contenuto (se non c’è nient’altro).

Un vantaggio: lexbor sa rilevare la codifica dal flusso di byte da solo — il che elimina gran parte del problema già in fase di parsing.

6.2. Conversione in UTF-8

Opzione A — iconv (GNU libiconv), disponibile quasi ovunque:

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

Opzione B — ICU (International Components for Unicode) — la strada più potente e affidabile: un elenco enorme di codifiche, normalizzazione Unicode e un rilevatore di codifica (ucsdet_*):

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

Opzione C — UTF8-CPP (utfcpp) — una libreria leggera header-only per validare, iterare e convertire testo già in UTF-8/UTF-16/UTF-32 (non transcodifica cp1252, ma è insostituibile per lavorare correttamente con lo stesso UTF-8).

6.3. Insidie pratiche

  • Non stampare UTF-8 nella console di Windows senza SetConsoleOutputCP(CP_UTF8): vedrai spazzatura anche se i dati sono corretti.
  • Su Windows, per i nomi di file con lettere accentate usa le API wide (std::wstring/UTF-16).
  • std::string contiene byte, non «caratteri»: per contare i caratteri veri (con à, è, ù) conta per code point (utfcpp/ICU), non con .size().
  • Fissa sempre l’invariante: «in ingresso, rilevamento e conversione in UTF-8; da lì in poi, solo UTF-8 lungo tutta la pipeline».

Molti siti richiedono una sessione: login, carrello, verifiche anti-bot, paginazione dietro autenticazione. I cookie vanno accettati, conservati e rispediti.

libcurl ha un «cookie engine» integrato:

c++
// Attivare il motore e conservare i cookie in un file tra un'esecuzione e l'altra:
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); // leggere (stringa vuota: attivare solo il motore in memoria)
curl_easy_setopt(curl, CURLOPT_COOKIEJAR,  "cookies.txt"); // scrivere durante il cleanup

// Inviare un cookie specifico a mano:
curl_easy_setopt(curl, CURLOPT_COOKIE, "session=abc123; lang=it");

All’interno di una stessa «sessione» riutilizza lo stesso handle CURL (o un unico cpr::Session): così cookie, connessioni keep-alive e sessioni TLS si conservano tra le richieste — il che è più veloce e più corretto dal punto di vista della logica del sito.

In 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; // cookie restituiti dal server

Un’insidia: perché un redirect trasferisca correttamente i cookie tra sottodomini, attiva il cookie engine prima della richiesta e non ricreare l’handle a ogni passaggio.


8. Proxy

I proxy servono per aggirare le restrizioni geografiche, distribuire il carico e ridurre il rischio di blocco per IP. libcurl supporta proxy HTTP, HTTPS e SOCKS5 di serie.

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

// SOCKS5 (con risoluzione DNS sul lato proxy — importante per l'anonimato):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://proxyhost:1080");

In cpr:

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

Rotazione dei proxy

Per lo scraping su larga scala si mantiene un pool di proxy e lo si ruota: in sequenza, a caso o in base alla «salute» (ban → esclusione temporanea). La strategia più semplice: un dizionario {proxy → contatore di errori/tempo di riposo} e la scelta di un proxy vivo prima di ogni richiesta. Per ogni proxy conviene tenere un cookie-jar e uno User-Agent dedicati, così le «identità» non si incrociano.

Il prefisso socks5h:// (con la lettera h) significa che la risoluzione DNS passa dal proxy e non avviene in locale — altrimenti la tua vera richiesta DNS ti tradirà. È un punto critico con i proxy e, soprattutto, con TOR (più sotto).


9. Scraping attraverso TOR

TOR è un caso particolare di proxy SOCKS5: il demone Tor locale apre un SOCKS5 di solito su 127.0.0.1:9050 (Tor Browser su 9150). Basta indirizzare lì le richieste:

c++
// Tutto il traffico passa dalla rete Tor; la 'h' risolve il DNS dentro Tor (obbligatorio!):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");

Cambiare nodo di uscita e indirizzo IP

Attraverso la ControlPort di Tor (porta 9051), con il segnale NEWNYM si può ottenere un nuovo circuito, cioè in pratica un nuovo IP. Per farlo apriamo un socket TCP verso la porta di controllo e inviamo i comandi secondo il Tor Control Protocol:

code
AUTHENTICATE "password"
SIGNAL NEWNYM
QUIT

Questo permette di cambiare l’IP di uscita tra una «ondata» di richieste e l’altra. Nella pratica, tieni conto che:

  • Tra un NEWNYM e l’altro Tor impone un piccolo periodo di attesa — non inviare il segnale troppo spesso.
  • Tor è lento: latenza alta e banda stretta. Per lo scraping massivo è più uno strumento di anonimato che di prestazioni.
  • Molti siti bloccano in blocco i nodi di uscita noti di Tor (403/CAPTCHA).
  • Non far passare da Tor i compiti in cui comunque ti autentichi con un account reale — l’anonimato si perde a livello di applicazione.

Tecnicamente il livello di codice è lo stesso dei proxy (sezione 8); cambia solo l’indirizzo del proxy e si aggiunge la logica di dialogo con la porta di controllo.


10. Multithreading

Lo scraping è un compito I/O-bound: la maggior parte del tempo se ne va ad aspettare la rete. Il parallelismo moltiplica le prestazioni. Esistono due approcci radicalmente diversi.

10.1. Pool di thread + un handle per thread

Il classico: un pool di thread worker, una coda di URL condivisa e thread-safe, e per ogni thread il proprio 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 PROPRIO per ogni thread
    for (;;) {
        std::string url;
        {
            std::lock_guard<std::mutex> lk(m);
            if (urls.empty()) break;
            url = urls.front(); urls.pop();
        }
        std::string html = fetch_with(curl, url);
        // ... parsing ...
    }
    curl_easy_cleanup(curl);
}

int main() {
    curl_global_init(CURL_GLOBAL_ALL); // UNA sola volta, prima di avviare i thread!
    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();
}

Punto critico: curl_global_init() va chiamata una sola volta nel thread principale, prima di avviare i worker; un handle CURL non si può condividere tra thread — ogni thread ha il suo (vedi curl: threadsafe).

10.2. curl_multi — molte connessioni in un solo thread

curl_multi gestisce centinaia o migliaia di trasferimenti paralleli in un solo thread tramite multiplexing (epoll/poll). È più efficiente in memoria di «un thread per connessione» e scala alla perfezione. Il contro: il codice si complica (ciclo curl_multi_perform + gestione degli eventi). I due approcci si possono combinare: più thread, ognuno con il proprio stack multi.

10.3. Primitive di parallelismo pronte all’uso

  • std::thread / std::async — gli strumenti di base della STL.
  • oneTBB (Intel Threading Building Blocks) — algoritmi paralleli di alto livello, pipeline di elaborazione (parallel_pipeline calza a pennello sullo schema «scaricare → analizzare → salvare») e contenitori thread-safe.
  • OpenMP — parallelizzazione semplice dei cicli con le pragma; più utile per il parsing pesante lato CPU che per l’attesa di rete.

Consiglio

Per la maggior parte dei progetti: pool di thread (16–64) + un handle per thread. Quando, con migliaia di connessioni, sbatterai contro i limiti di memoria o di numero di thread, passa a curl_multi.


11. Archiviazione di URL e code

(Sezione panoramica: qui siamo già nell’architettura di un crawler.) Non appena lo scraper comincia a seguire i link, compare il compito di gestire il frontier — la coda degli URL non ancora visitati — e la deduplicazione.

Sottocompiti chiave:

  • Coda dei task (frontier). Nel caso semplice, una std::queue in memoria. Per la resilienza e la distribuzione, un broker esterno: RabbitMQ (client rabbitmq-c) o Redis come coda/insieme (client hiredis).
  • Deduplicazione degli URL. Per non scaricare due volte la stessa cosa: uno std::unordered_set di URL normalizzati in memoria; con grandi volumi, un filtro di Bloom (compatto, al prezzo di rari falsi positivi) o chiavi in Redis.
  • Normalizzazione degli URL. Riduzione alla forma canonica (schema, host in minuscolo, ordinamento della query, rimozione del #fragment, risoluzione dei link relativi). Aiuta Boost.URL.
  • Persistenza. Lo stato della scansione e i risultati vanno in un database: SQLite per una singola macchina; PostgreSQL/ClickHouse per la scala.
  • Politica di scansione. BFS/DFS, priorità per dominio, rate limiting per host con pause cortesi, limite di profondità.

Pipeline tipica: frontier → downloader (pool/multi) → parser → estrattore di link → normalizzazione → dedup → di nuovo nel frontier, e i dati estratti finiscono nello storage.


12. Cos’altro tenere presente

Temi che spesso si dimenticano, ma senza i quali uno scraper «da battaglia» si rompe in fretta:

  • robots.txt e cortesia. Fai il parsing di robots.txt e rispettalo (esiste il parser ufficiale google/robotstxt); imposta pause e limita le RPS per dominio. È la scelta etica e riduce il rischio di ban.

  • Protezione anti-bot e impronte. I siti analizzano lo User-Agent, l’insieme e l’ordine degli header, l’impronta TLS (JA3/JA4), il framing HTTP/2 e il comportamento. Come minimo, imposta header verosimili (Accept, Accept-Language, User-Agent) e non «bussare» sempre allo stesso modo. CAPTCHA e challenge JS sono un altro strato di problemi.

  • Rendering di JavaScript. libcurl/lexbor vedono solo l’HTML originale, senza eseguire il JS. Per i siti SPA (React/Vue), dove il contenuto viene caricato dagli script, serve o un browser headless (pilotare Chromium via Chrome DevTools Protocol da C++), oppure — spesso più semplice — individuare e chiamare direttamente la stessa API JSON interna usata dal frontend.

  • Compressione dei contenuti. Attiva Accept-Encoding (gzip/deflate/brotli): riduce il traffico di parecchie volte; libcurl decomprime in modo trasparente con CURLOPT_ACCEPT_ENCODING. Per la decompressione manuale: zlib e brotli.

  • Retry e backoff. La rete è instabile. Implementa i retry con attesa esponenziale e jitter, rispetta l’header Retry-After sui 429/503 e imposta timeout ragionevoli (CURLOPT_TIMEOUT, CURLOPT_CONNECTTIMEOUT).

  • Memoria e risorse. Chiudi gli handle, libera gli alberi DOM (*_destroy), tieni d’occhio Content-Length e il limite di dimensione della risposta (CURLOPT_MAXFILESIZE), per non scaricare per sbaglio in memoria un file da gigabyte.

  • Logging e monitoraggio. Conta i codici di risposta, la velocità e la quota di ban per dominio — altrimenti non saprai quando lo scraper è «morto in silenzio».


13. Pro e contro

Pro dell’implementazione in C++

  • Prestazioni e bassa latenza. Velocità vicina all’hardware, overhead minimo per richiesta.
  • Efficienza di memoria. Si possono gestire migliaia di connessioni con risorse modeste (soprattutto con curl_multi); niente pause del garbage collector.
  • Controllo totale. Regolazione fine di TLS, socket, timeout e memoria, là dove i linguaggi di alto livello nascondono i dettagli.
  • Un ecosistema maturo di librerie C. libcurl, OpenSSL, libxml2, ICU, lexbor: fondamenta industriali e collaudate.
  • Integrazione facile in un backend C/C++ esistente, senza ponti tra linguaggi.

Contro

  • Sviluppo più lungo e più costoso. Quello che in Python si scrive in una serata, in C++ richiede più codice e più attenzione.
  • Gestione manuale delle risorse. Memory leak, handle appesi, race condition nel multithreading: responsabilità tua.
  • API in C basate su callback (libcurl, libxml2), verbose; i wrapper (cpr) aiutano solo in parte.
  • Poche «batterie incluse» per i siti JS. Non esiste un equivalente nativo di Selenium/Playwright; un browser headless da C++ è una sofferenza.
  • Meno framework di scraping pronti rispetto a Python (non c’è un analogo di Scrapy «di serie»): si scrive più codice a mano.

Conclusione: C++ conviene quando scala, velocità e consumo di risorse sono critici (crawler ad alto carico, un prodotto su stack C++). Per compiti una tantum e prototipi è più rapido puntare su Python o Go.


14. Lo stack finale

Un set equilibrato per uno scraper di produzione in C++:

Livello Libreria
Client HTTP cpr (sopra libcurl)
TLS OpenSSL (via libcurl)
Parsing HTML lexbor (o libxml2 per XPath)
JSON nlohmann/json / RapidJSON
Codifiche ICU o iconv
Multithreading pool di std::thread + curl_multi, oneTBB se serve
Proxy/TOR libcurl (socks5h://) + ControlPort di Tor
Coda/dedup Redis / RabbitMQ, filtro di Bloom
Storage SQLite / PostgreSQL
Normalizzazione URL Boost.URL

Parti in piccolo — cpr + lexbor + un solo thread — e aggiungi complessità (proxy, multithreading, code) solo quando il carico reale lo richiede.