Indice
- Introduzione: perché fare scraping in C++ e cosa tenere a mente
- Come scarichiamo la pagina: i client HTTP
- Lavorare con HTTPS / SSL
- Leggere lo stato della risposta e gli header
- Librerie per il parsing del contenuto
- Risolvere i problemi di codifiche e caratteri speciali
- Lavorare con cookie e sessioni
- Uso dei proxy
- Scraping attraverso TOR
- Multithreading e
curl_multi - Archiviazione di URL e code (panoramica)
- Cos’altro tenere presente
- Pro e contro principali dell’implementazione in C++
- 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:
- Fase di rete — scaricare l’HTML/JSON via HTTP(S).
- 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.
#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:
#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 rispostaPer 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.
#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:
// 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/VERIFYHOSTin 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 uncacert.pemaggiornato (lo pubblica il progetto curl) e indicalo conCURLOPT_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
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
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 richiestaA 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).
#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.
#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->nodeTab5.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à):
- L’header HTTP
Content-Type: text/html; charset=iso-8859-1. - Il meta dell’HTML:
<meta charset="...">o<meta http-equiv="Content-Type" content="...; charset=...">. - Il BOM all’inizio del file (per UTF-8/16).
- 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:
#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_*):
#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::stringcontiene 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».
7. Cookie e sessioni
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:
// 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:
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 serverUn’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.
// 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:
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 letterah) 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:
// 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:
AUTHENTICATE "password"
SIGNAL NEWNYM
QUITQuesto permette di cambiare l’IP di uscita tra una «ondata» di richieste e l’altra. Nella pratica, tieni conto che:
- Tra un
NEWNYMe 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.
#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_pipelinecalza 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::queuein 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_setdi 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.txte cortesia. Fai il parsing dirobots.txte 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 conCURLOPT_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-Aftersui429/503e imposta timeout ragionevoli (CURLOPT_TIMEOUT,CURLOPT_CONNECTTIMEOUT).Memoria e risorse. Chiudi gli handle, libera gli alberi DOM (
*_destroy), tieni d’occhioContent-Lengthe 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.