Scraping por linguagem 18 min de leitura

Web scraping em C++: guia completo

Web scraping em C++: libcurl, parsers de HTML, quando o desenvolvimento em baixo nível se justifica e o que ele traz em velocidade.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 24 março 2025

Sumário

  1. Introdução: por que fazer scraping em C++ e o que convém lembrar
  2. Como obtemos a página: clientes HTTP
  3. Trabalho com HTTPS / SSL
  4. Obtenção do status da resposta e dos cabeçalhos
  5. Bibliotecas para parsear o conteúdo
  6. Solução de problemas com codificações e caracteres especiais
  7. Trabalho com cookies e sessões
  8. Uso de proxies
  9. Scraping via TOR
  10. Multithreading e curl_multi
  11. Armazenamento de URLs e filas (panorama)
  12. O que mais é preciso levar em conta
  13. Principais vantagens e desvantagens da implementação em C++
  14. Configuração final recomendada

1. Introdução

Web scraping é a obtenção automática de páginas web e a extração de dados estruturados a partir delas. Tecnicamente, a tarefa se divide em duas etapas independentes:

  1. Etapa de rede — baixar o HTML/JSON por HTTP(S).
  2. Etapa de parsing — converter o texto «cru» em uma árvore (DOM) e extrair os nós necessários.

C++ não é escolhido para isso por comodidade (em Python ou Go um scraper se escreve várias vezes mais rápido), mas por desempenho e controle: dezenas de milhares de conexões em um único núcleo, consumo mínimo de memória, latência previsível, ausência de pausas do GC e uma integração simples em um backend C/C++ existente.

Antes de escrever código, lembre do lado legal e ético: respeite o robots.txt, não gere carga excessiva (rate limiting), leia os termos de uso do site e a legislação sobre dados pessoais. As técnicas para contornar bloqueios (proxies, TOR) são tratadas mais abaixo como uma ferramenta, não como um convite a descumprir as regras das plataformas.


2. Como obtemos a página

Este é o alicerce de todo o scraper. Em C++ há várias opções, do clássico de baixo nível a wrappers práticos «no estilo Python».

2.1. libcurl — o padrão do setor

A libcurl é o padrão de fato das requisições de rede em C/C++. Suporta HTTP/1.1, HTTP/2, HTTP/3, HTTPS, proxies, SOCKS5, cookies, compressão e timeouts: literalmente tudo o que um scraper precisa, em uma única biblioteca. Seu porém: uma API em C baseada em callbacks, bastante verbosa.

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

// Callback: o curl o invoca à medida que os dados chegam; nós os acumulamos em uma string.
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 os redirecionamentos
    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) {
        // tratar curl_easy_strerror(res)
    }
    curl_easy_cleanup(curl);
    return body;
}

Repare em CURLOPT_ACCEPT_ENCODING, "": ele ativa a descompressão transparente de gzip/deflate/brotli; sem isso você receberá lixo binário em vez de HTML.

2.2. cpr — «Curl for People»

O cpr (C++ Requests) é um wrapper moderno sobre a libcurl no espírito do Python Requests. Requer C++17 e é mantido ativamente (documentação). A mesma funcionalidade, mas com um código várias vezes mais curto:

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 da resposta

Para a maioria dos projetos é o melhor ponto de partida: você obtém toda a potência da libcurl (proxies, cookies, SSL, requisições assíncronas) com uma API prática. Integra-se via FetchContent do CMake, vcpkg ou Conan.

2.3. cpp-httplib — header-only

O cpp-httplib: um único arquivo de cabeçalho, sem dependências externas (para HTTPS é preciso o OpenSSL). Ideal quando você não quer arrastar o curl. Desvantagens: apenas HTTP/1.1, sem suporte integrado a proxies SOCKS nem à compressão 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 — baixo nível e assincronia

A Boost.Beast é construída sobre a Boost.Asio e dá controle total sobre HTTP/WebSocket no nível dos sockets com um modelo assíncrono (corrotinas, futures, callbacks). É o caminho para quem precisa de dezenas de milhares de conexões simultâneas e de uma lógica de rede sob medida. O preço: bem mais código; além disso, o HTTPS terá de ser configurado à mão via SSL-stream do Asio.

O que escolher

Cenário Recomendação
Começar rápido, scraper típico cpr
Máximo controle e recursos, o «padrão» consolidado libcurl diretamente
Mínimo de dependências, cliente simples cpp-httplib
Dezenas de milhares de conexões assíncronas Boost.Beast / Asio

3. Trabalho com HTTPS / SSL

Hoje praticamente toda a web é HTTPS, então TLS não é uma opção, é a norma.

3.1. Com libcurl/cpr

A libcurl usa por conta própria uma camada TLS (por padrão o OpenSSL, embora existam compilações com GnuTLS, mbedTLS, BoringSSL, Schannel no Windows ou Secure Transport no macOS). O essencial é a verificação de certificados:

c++
// ATIVADO POR PADRÃO — mude apenas de forma consciente.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // verificar a cadeia de certificados
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // verificar o nome do host
// Indicar um CA-bundle próprio se o do sistema não for encontrado:
curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/cacert.pem");

Nunca desative VERIFYPEER/VERIFYHOST em produção para «consertar» erros de certificado: isso abre a porta para ataques MITM. Se o conjunto de certificados raiz do sistema não estiver disponível (comum no Windows ou em contêineres), baixe um cacert.pem atualizado (publicado pelo projeto curl) e indique-o com CURLOPT_CAINFO.

No cpr, o comportamento padrão é seguro; se necessário, ajusta-se via cpr::SslOptions.

3.2. Detalhes finos

  • O SNI (Server Name Indication) vem ativado por padrão; é necessário para os hosts virtuais.
  • Versão do TLS: faz sentido forçar no mínimo o TLS 1.2 (CURLOPT_SSLVERSION = CURL_SSLVERSION_TLSv1_2).
  • Fingerprinting TLS: os sistemas anti-bots avançados sabem distinguir os clientes pela impressão digital JA3/JA4 do handshake TLS. A impressão digital habitual da libcurl difere da de um navegador; é outro grande tema à parte (que chega a compilações de curl com patches para imitar o ClientHello de um navegador).

4. Status da resposta e cabeçalhos

Um scraper é obrigado a reagir aos códigos HTTP: 200 — correto, 301/302 — redirecionamento, 403/429 — bloqueio ou limite de requisições, 5xx — erro do servidor (tentar de novo mais tarde).

4.1. Código de resposta e cabeçalhos na 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);

// O conjunto completo de cabeçalhos é capturado com um callback à parte:
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers_map);

Campos úteis do getinfo: CURLINFO_RESPONSE_CODE, CURLINFO_CONTENT_TYPE, CURLINFO_EFFECTIVE_URL (URL final após os redirecionamentos), CURLINFO_REDIRECT_COUNT, CURLINFO_TOTAL_TIME, CURLINFO_SIZE_DOWNLOAD.

4.2. No cpr tudo chega já 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 cabeçalhos, muito prático
r.url;                       // URL final
r.elapsed;                   // duração da requisição

Para que isso serve ao scraper: do cabeçalho Content-Type: text/html; charset=iso-8859-1 extrai-se a codificação (veja a seção sobre codificações), e do Retry-After, diante de um 429, quanto é preciso esperar antes de tentar de novo.


5. Bibliotecas para parsear o conteúdo

Você já baixou o HTML: agora é hora de construir o DOM e extrair os nós com seletores. Não parseie HTML com expressões regulares: o layout real, com tags não fechadas, aninhamento e comentários, quebra qualquer regex; use um parser completo.

5.1. lexbor — a opção moderna n.º 1

O lexbor (GitHub) é um parser HTML5 rápido, conforme o padrão WHATWG, escrito em C puro e sem dependências externas. Suporta seletores CSS, detecção da codificação a partir do fluxo de bytes e trabalho com o DOM. É o sucessor de fato do myhtml/Modest e hoje a melhor opção para código novo (é ele, aliás, que o parser do DOM integrado ao PHP 8.4 utiliza).

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());
// depois, percurso do DOM ou busca com o módulo selectors (seletores CSS)
lxb_html_document_destroy(doc);

Para dispor de um wrapper C++ prático sobre o lexbor existem projetos de terceiros (por exemplo, o sprexer).

5.2. libxml2 + XPath

A libxml2 é uma biblioteca madura e testada pelo tempo. Seu htmlReadMemory() digere o HTML «sujo», e o XPath oferece seleções poderosas (não traz seletores CSS de fábrica, mas o XPath é mais expressivo). Uma escolha excelente se os seus dados se encaixam bem em expressões 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);
// percorrer res->nodesetval->nodeTab

5.3. Gumbo — o clássico, mas arquivado

O Gumbo, do Google, foi por muito tempo o padrão do parsing HTML5 em C/C++, mas o repositório está arquivado (somente leitura desde janeiro de 2026). O desenvolvimento continua no fork da comunidade no Codeberg. Para código novo é preferível o lexbor; mencionamos o Gumbo porque você o encontrará em uma infinidade de projetos existentes.

5.4. Outras ferramentas

  • htmlcxx — um parser HTML/CSS simples em C++; útil para tarefas leves, mas há muito tempo sem evoluir.
  • pugixml e RapidXML — para XML estrito (RSS, sitemaps, SOAP), não para HTML arbitrário.
  • JSON: muitos sites entregam os dados via API ou JSON embutido. Escolha o nlohmann/json (praticidade) ou o RapidJSON (velocidade).

O que escolher

Tarefa Recomendação
Scraper HTML5 novo, seletores CSS necessários lexbor
Seleções complexas, você está habituado ao XPath libxml2
Manutenção de código legado Gumbo / fork no Codeberg
XML estrito (RSS/sitemap) pugixml
API JSON nlohmann/json ou RapidJSON

6. Codificações e caracteres especiais

Uma dor clássica: a página chega com «caracteres estranhos» (mojibake). A causa é quase sempre a mesma: um descompasso de codificações. Na web lusófona convivem o UTF-8 e, nos sites antigos, o windows-1252 (cp1252) ou o ISO-8859-1 (Latin-1). Dentro do programa, mantenha tudo em UTF-8, convertendo na entrada.

6.1. Como detectar a codificação de origem

Fontes (por ordem de prioridade):

  1. O cabeçalho HTTP Content-Type: text/html; charset=iso-8859-1.
  2. A meta do HTML: <meta charset="..."> ou <meta http-equiv="Content-Type" content="...; charset=...">.
  3. O BOM no começo do arquivo (para UTF-8/16).
  4. Heurística sobre o conteúdo (se não houver mais nada).

Uma vantagem: o lexbor sabe detectar a codificação a partir do fluxo de bytes por conta própria, o que elimina boa parte do problema já na fase de parsing.

6.2. Conversão para UTF-8

Opção A — iconv (GNU libiconv), disponível em quase todo lugar:

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

Opção B — ICU (International Components for Unicode) — o caminho mais poderoso e confiável: uma lista enorme de codificações, normalização Unicode e um detector de codificação (ucsdet_*):

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

Opção C — UTF8-CPP (utfcpp) — uma biblioteca leve header-only para validar, iterar e converter texto já em UTF-8/UTF-16/UTF-32 (não transcodifica cp1252, mas é insubstituível para trabalhar corretamente com o próprio UTF-8).

6.3. Armadilhas práticas

  • Não imprima UTF-8 no console do Windows sem SetConsoleOutputCP(CP_UTF8): você verá lixo mesmo com os dados corretos.
  • No Windows, para os nomes de arquivo com acentos ou cedilhas use a API wide (std::wstring/UTF-16).
  • std::string armazena bytes, não «caracteres»: para contar caracteres reais (com á, ç, ã) conte por pontos de código (utfcpp/ICU), não com .size().
  • Fixe sempre o invariante: «na entrada, detecção e conversão para UTF-8; a partir daí, só UTF-8 em todo o pipeline».

Muitos sites exigem sessão: login, carrinho, verificações anti-bots, paginação após a autenticação. Os cookies precisam ser aceitos, armazenados e reenviados.

A libcurl traz um «cookie engine» integrado:

c++
// Ativar o motor e conservar os cookies em um arquivo entre execuções:
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); // ler (string vazia: ativar apenas o motor em memória)
curl_easy_setopt(curl, CURLOPT_COOKIEJAR,  "cookies.txt"); // gravar durante o cleanup

// Enviar um cookie específico à mão:
curl_easy_setopt(curl, CURLOPT_COOKIE, "session=abc123; lang=pt");

Dentro de uma mesma «sessão», reutilize o mesmo handle CURL (ou um único cpr::Session): assim os cookies, as conexões keep-alive e as sessões TLS se conservam entre as requisições, o que é mais rápido e mais correto do ponto de vista da lógica do site.

No 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 devolvidos pelo servidor

Uma armadilha: para que um redirecionamento transporte corretamente os cookies entre subdomínios, ative o cookie engine antes da requisição e não recrie o handle a cada passo.


8. Proxies

Os proxies servem para contornar restrições geográficas, distribuir a carga e reduzir o risco de bloqueio por IP. A libcurl suporta proxies HTTP, HTTPS e SOCKS5 de fábrica.

c++
// Proxy HTTP com autenticação:
curl_easy_setopt(curl, CURLOPT_PROXY, "http://user:pass@proxyhost:8080");

// SOCKS5 (com resolução DNS no lado do proxy — importante para o anonimato):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://proxyhost:1080");

No cpr:

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

Rotação de proxies

Para o scraping em grande escala mantém-se um pool de proxies que vai sendo rotacionado: em círculo, ao acaso ou conforme sua «saúde» (ban → exclusão temporária). A estratégia mais simples: um dicionário {proxy → contador de erros/tempo de repouso} e a escolha de um proxy vivo antes de cada requisição. Convém atribuir a cada proxy seu próprio cookie jar e seu User-Agent, para que as «identidades» não se cruzem.

O prefixo socks5h:// (com a letra h) significa que a resolução DNS passa pelo proxy e não é feita localmente; caso contrário, sua consulta DNS real vai entregá-lo. É crítico com proxies e, sobretudo, com o TOR (mais abaixo).


9. Scraping via TOR

O TOR é um caso particular de proxy SOCKS5: o daemon local do Tor levanta um SOCKS5 normalmente em 127.0.0.1:9050 (o Tor Browser, na 9150). Basta direcionar as requisições para lá:

c++
// Todo o tráfego pela rede Tor; o 'h' resolve o DNS dentro do Tor (obrigatório!):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");

Troca de «identidade» (novo nó de saída)

Através do ControlPort do Tor (porta 9051) é possível obter com o sinal NEWNYM uma nova cadeia de nós, ou seja, na prática um novo IP. Para isso abrimos um socket TCP para a porta de controle e enviamos comandos conforme o Tor Control Protocol:

code
AUTHENTICATE "senha"
SIGNAL NEWNYM
QUIT

Isso permite trocar o IP de saída entre «ondas» de requisições. Na prática, leve em conta:

  • Entre um NEWNYM e outro, o Tor impõe um pequeno período de resfriamento: não dispare o sinal com frequência demais.
  • O Tor é lento: latência alta e canal estreito. Para o scraping em massa é mais uma ferramenta de anonimato do que de desempenho.
  • Muitos sites bloqueiam em bloco os nós de saída conhecidos do Tor (403/CAPTCHA).
  • Não passe pelo Tor tarefas em que você de todo modo se autentica com uma conta real: o anonimato se perde no nível da aplicação.

Tecnicamente, a camada de código é a mesma dos proxies (seção 8); só muda o endereço do proxy e se adiciona a lógica de diálogo com a porta de controle.


10. Multithreading

O scraping é uma tarefa I/O-bound: a maior parte do tempo se vai esperando pela rede. O paralelismo multiplica a vazão. Há duas abordagens radicalmente distintas.

10.1. Pool de threads + um handle por thread

O clássico: um pool de threads de trabalho, uma fila de URLs compartilhada e protegida contra concorrência, e em cada thread seu próprio 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 PRÓPRIO por 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); // UMA única vez, antes de iniciar as threads!
    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() é chamado uma única vez na thread principal, antes de lançar os workers; um handle CURL não pode ser compartilhado entre threads: cada thread tem o seu (veja curl: threadsafe).

10.2. curl_multi — muitas conexões em uma única thread

O curl_multi gerencia centenas ou milhares de transferências paralelas em uma única thread por multiplexação (epoll/poll). É mais eficiente em memória do que «uma thread por conexão» e escala maravilhosamente. Seu porém: o código se complica (loop curl_multi_perform + gestão de eventos). É possível combinar as duas abordagens: várias threads, cada uma com sua própria pilha multi.

10.3. Primitivas de paralelismo prontas para usar

  • std::thread / std::async — os meios básicos da STL.
  • oneTBB (Intel Threading Building Blocks) — algoritmos paralelos de alto nível, dutos de processamento (o parallel_pipeline encaixa em cheio no esquema «baixar → parsear → salvar»), contêineres seguros para threads.
  • OpenMP — paralelização simples de loops com pragmas; costuma render mais no parsing intensivo em CPU do que na espera de rede.

Recomendação

Para a maioria dos projetos: pool de threads (16--64) + um handle por thread. Quando esbarrar na memória ou no número de threads ao chegar a milhares de conexões, passe ao curl_multi.


11. Armazenamento de URLs e filas

(Seção panorâmica: isto já é arquitetura de crawler.) Assim que o scraper começa a seguir links, surge a tarefa de gerenciar o frontier — a fila de URLs ainda não visitadas — e a deduplicação.

Subtarefas-chave:

  • Fila de trabalhos (frontier). No caso simples, uma std::queue em memória. Para resiliência e distribuição, um broker externo: RabbitMQ (cliente rabbitmq-c) ou Redis como fila/conjunto (cliente hiredis).
  • Deduplicação de URLs. Para não baixar a mesma coisa duas vezes: um std::unordered_set de URLs normalizadas em memória; com grandes volumes, um filtro de Bloom (compacto, em troca de raros falsos positivos) ou chaves no Redis.
  • Normalização de URLs. Redução à forma canônica (esquema, host em minúsculas, ordem da query, remoção do #fragment, resolução de links relativos). Ajuda a Boost.URL.
  • Persistência. O estado do rastreamento e os resultados, em um banco de dados: SQLite para uma única máquina; PostgreSQL/ClickHouse, para a escala.
  • Política de rastreamento. BFS/DFS, prioridades por domínio, rate limiting por host com pausas corteses, limite de profundidade.

Pipeline típico: frontier → baixador (pool/multi) → parser → extrator de links → normalização → dedup → de volta ao frontier, e os dados extraídos, para o armazenamento.


12. O que mais é preciso levar em conta

Temas que muitas vezes são esquecidos, mas sem os quais um scraper «de combate» quebra logo:

  • robots.txt e cortesia. Parseie e respeite o robots.txt (existe o parser oficial google/robotstxt); adicione pausas e limite as RPS por domínio. É o ético e, de quebra, reduz o risco de ban.

  • Proteção anti-bots e impressões digitais. Os sites analisam o User-Agent, o conjunto e a ordem dos cabeçalhos, a impressão digital TLS (JA3/JA4), o framing HTTP/2 e o comportamento. No mínimo, envie cabeçalhos verossímeis (Accept, Accept-Language, User-Agent) e não «bata na porta» sempre do mesmo jeito. Os CAPTCHAs e os desafios JS são outra camada de problemas.

  • Renderização de JavaScript. libcurl/lexbor só veem o HTML original, sem executar o JS. Para os sites SPA (React/Vue), em que o conteúdo é carregado pelos scripts, é preciso ou um navegador headless (controlar o Chromium via Chrome DevTools Protocol a partir do C++), ou — o que costuma ser mais simples — localizar e chamar diretamente a mesma API JSON interna que o frontend utiliza.

  • Compressão do conteúdo. Ative o Accept-Encoding (gzip/deflate/brotli): economiza o tráfego várias vezes; a libcurl descomprime de forma transparente com CURLOPT_ACCEPT_ENCODING. Para descomprimir à mão: zlib e brotli.

  • Novas tentativas e backoff. A rede é instável. Implemente novas tentativas com espera exponencial e jitter, respeite o cabeçalho Retry-After nos 429/503 e fixe timeouts razoáveis (CURLOPT_TIMEOUT, CURLOPT_CONNECTTIMEOUT).

  • Memória e recursos. Feche os handles, libere as árvores DOM (*_destroy), vigie o Content-Length e o limite de tamanho da resposta (CURLOPT_MAXFILESIZE) para não baixar por acidente um arquivo de gigabytes para a memória.

  • Logging e monitoramento. Conte os códigos de resposta, a velocidade e a proporção de bans por domínio; caso contrário, você não saberá quando o seu scraper «morreu em silêncio».


13. Vantagens e desvantagens

Vantagens da implementação em C++

  • Desempenho e baixa latência. Velocidade próxima ao hardware, sobrecusto mínimo por requisição.
  • Eficiência de memória. É possível sustentar milhares de conexões com recursos modestos (sobretudo com o curl_multi); não há pausas do coletor de lixo.
  • Controle total. Ajuste fino de TLS, sockets, timeouts e memória onde as linguagens de alto nível escondem os detalhes.
  • Um ecossistema maduro de bibliotecas C. libcurl, OpenSSL, libxml2, ICU, lexbor: um alicerce industrial e comprovado.
  • Integração simples em um backend C/C++ existente, sem pontes entre linguagens.

Desvantagens

  • Desenvolvimento mais longo e mais caro. O que em Python se escreve em uma tarde, em C++ exige mais código e mais cuidado.
  • Gestão manual de recursos. Vazamentos de memória, handles pendurados, condições de corrida no multithreading: responsabilidade sua.
  • APIs em C baseadas em callbacks (libcurl, libxml2), verbosas; os wrappers (cpr) atenuam isso em parte.
  • Poucas «pilhas incluídas» para os sites com JS. Não há um equivalente nativo do Selenium/Playwright; um navegador headless a partir do C++ é um suplício.
  • Menos frameworks de scraping prontos do que em Python (não há um análogo do Scrapy «de fábrica»): escreve-se mais à mão.

Conclusão: C++ compensa quando a escala, a velocidade e o consumo de recursos são críticos (crawlers de alta carga, um produto sobre stack C++). Para tarefas pontuais e protótipos, é mais rápido recorrer a Python ou Go.


14. Configuração final

Um conjunto equilibrado para um scraper de produção em C++:

Camada Biblioteca
Cliente HTTP cpr (sobre a libcurl)
TLS OpenSSL (via libcurl)
Parsing de HTML lexbor (ou libxml2 para XPath)
JSON nlohmann/json / RapidJSON
Codificações ICU ou iconv
Multithreading pool de std::thread + curl_multi, e oneTBB se necessário
Proxies/TOR libcurl (socks5h://) + ControlPort do Tor
Fila/dedup Redis / RabbitMQ, filtro de Bloom
Armazenamento SQLite / PostgreSQL
Normalização de URLs Boost.URL

Comece pelo pequeno — cpr + lexbor + uma única thread — e adicione complexidade (proxies, multithreading, filas) apenas quando a carga real exigir.