Saltar para o conteúdo
Comece grátis
Voltar ao blogue

Web scraping em C e C++ com a libcurl e o lexbor

Raluca PenciucÚltima atualização em 28 min read
Web scraping em C e C++ com a libcurl e o lexbor
Resumo: Para web scraping em C e C++, utilize a libcurl para descarregar páginas e a lexbor para analisar HTML5 com seletores CSS; ambas são C puro, pelo que uma única stack serve as duas linguagens. Escolha C++ quando o scraper vive numa base de código nativa, corre em hardware limitado ou analisa grandes volumes de HTML. Para tarefas rápidas e pontuais, Python é mais rápido de escrever.

Este guia mostra-lhe como extrair dados de sites em C e C++ (não em C#) com duas bibliotecas C portáteis: a libcurl para descarregar páginas e a lexbor para analisar HTML5 com seletores CSS. Ambas são em C puro, pelo que uma única pilha serve ambas as linguagens. Além disso, instalam-se da mesma forma no Windows, no macOS e no Linux.

Irá criar um scraper em C++20 para uma livraria de exercícios públicos, a «Books to Scrape». Este segue a paginação, visita todas as páginas de produtos com um pequeno conjunto de threads e guarda ficheiros CSV e JSON limpos. Em seguida, irá lidar com uma página JavaScript no site «Quotes to Scrape» de três formas: lendo os dados diretamente de uma tag script, renderizando-os com o Chrome headless e enviando-os para uma API de scraping. Por fim, voltará a ver o scraper principal como um programa simples em C. O CMake e o vcpkg compilam tudo a partir de um único manifesto.

O que irá criar:

  1. Um rastreador que segue os links «seguinte» nas páginas de listagem, de forma educada.
  2. Uma etapa de página de produto que adiciona códigos UPC e quantidades em stock com quatro threads de trabalho.
  3. Exportação em CSV (RFC 4180) e JSON ordenado com preços expressos em números inteiros.
  4. Três formas de obter dados de uma página JavaScript.
  5. Um scraper simples em C17 construído com base nas mesmas duas bibliotecas.

Prefere programar em C#? O nosso guia de web scraping em C# aborda o HttpClient, o AngleSharp e o Playwright para .NET.

O C++ é adequado para a extração de dados da Web?

Sim, quando o scraper tem de ser integrado numa base de código C ou C++ já existente, ser executado em hardware com recursos limitados ou processar grandes volumes de HTML com pouca memória. Para tarefas pontuais, o Python é mais rápido de escrever. O C++ não tem uma ligação oficial ao Playwright, pelo que os sites com muito JavaScript necessitam do Chrome headless ou de uma API de scraping.

O C++ é uma boa opção para:

  • Dispositivos incorporados e de borda, onde um pequeno binário nativo é preferível ao envio de um runtime Python.
  • Serviços nativos existentes, tais como sistemas de negociação, indexadores de pesquisa e agentes de monitorização, onde o scraper é mais um módulo em vez de um processo separado noutra linguagem.
  • Análise de grande volume, em que um menor consumo de CPU e memória por página se traduz em poupanças reais nos servidores.
  • Equipas que pretendem o mesmo analisador em C e C++, uma vez que a libcurl e o lexbor são bibliotecas C com wrappers em C++.

O C++ não é adequado para:

  • Scripts rápidos e pontuais, em que um notebook Python permite obter um ficheiro CSV mais rapidamente.
  • Automação pesada do navegador com inícios de sessão, cliques e tempos de espera. O Playwright suporta JavaScript, Python, Java e .NET, mas não C++.
  • Equipas sem ninguém que reveja código C++ regularmente.

A gestão manual da memória é o principal risco nos scrapers nativos, porque todas as bibliotecas C fornecem ponteiros brutos que têm de ser libertados. Este guia envolve cada identificador C numa pequena classe RAII e mostra como verificar o resultado com sanitizers, para que uma limpeza esquecida seja detetada numa execução de teste, em vez de se traduzir numa fuga de memória em produção.

O que é necessário para o web scraping em C e C++?

É necessário um compilador C++20, o CMake e três bibliotecas: a libcurl para descarregar páginas, a lexbor para analisar HTML5 e executar seletores CSS, e a nlohmann/json para escrever JSON. A libcurl e o lexbor são bibliotecas em C puro, pelo que a mesma pilha funciona em C. O vcpkg instala as três no Windows, macOS e Linux.

Os tutoriais mais antigos ainda recomendam bibliotecas que estão arquivadas ou que não conseguem lidar com HTML do mundo real.

Bibliotecas de scraping em C e C++ (versões vcpkg, novembro de 2026)

Biblioteca

Linguagem

Utilize-a para

Versão (vcpkg)

Estado

libcurl

C

Pedidos HTTP(S), proxies, cookies

8.23.0

Ativo; incluído na maioria dos sistemas operativos

cpr

C++

Wrapper ao estilo do requests em torno da libcurl

1.15.1

Ativo

cpp-httplib

C++ (apenas cabeçalhos)

Cliente e servidor HTTP simples

0.63.0

Ativo

Boost.Beast

C++

HTTP assíncrono de baixo nível e WebSocket

1.92.0

Ativo; curva de aprendizagem acentuada

lexbor

C

Análise de HTML5 + seletores CSS

3.0.1

Ativo; sem dependências externas

libxml2

C

Análise de HTML/XML + XPath

2.15.4

Ativo; o analisador HTML não é um construtor completo de árvores HTML5

Gumbo

C

Análise de HTML5 (sem seletores)

0.14.1 (fork da comunidade)

Repositório do Google arquivado; fork no Codeberg

pugixml

C++

Apenas XML (entrada bem formada)

1.16

Ativo; não para HTML do mundo real

nlohmann/json

C++ (apenas cabeçalho)

Saída JSON e análise de script-JSON

3.12.0

Ativo

Este guia utiliza a libcurl, o lexbor e o nlohmann/json. Ambas as bibliotecas principais estão escritas em C, são mantidas ativamente e estão disponíveis no vcpkg e no Conan. O lexbor não tem quaisquer dependências externas e a libcurl já vem incluída em quase todos os sistemas operativos. Fica com um analisador ao nível dos navegadores e um cliente HTTP comprovado em ação, sem necessidade de instalar nada de invulgar. O código que escreve para C++ também se transfere para C simples, praticamente linha a linha.

Configure o projeto com o CMake e o vcpkg

O projeto utiliza um manifesto vcpkg, um pequeno ficheiro JSON que lista as bibliotecas. Quando o CMake configura o projeto com o ficheiro da cadeia de ferramentas do vcpkg, o vcpkg lê o manifesto e compila todas as dependências para a sua plataforma. Consulte o modo de manifesto do vcpkg para obter mais detalhes. Eis a estrutura final que irá obter:

bookscraper/
├── CMakeLists.txt
├── vcpkg.json
├── src/
│   ├── http.hpp      # libcurl client: timeouts, retries, Retry-After
│   ├── html.hpp      # lexbor RAII wrapper: parse + CSS selectors
│   ├── books.hpp     # Book struct, listing and detail parsers, URL resolution
│   ├── csv.hpp       # RFC 4180 CSV writer
│   ├── robots.hpp    # robots.txt check
│   └── main.cpp      # crawl, JavaScript, and Scraper API commands
├── tests/parser_test.cpp
└── fixtures/         # saved pages for offline tests

vcpkg.json

{
  "name": "bookscraper",
  "version": "1.0.0",
  "dependencies": ["curl", "lexbor", "nlohmann-json"]
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.21)
project(bookscraper CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(CURL REQUIRED)
find_package(lexbor CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
# lexbor exports lexbor::lexbor for shared builds and lexbor::lexbor_static for static ones
if(TARGET lexbor::lexbor)
  set(LEXBOR_TARGET lexbor::lexbor)
else()
  set(LEXBOR_TARGET lexbor::lexbor_static)
endif()

add_executable(bookscraper src/main.cpp)
target_link_libraries(bookscraper PRIVATE CURL::libcurl ${LEXBOR_TARGET} nlohmann_json::nlohmann_json)

enable_testing()
add_executable(parser_test tests/parser_test.cpp)
target_include_directories(parser_test PRIVATE src)
target_compile_definitions(parser_test PRIVATE FIXTURES="${CMAKE_CURRENT_SOURCE_DIR}/fixtures")
target_link_libraries(parser_test PRIVATE CURL::libcurl ${LEXBOR_TARGET})
add_test(NAME parser COMMAND parser_test)

O if(TARGET ...) bloco é importante. Por predefinição, o vcpkg compila o lexbor como uma biblioteca estática no Linux, e a compilação estática exporta um nome de alvo CMake diferente do da versão partilhada. Manter ambas as linhas evita-lhe um erro de ligação confuso.

Clone e inicie o vcpkg uma vez; depois, configure, compile e teste. No Windows, execute bootstrap-vcpkg.bat e utilize barras invertidas:

git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build

Saída (tail)

-- Found nlohmann_json: …/vcpkg_installed/x64-linux/share/nlohmann_json/nlohmann_jsonConfig.cmake (found version "3.12.0")
-- Configuring done (67.9s)
[100%] Built target bookscraper
1/1 Test #1: parser ...........................   Passed    0.00 sec
100% tests passed, 0 tests failed out of 1

A primeira execução do `configure` compila as bibliotecas a partir do código-fonte, pelo que demora um minuto. As execuções posteriores do `configure` reutilizam-nas.

Se preferir utilizar pacotes do sistema, o Debian e o Ubuntu têm sudo apt install libcurl4-openssl-dev nlohmann-json3-dev. O lexbor não está incluído na versão estável do Debian, por isso instale-o com o vcpkg ou compile-o a partir do código-fonte. No macOS, brew install curl lexbor nlohmann-json instala os três.

Visual Studio: abre a pasta do projeto no Visual Studio 2022 ou posterior, que o deteta CMakeLists.txt automaticamente. Em CMakePresets.json, defina CMAKE_TOOLCHAIN_FILE para o scripts/buildsystems/vcpkg.cmake, e o manifesto instala o libcurl, o lexbor e o nlohmann/json na primeira configuração. Não é necessário definir manualmente os caminhos de inclusão, os caminhos das bibliotecas ou as variáveis de ambiente.

Como se faz o scraping de uma página web em C++?

Descarregue a página com o libcurl para um std::string, analise-a com o lexbor e selecione elementos com seletores CSS, como article.product_pod. Leia o texto ou os atributos de cada elemento e, em seguida, converta preços e links relativos em valores limpos. Envolva os handles C em pequenas classes RAII para que cada analisador e handle seja libertado automaticamente.

Comece pelo analisador, pois é a parte com que mais irá trabalhar. A API do lexbor é C puro: cria-se objetos, inicializa-se, utiliza-se e destrói-se. A Html classe abaixo oculta esse ciclo de vida. O seu destrutor liberta tudo, mesmo quando é lançada uma exceção a meio de uma página.

// html.hpp: a small RAII layer over lexbor's HTML5 parser and CSS selector engine
#pragma once
#include <lexbor/html/html.h>
#include <lexbor/css/css.h>
#include <lexbor/selectors/selectors.h>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>

using Node = lxb_dom_node_t*;

class Html {
public:
    explicit Html(std::string_view html)
        : doc_(lxb_html_document_create()), css_(lxb_css_parser_create()), mem_(lxb_css_memory_create()),
          css_sel_(lxb_css_selectors_create()), sel_(lxb_selectors_create()) {
        if (!doc_ || !css_ || !mem_ || !css_sel_ || !sel_
            || lxb_html_document_parse(doc_, reinterpret_cast<const lxb_char_t*>(html.data()), html.size()) != LXB_STATUS_OK
            || lxb_css_parser_init(css_, nullptr) != LXB_STATUS_OK
            || lxb_css_memory_init(mem_, 128) != LXB_STATUS_OK
            || lxb_css_selectors_init(css_sel_) != LXB_STATUS_OK
            || lxb_selectors_init(sel_) != LXB_STATUS_OK) {
            release();
            throw std::runtime_error("lexbor: could not parse the document");
        }
        // Every compiled selector lives in one memory pool that is freed with the document.
        lxb_css_parser_memory_set(css_, mem_);
        lxb_css_parser_selectors_set(css_, css_sel_);
    }
    ~Html() { release(); }
    Html(const Html&) = delete;
    Html& operator=(const Html&) = delete;

    // All elements under `scope` (the whole document by default) that match a CSS selector.
    std::vector<Node> select(std::string_view css, Node scope = nullptr) const {
        std::vector<Node> found;
        lxb_css_selector_list_t* list = compile(css);
        lxb_selectors_find(sel_, scope ? scope : lxb_dom_interface_node(doc_), list,
            [](Node node, lxb_css_selector_specificity_t, void* out) -> lxb_status_t {
                static_cast<std::vector<Node>*>(out)->push_back(node);
                return LXB_STATUS_OK;
            }, &found);
        return found;
    }

    Node first(std::string_view css, Node scope = nullptr) const {
        auto nodes = select(css, scope);
        return nodes.empty() ? nullptr : nodes.front();
    }

    std::string text(Node node) const {
        if (!node) return {};
        size_t len = 0;
        lxb_char_t* raw = lxb_dom_node_text_content(node, &len);   // allocated by lexbor
        std::string out(reinterpret_cast<char*>(raw), len);
        lxb_dom_document_destroy_text(lxb_dom_interface_document(doc_), raw);
        return out;
    }

    static std::string attr(Node node, std::string_view name) {
        if (!node) return {};
        size_t len = 0;
        const lxb_char_t* value = lxb_dom_element_get_attribute(lxb_dom_interface_element(node),
            reinterpret_cast<const lxb_char_t*>(name.data()), name.size(), &len);
        return value ? std::string(reinterpret_cast<const char*>(value), len) : std::string{};
    }

private:
    // Compile each selector once per document; "h3 a" inside 20 cards is parsed a single time.
    lxb_css_selector_list_t* compile(std::string_view css) const {
        auto it = cache_.find(std::string(css));
        if (it != cache_.end()) return it->second;
        auto* list = lxb_css_selectors_parse(css_, reinterpret_cast<const lxb_char_t*>(css.data()), css.size());
        if (!list || css_->status != LXB_STATUS_OK) throw std::invalid_argument("bad CSS selector: " + std::string(css));
        return cache_.emplace(std::string(css), list).first->second;
    }
    void release() {
        if (sel_) lxb_selectors_destroy(sel_, true);
        if (css_sel_) lxb_css_selectors_destroy(css_sel_, true);
        if (css_) lxb_css_parser_destroy(css_, true);
        if (mem_) lxb_css_memory_destroy(mem_, true);     // frees every compiled selector list
        if (doc_) lxb_html_document_destroy(doc_);
        sel_ = nullptr; css_sel_ = nullptr; css_ = nullptr; mem_ = nullptr; doc_ = nullptr;
    }
    lxb_html_document_t* doc_;
    lxb_css_parser_t* css_;
    lxb_css_memory_t* mem_;
    lxb_css_selectors_t* css_sel_;
    lxb_selectors_t* sel_;
    mutable std::unordered_map<std::string, lxb_css_selector_list_t*> cache_;
};

Há três detalhes do lexbor que vale a pena conhecer antes de alterar esta classe:

  1. O documento, o analisador CSS e o motor de seleção são objetos separados. São necessários os três para executar uma consulta, e a classe cria-os e destrói-os num único local, para que nenhum possa sobreviver aos outros.
  2. Os seletores compilados partilham um único conjunto de memória. A classe compila cada cadeia de caracteres do seletor uma vez e armazena-a em cache durante toda a vida útil do documento, pelo que h3 a o conteúdo de 20 fichas de produto é analisado uma única vez. O conjunto de memória também explica uma falha clássica do lexbor: se libertar a memória de um seletor e, em seguida, analisar outro seletor com o mesmo analisador, o lexbor lê a memória libertada. Um conjunto de memória partilhado, libertado no destrutor, evita isso.
  3. O conteúdo de texto é alocado automaticamente. lxb_dom_node_text_content retorna um novo buffer, e deve devolvê-lo com lxb_dom_document_destroy_text. O text() função auxiliar copia-o para um std::string e liberta-o imediatamente.

A documentação do módulo de seletores lexbor enumera todos os seletores suportados, incluindo :has() e :nth-child().

Em seguida, transforme o HTML em registos tipados. Cada ficha de livro no «Books to Scrape» contém um título, preço, classificação por estrelas, linha de disponibilidade e um link para a página do produto.

// books.hpp: turn Books to Scrape HTML into typed records
#pragma once
#include "html.hpp"
#include <curl/curl.h>
#include <charconv>
#include <memory>
#include <optional>
#include <string>
#include <vector>

struct Book {
    std::string title;
    long price_pence = 0;     // integer money: 51.77 GBP is stored as 5177
    int rating = 0;           // 1 to 5 stars
    bool in_stock = false;
    std::string url;          // absolute product URL
    std::string upc;          // filled from the product page
    int stock_count = 0;      // filled from the product page
};

// Resolve an href against the URL of the page it came from, using libcurl's URL API.
inline std::string resolve_url(const std::string& page_url, const std::string& href) {
    std::unique_ptr<CURLU, decltype(&curl_url_cleanup)> u(curl_url(), &curl_url_cleanup);
    if (!u || curl_url_set(u.get(), CURLUPART_URL, page_url.c_str(), 0) != CURLUE_OK
           || curl_url_set(u.get(), CURLUPART_URL, href.c_str(), 0) != CURLUE_OK)  // relative to the page
        return {};
    char* full = nullptr;
    if (curl_url_get(u.get(), CURLUPART_URL, &full, 0) != CURLUE_OK) return {};
    std::string out(full);
    curl_free(full);
    return out;
}

// "£51.77" -> 5177. Locale-independent: std::from_chars ignores the user's decimal separator.
inline std::optional<long> parse_pence(std::string_view text) {
    const auto start = text.find_first_of("0123456789");
    if (start == std::string_view::npos) return std::nullopt;
    text.remove_prefix(start);
    long whole = 0, frac = 0;
    auto [p, ec] = std::from_chars(text.data(), text.data() + text.size(), whole);
    if (ec != std::errc{}) return std::nullopt;
    if (p != text.data() + text.size() && *p == '.') {
        const char* f = p + 1;
        auto [q, ec2] = std::from_chars(f, text.data() + text.size(), frac);
        if (ec2 != std::errc{} || q - f != 2) return std::nullopt;
    }
    return whole * 100 + frac;
}

// <p class="star-rating Three"> -> 3
inline int parse_rating(const std::string& css_class) {
    static constexpr const char* words[] = {"One", "Two", "Three", "Four", "Five"};
    for (int i = 0; i < 5; ++i)
        if (css_class.find(words[i]) != std::string::npos) return i + 1;
    return 0;
}

struct ListingPage {
    std::vector<Book> books;
    std::string next_url;     // empty on the last page
};

inline ListingPage parse_listing(std::string_view html, const std::string& page_url) {
    Html doc(html);
    ListingPage page;
    for (Node card : doc.select("article.product_pod")) {
        Node link = doc.first("h3 a", card);
        Book b;
        b.title = Html::attr(link, "title");                   // the visible text is truncated with "..."
        b.url = resolve_url(page_url, Html::attr(link, "href"));
        b.price_pence = parse_pence(doc.text(doc.first(".price_color", card))).value_or(-1);
        b.rating = parse_rating(Html::attr(doc.first("p.star-rating", card), "class"));
        b.in_stock = doc.text(doc.first(".availability", card)).find("In stock") != std::string::npos;
        page.books.push_back(std::move(b));
    }
    if (Node next = doc.first("li.next a")) page.next_url = resolve_url(page_url, Html::attr(next, "href"));
    return page;
}

// Product page: UPC and "In stock (22 available)" from the information table.
inline void parse_detail(std::string_view html, Book& b) {
    Html doc(html);
    for (Node row : doc.select("table.table-striped tr")) {
        const std::string key = doc.text(doc.first("th", row));
        const std::string value = doc.text(doc.first("td", row));
        if (key == "UPC") b.upc = value;
        if (key == "Availability") {
            const auto open = value.find('(');
            if (open != std::string::npos) std::from_chars(value.data() + open + 1, value.data() + value.size(), b.stock_count);
        }
    }
}

Os preços permanecem como pence inteiros até ao momento em que os imprimes. A virgula flutuante binária não consegue representar 51,77 com exatidão, pelo que somar ou comparar double preços resulta em desvios de frações de um cêntimo. long Os valores inteiros, por outro lado, não sofrem essa variação. O título provém do atributo title , em vez de do seu texto, porque o texto visível é truncado com «...» em títulos longos. parse_detail Lê a tabela de informações da página do produto mais tarde, quando o rastreador visita cada livro.

main.cpp liga os cabeçalhos entre si. Começa com as inclusões, um User-Agent válido e um pequeno ficheiro auxiliar, e depois executa-se com base num nome de comando:

// main.cpp: Books to Scrape crawler (libcurl + lexbor), plus the JavaScript and Scraper API paths
#include "books.hpp"
#include "csv.hpp"
#include "http.hpp"
#include "robots.hpp"
#include <nlohmann/json.hpp>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <thread>

static const std::string kUserAgent = "bookscraper/1.0 (+https://example.com/contact)";

static std::string read_file(const std::string& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in) throw std::runtime_error("cannot open " + path);
    std::ostringstream ss; ss << in.rdbuf(); return ss.str();
}
int main(int argc, char** argv) try {
    CurlGlobal curl;
    const std::string cmd = argc > 1 ? argv[1] : "crawl";
    // if (cmd == "fixture") { … } else if (cmd == "crawl") { … } … : the commands in this guide
    return 0;
} catch (const std::exception& e) {
    std::cerr << "error: " << e.what() << '\n';
    return 1;
}

Execute primeiro o analisador numa cópia guardada da página. Se algo falhar aqui, sabe que se trata de um problema de análise e não de rede. Este é o fixture comando:

    if (cmd == "fixture") {                       // offline: parse saved HTML
        auto page = parse_listing(read_file(argv[2]), "https://books.toscrape.com/catalogue/page-1.html");
        for (size_t i = 0; i < 3 && i < page.books.size(); ++i) {
            const Book& b = page.books[i];
            std::cout << b.title << " | " << format_price(b.price_pence) << " | " << b.rating << " stars | " << b.url << '\n';
        }
        std::cout << page.books.size() << " books, next: " << page.next_url << '\n';
    }
curl -s https://books.toscrape.com/catalogue/page-1.html -o page1.html
./build/bookscraper fixture page1.html

Saída

A Light in the Attic | 51.77 | 3 stars | https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Tipping the Velvet | 53.74 | 1 stars | https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html
Soumission | 50.10 | 1 stars | https://books.toscrape.com/catalogue/soumission_998/index.html
20 books, next: https://books.toscrape.com/catalogue/page-2.html

Porquê o lexbor em vez do libxml2 ou do Gumbo?

O lexbor segue as regras de análise de HTML do WHATWG que os navegadores utilizam, possui um motor de seletores CSS integrado e é mantido ativamente. O analisador de HTML do libxml2 é anterior ao HTML5, pelo que a sua árvore pode diferir daquela que o DevTools apresenta. A Google arquivou o seu repositório do Gumbo. Existe um fork da comunidade que continua ativo, mas nenhuma das versões possui um motor de seletores.

A diferença torna-se evidente em marcação desorganizada, algo que os sites reais produzem constantemente. Aqui está um campo «input» com aninhamento incorreto, <table><b>Sale</b><tr><td>9.99</td></tr></table>, analisado pelo lexbor, pela libxml2 2.9 (a versão fornecida pela maioria das distribuições Linux) e pelo Chrome:

Saída

lexbor : <body><b>Sale</b><table><tbody><tr><td>9.99</td></tr></tbody></table></body>
libxml2: <body><table>
<b>Sale</b><tr><td>9.99</td></tr>
</table></body>
chrome  : <html><head></head><body><b>Sale</b><table><tbody><tr><td>9.99</td></tr></tbody></table></body></html>

O lexbor e o Chrome constroem a mesma árvore: o elemento <b> elemento passa para a frente da tabela e um <tbody> aparece à volta da linha. O libxml2 mantém o <b> dentro da tabela e não adiciona nenhum <tbody>. Assim, um seletor que se copie do DevTools, como table > tbody > tr, corresponde no lexbor e, silenciosamente, não devolve nada nessa árvore do libxml2. As regras que o lexbor segue estão na especificação de análise sintática do HTML da WHATWG. As versões mais recentes do libxml2 tokenizam de forma mais semelhante ao HTML5, mas ainda não constroem a árvore HTML5 completa.

Utilizar a libxml2 e o XPath em vez disso

Se o seu projeto já inclui a libxml2, ou se pensa em XPath, pode analisar a mesma página de listagem com ela. Envolva cada objeto libxml2 num std::unique_ptr com a respetiva função livre correspondente, passe HTML_PARSE_NONET para que o analisador nunca recupere nada da rede e liberte todas as cadeias de caracteres que a libxml2 lhe fornecer com xmlFree, e não free(). Se estiver a decidir entre as duas linguagens de consulta, o artigo «XPath vs. seletores CSS» compara-as lado a lado.

// libxml2 alternative: the same listing page through libxml2's HTML parser and XPath.
#include <libxml/HTMLparser.h>
#include <libxml/xpath.h>
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>

int main(int argc, char** argv) {
    std::ifstream in(argc > 1 ? argv[1] : "fixtures/books-page1.html", std::ios::binary);
    std::ostringstream ss; ss << in.rdbuf(); const std::string html = ss.str();

    std::unique_ptr<xmlDoc, decltype(&xmlFreeDoc)> doc(
        htmlReadMemory(html.data(), static_cast<int>(html.size()), "https://books.toscrape.com/catalogue/page-1.html",
                       "UTF-8", HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING | HTML_PARSE_NONET),
        &xmlFreeDoc);
    if (!doc) { std::cerr << "parse failed\n"; return 1; }
    std::unique_ptr<xmlXPathContext, decltype(&xmlXPathFreeContext)> ctx(xmlXPathNewContext(doc.get()), &xmlXPathFreeContext);
    std::unique_ptr<xmlXPathObject, decltype(&xmlXPathFreeObject)> res(
        xmlXPathEvalExpression(BAD_CAST "//article[contains(@class,'product_pod')]//h3/a/@title", ctx.get()),
        &xmlXPathFreeObject);
    if (!res || !res->nodesetval) return 1;
    for (int i = 0; i < res->nodesetval->nodeNr && i < 3; ++i) {
        xmlChar* title = xmlNodeGetContent(res->nodesetval->nodeTab[i]);
        std::cout << reinterpret_cast<char*>(title) << '\n';
        xmlFree(title);                         // libxml2 memory goes back through xmlFree, not free()
    }
    std::cout << res->nodesetval->nodeNr << " titles\n";
}
g++ -std=c++20 libxml2_xpath.cpp $(pkg-config --cflags --libs libxml-2.0) -o libxml2_xpath
./libxml2_xpath page1.html

Resultado

A Light in the Attic
Tipping the Velvet
Soumission
20 titles

Como encontrar os seletores CSS certos?

Abre a página no teu navegador, clica com o botão direito do rato nos dados e seleciona «Inspecionar». Escolha seletores curtos baseados em classes ou atributos estáveis, como «article.product_pod h3 a», e teste-os na consola do DevTools com «document.querySelectorAll». Em seguida, verifique também o HTML bruto, pois o DevTools mostra o DOM analisado, incluindo os elementos «tbody» que os navegadores inserem.

No «Books to Scrape», cada livro encontra-se num <article class="product_pod">. No seu interior, os seletores de que precisa são curtos e associados a nomes de classes que descrevem o conteúdo, o que os torna menos propensos a falhar do que caminhos longos copiados com a opção «Copiar seletor»:

Dados

Seletor (dentro do cartão)

O que ler

Título

h3 a

O title (o texto do link é truncado com «...»)

Preço

.price_color

O texto, como £51.77

Classificação

p.star-rating

A segunda palavra do class atributo, como Three

Próxima página

li.next a (página inteira)

O href atributo

Teste cada seletor na consola antes de escrever em C++: document.querySelectorAll("article.product_pod").length deve devolver 20. Em seguida, prima Ctrl+U para ver o código-fonte e confirmar que os mesmos elementos existem no HTML enviado pelo servidor, e não apenas no DOM após a execução dos scripts. Esta verificação permite detetar precocemente páginas criadas com JavaScript. A nossa ficha de referência de seletores CSS abrange seletores de atributos, pseudoclasses e combinadores, para quando os seletores simples não forem suficientes.

Como deve configurar a libcurl para a extração de dados da Web?

Crie um identificador simples por thread e reutilize-o para que as ligações permaneçam abertas. Defina um User-Agent descritivo, tempos de espera para ligação e tempo total, um limite de redirecionamentos e CURLOPT_ACCEPT_ENCODING como uma cadeia vazia para permitir a compressão. Verifique você mesmo o estado HTTP, tente novamente apenas nos erros 408, 429, 5xx e de rede, e respeite o cabeçalho Retry-After.

A HttpClient classe envolve um identificador «easy» da libcurl com esses valores predefinidos. CurlGlobal é executada curl_global_init uma vez, em main, antes de qualquer thread ser iniciada, porque essa chamada não é segura para threads.

// http.hpp: one reusable libcurl handle with timeouts, an honest User-Agent, and polite retries
#pragma once
#include <curl/curl.h>
#include <algorithm>
#include <chrono>
#include <stdexcept>
#include <string>
#include <thread>

// Call curl_global_init() once, in main(), before any thread starts.
struct CurlGlobal {
    CurlGlobal() {
        if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
            throw std::runtime_error("curl_global_init failed");
    }
    ~CurlGlobal() { curl_global_cleanup(); }
    CurlGlobal(const CurlGlobal&) = delete;
    CurlGlobal& operator=(const CurlGlobal&) = delete;
};

struct Response {
    long status = 0;
    std::string body;
};

class HttpClient {
public:
    explicit HttpClient(const std::string& user_agent, long timeout_seconds = 30)
        : curl_(curl_easy_init()) {
        if (!curl_) throw std::runtime_error("curl_easy_init failed");
        curl_easy_setopt(curl_, CURLOPT_USERAGENT, user_agent.c_str());  // libcurl copies the string
        curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L);
        curl_easy_setopt(curl_, CURLOPT_MAXREDIRS, 5L);
        curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L);
        curl_easy_setopt(curl_, CURLOPT_TIMEOUT, timeout_seconds);
        curl_easy_setopt(curl_, CURLOPT_ACCEPT_ENCODING, "");  // accept every compression this libcurl supports
        curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, &HttpClient::append);
    }
    ~HttpClient() { curl_easy_cleanup(curl_); }
    HttpClient(const HttpClient&) = delete;             // one handle per thread; never share it
    HttpClient& operator=(const HttpClient&) = delete;

    // GET a URL. Retries network errors, 408, 429, and 5xx; waits for Retry-After when the server sends it.
    Response get(const std::string& url, int max_retries = 3) {
        for (int attempt = 0;; ++attempt) {
            Response r;
            curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &r.body);
            const CURLcode rc = curl_easy_perform(curl_);
            if (rc == CURLE_OK) curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &r.status);

            const bool retryable = rc != CURLE_OK || r.status == 408 || r.status == 429 || r.status >= 500;
            if (!retryable || attempt == max_retries) {
                if (rc != CURLE_OK)
                    throw std::runtime_error(std::string("GET ") + url + " failed: " + curl_easy_strerror(rc));
                return r;
            }

            curl_off_t retry_after = 0;                   // seconds, parsed by libcurl; 0 when absent
            curl_easy_getinfo(curl_, CURLINFO_RETRY_AFTER, &retry_after);
            const auto backoff = std::chrono::seconds(1LL << attempt);   // 1 s, 2 s, 4 s
            const auto wait = retry_after > 0 ? std::chrono::seconds(std::min<curl_off_t>(retry_after, 120)) : backoff;
            std::this_thread::sleep_for(wait);
        }
    }

private:
    static size_t append(char* data, size_t size, size_t count, void* out) {
        static_cast<std::string*>(out)->append(data, size * count);
        return size * count;
    }
    CURL* curl_;
};

Quatro opções nesta classe são importantes:

  • No CURLOPT_FAILONERROR. Essa opção transforma erros HTTP em erros de transferência e descarta o corpo da resposta. O scraper precisa tanto do código de estado como do corpo da resposta, por isso verifica ele próprio o estado.
  • Reintenta apenas quando isso pode ajudar. Um erro 404 não se resolve sozinho, por isso o cliente reintenta erros de rede, respostas 408, 429 e 5xx, até três vezes.
  • CURLINFO_RETRY_AFTER faz a análise. A libcurl lê o cabeçalho Retry-After em qualquer um dos formatos (segundos ou uma data HTTP) e fornece-lhe os segundos. O cliente limita a espera a 120 segundos, para que um cabeçalho hostil ou corrompido não possa paralisar o rastreio durante horas. Na ausência do cabeçalho, ele espera 1, 2 e, em seguida, 4 segundos.
  • O identificador é reutilizado. Cada get chamada mantém a mesma ligação aberta quando o servidor o permite, o que é mais rápido para si e menos oneroso para o site.

Para ver a lógica de repetição em ação, aponte o cliente para um pequeno servidor local que responda com 429 Retry-After: 2 duas vezes e, em seguida, 200:

# Local test server: the first two requests get 429 + Retry-After: 2, then 200.
import http.server, sys
hits = 0
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        global hits; hits += 1
        if hits <= 2:
            self.send_response(429); self.send_header("Retry-After", "2"); self.end_headers(); self.wfile.write(b"slow down")
        else:
            self.send_response(200); self.end_headers(); self.wfile.write(b"ok after %d tries" % hits)
    def log_message(self, fmt, *a): sys.stderr.write("server: " + fmt % a + "\n")
http.server.HTTPServer(("127.0.0.1", 8429), H).serve_forever()
#include "../src/http.hpp"
#include <iostream>
int main() {
    CurlGlobal g;
    HttpClient http("bookscraper/1.0");
    auto t0 = std::chrono::steady_clock::now();
    Response r = http.get("http://127.0.0.1:8429/");
    auto s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
    std::cout << "status " << r.status << ", body \"" << r.body << "\", waited " << s << " s\n";
}

Saída (linhas do registo do servidor, seguidas do programa)

server: "GET / HTTP/1.1" 429 -
server: "GET / HTTP/1.1" 429 -
server: "GET / HTTP/1.1" 200 -
status 200, body "ok after 3 tries", waited 4.00422 s

O cliente esperou dois segundos duas vezes, exatamente como o servidor solicitou, e conseguiu na terceira tentativa.

User-Agent: identifica o teu scraper e dá aos proprietários do site uma forma de te contactar, como bookscraper/1.0 (+https://example.com/contact). Fingir ser um navegador de computador torna o seu tráfego mais difícil de distinguir de atividades abusivas e não o torna mais bem-vindo. O nosso guia sobre cabeçalhos HTTP para scraping na Web aborda os outros cabeçalhos que vale a pena definir.

Proxies: adicione CURLOPT_PROXY (por exemplo socks5h://host:1080) e CURLOPT_PROXYUSERPWD leia as variáveis de ambiente para encaminhar o tráfego através de um proxy. Esta opção corresponde à opção -x , pelo que as mesmas configurações utilizadas ao usar o curl com um proxy são mantidas.

cpr: se preferir uma API em C++ ao estilo do `requests`, o `cpr` envolve a `libcurl`, e o resto deste guia funciona da mesma forma.

Como se extrai dados de várias páginas em C++?

Analise o link «próxima página» em cada página de listagem, resolva-o em relação ao URL atual e repita o processo até não restarem links ou até atingir o limite de páginas. A API de URL da libcurl resolve links relativos corretamente. Faça uma pausa entre os pedidos e recupere páginas de detalhes com um pequeno conjunto fixo de threads de trabalho, cada uma com o seu próprio identificador da libcurl.

O ciclo de rastreamento é curto porque o analisador já devolve o URL seguinte:

// 1. Follow "next" links until the last page or a page limit, pausing between requests.
static std::vector<Book> crawl(HttpClient& http, std::string url, int max_pages) {
    std::vector<Book> all;
    for (int page = 1; !url.empty() && page <= max_pages; ++page) {
        Response r = http.get(url);
        if (r.status != 200) { std::cerr << "HTTP " << r.status << " for " << url << '\n'; break; }
        ListingPage lp = parse_listing(r.body, url);
        std::cerr << "page " << page << ": " << lp.books.size() << " books\n";
        all.insert(all.end(), lp.books.begin(), lp.books.end());
        url = lp.next_url;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    return all;
}

O limite de páginas protege-o de loops e de rastrear acidentalmente um catálogo de 1 000 páginas quando a sua intenção era testar apenas duas páginas. A pausa de meio segundo mantém a taxa de pedidos moderada.

A parte complicada é resolve_url em books.hpp. Os links do «Books to Scrape» são relativos: um cartão num /catalogue/page-1.html aponta para a-light-in-the-attic_1000/index.html, e o mesmo href na página inicial significa outra coisa, porque a página inicial está um diretório acima. Juntar cadeias de caracteres falha em casos como este e em ../ caminhos. A API de URL da libcurl aplica, em vez disso, as regras de resolução padrão: define o URL da página como base, coloca o href por cima dela e obtém a URL absoluta. Os testes offline apresentados mais adiante neste guia verificam tanto um caminho relativo simples como um ../ ligação.

O crawl comando verifica o ficheiro robots.txt (explicado na secção sobre scraping responsável), rastreia as páginas de listagem, visita cada página de produto e gera ambos os ficheiros de exportação:

    if (cmd == "crawl") {                  // live: listing pages, product pages, CSV + JSON
        const int max_pages = argc > 2 ? std::atoi(argv[2]) : 2;
        HttpClient http(kUserAgent);
        Response robots_txt = http.get("https://books.toscrape.com/robots.txt");
        Robots robots(robots_txt.status == 200 ? robots_txt.body : "", kUserAgent);
        std::cerr << "robots.txt: HTTP " << robots_txt.status << ", /catalogue/ allowed: "
                  << std::boolalpha << robots.allowed("/catalogue/page-1.html") << '\n';
        if (!robots.allowed("/catalogue/page-1.html")) return 1;
        auto books = crawl(http, "https://books.toscrape.com/catalogue/page-1.html", max_pages);
        enrich(books, 4);
        write_csv("books.csv", books);
        write_json("books.json", books);
        std::cout << "saved " << books.size() << " books to books.csv and books.json\n";
    }
cd build && ./bookscraper crawl 2

Saída (cerca de 7 segundos para 2 páginas de listagem, 40 páginas de produto e as pausas)

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

Quando o próprio rastreio se torna o projeto, com milhares de páginas, descoberta de links e agendamento, a API de Rastreio da WebScrapingAPI recolhe páginas relacionadas através de um único rastreio delimitado.

Como se faz o scraping de páginas em simultâneo?

Inicie um número fixo de trabalhadores std::jthread, cerca de quatro, e deixe que cada um reivindique o índice seguinte a partir de um contador std::atomic. Atribua a cada trabalhador o seu próprio identificador «easy» da libcurl, pois um identificador nunca deve ser utilizado por duas threads ao mesmo tempo. Grave cada resultado no seu próprio espaço, para que não seja necessário utilizar um mutex.

As páginas de produtos são independentes umas das outras, pelo que se adequam naturalmente a um pequeno conjunto de threads:

// 2. Visit product pages with a small, fixed number of workers. Each worker owns its own
//    libcurl handle, and each result goes into its own slot, so no mutex is needed.
static void enrich(std::vector<Book>& books, unsigned workers) {
    std::atomic<size_t> next{0};
    std::vector<std::jthread> pool;
    for (unsigned w = 0; w < workers; ++w)
        pool.emplace_back([&] {
            HttpClient http(kUserAgent);
            for (size_t i = next++; i < books.size(); i = next++) {
                try {
                    Response r = http.get(books[i].url);
                    if (r.status == 200) parse_detail(r.body, books[i]);
                } catch (const std::exception& e) { std::cerr << e.what() << '\n'; }
            }
        });
}   // jthread joins on destruction

Cada trabalhador cria o seu próprio HttpClient dentro da thread e, em seguida, entra num ciclo: obtém o índice seguinte, recupera a página desse livro e preenche upc e stock_count nessa Book. Como nunca há dois trabalhadores a aceder ao mesmo elemento e o vetor nunca altera o tamanho durante o ciclo, não há nada para bloquear. std::jthread O `join` é executado automaticamente quando é destruído, pelo que a função não pode terminar enquanto um trabalhador ainda estiver a escrever. Não há join chamadas para esquecer.

Quatro workers é um valor predefinido razoável para um único site. Mais threads, na maioria das vezes, aumentam a carga no destino. Se alguma vez precisares de centenas de transferências em paralelo, consulta a interface «multi» da libcurl, que gere muitas transferências a partir de uma única thread, em vez de adicionar threads.

Como é que se guardam os dados extraídos em CSV ou JSON?

Armazene os preços como cêntimos ou pence inteiros e, em seguida, crie um ficheiro CSV que siga a norma RFC 4180: coloque entre aspas os campos que contenham vírgulas, aspas ou quebras de linha, duplique quaisquer aspas internas e termine as linhas com CRLF. Para JSON, a função `ordered_json` da biblioteca nlohmann/json mantém os campos na ordem em que os adiciona. Ambos os formatos abrem corretamente em folhas de cálculo e ferramentas de dados.

Os títulos dos livros corrompem rapidamente um ficheiro CSV simples. O «Books to Scrape» tem títulos com vírgulas e parênteses nas primeiras páginas, e unir campos com vírgulas simples deslocaria todas as colunas a seguir a eles. O código abaixo segue a norma RFC 4180:

// csv.hpp: RFC 4180 output, so titles with commas or quotes stay in one column
#pragma once
#include "books.hpp"
#include <cstdio>
#include <fstream>
#include <string>
#include <vector>

inline std::string csv_field(const std::string& s) {
    if (s.find_first_of(",\"\r\n") == std::string::npos) return s;
    std::string out = "\"";
    for (char c : s) {
        if (c == '"') out += '"';   // a quote inside a field is written twice
        out += c;
    }
    return out + '"';
}

inline std::string format_price(long pence) {
    char buf[32];
    std::snprintf(buf, sizeof buf, "%ld.%02ld", pence / 100, pence % 100);
    return buf;
}

inline void write_csv(const std::string& path, const std::vector<Book>& books) {
    std::ofstream out(path, std::ios::binary);       // binary: "\r\n" stays exactly as written on Windows
    out << "title,price_gbp,rating,in_stock,stock_count,upc,url\r\n";
    for (const Book& b : books)
        out << csv_field(b.title) << ',' << format_price(b.price_pence) << ',' << b.rating << ','
            << (b.in_stock ? "true" : "false") << ',' << b.stock_count << ',' << csv_field(b.upc) << ','
            << csv_field(b.url) << "\r\n";
    if (!out) throw std::runtime_error("could not write " + path);
}

O ficheiro abre-se propositadamente no modo binário. No modo de texto, o Windows transforma cada \n em \r\n, pelo que os \r\n finais de linha ficariam assim: \r\r\n. O modo binário grava exatamente os bytes solicitados em todas as plataformas.

O gravador JSON utiliza ordered_json, pelo que os campos aparecem na mesma ordem que as colunas do CSV, em vez de por ordem alfabética:

static void write_json(const std::string& path, const std::vector<Book>& books) {
    nlohmann::ordered_json arr = nlohmann::ordered_json::array();   // keeps field order
    for (const Book& b : books)
        arr.push_back({{"title", b.title}, {"price_gbp", b.price_pence / 100.0}, {"rating", b.rating},
                       {"in_stock", b.in_stock}, {"stock_count", b.stock_count}, {"upc", b.upc}, {"url", b.url}});
    std::ofstream(path) << arr.dump(2) << '\n';
}

Saída: books.csv (primeiras linhas, mais a linha que precisa de aspas)

title,price_gbp,rating,in_stock,stock_count,upc,url
A Light in the Attic,51.77,3,true,22,a897fe39b1053632,https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Tipping the Velvet,53.74,1,true,20,90fa61229261140a,https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html
…
"Starving Hearts (Triangular Trade Trilogy, #1)",13.99,2,true,19,0312262ecafa5a40,https://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html

Saída: books.json (primeiro registo)

{
  "title": "A Light in the Attic",
  "price_gbp": 51.77,
  "rating": 3,
  "in_stock": true,
  "stock_count": 22,
  "upc": "a897fe39b1053632",
  "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
}

O título «Starving Hearts» permanece numa única coluna porque a vírgula está entre aspas. Os preços foram analisados com std::from_chars, que ignora a localização do utilizador, pelo que um sistema configurado para utilizar vírgulas como separadores decimais não pode alterar o resultado. O preço é analisado como dois números inteiros, libras e pence, porque o suporte a números de ponto flutuante from_chars ainda difere entre as bibliotecas padrão.

Como se faz o scraping de um site em C puro?

Utilize as mesmas duas bibliotecas sem os wrappers de C++: a libcurl grava a resposta num buffer expansível através de uma callback de escrita, e o lexbor analisa o buffer e executa seletores CSS. Liberte todos os identificadores em todos os caminhos. Uma única instrução de limpeza com «goto» mantém tudo isso controlável. O programa completo em C tem menos de 80 linhas.

Tudo o que foi referido acima também funciona em C17. O programa abaixo recupera a primeira página «Books to Scrape» e imprime todos os títulos:

/* scrape.c: plain C17. Fetch one Books to Scrape page with libcurl, print titles and prices with lexbor. */
#include <curl/curl.h>
#include <lexbor/html/html.h>
#include <lexbor/css/css.h>
#include <lexbor/selectors/selectors.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct { char *data; size_t len; } buffer;

static size_t on_data(char *ptr, size_t size, size_t n, void *userdata) {
    buffer *buf = userdata;
    size_t add = size * n;
    char *grown = realloc(buf->data, buf->len + add + 1);
    if (!grown) return 0;                       /* returning less than add makes libcurl abort the transfer */
    buf->data = grown;
    memcpy(buf->data + buf->len, ptr, add);
    buf->len += add;
    buf->data[buf->len] = '\0';
    return add;
}

static lxb_status_t on_match(lxb_dom_node_t *node, lxb_css_selector_specificity_t spec, void *ctx) {
    (void) spec; (void) ctx;
    size_t len = 0;
    const lxb_char_t *title = lxb_dom_element_get_attribute(lxb_dom_interface_element(node),
                                                            (const lxb_char_t *) "title", 5, &len);
    if (title) printf("%.*s\n", (int) len, (const char *) title);
    return LXB_STATUS_OK;
}

int main(void) {
    const char *url = "https://books.toscrape.com/catalogue/page-1.html";
    static const char selector[] = "article.product_pod h3 a";
    buffer page = {0};
    long status = 0;
    int rc = EXIT_FAILURE;

    if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return EXIT_FAILURE;
    CURL *curl = curl_easy_init();
    if (!curl) goto cleanup_global;
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "bookscraper-c/1.0 (+https://example.com/contact)");
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_data);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &page);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) { fprintf(stderr, "request failed: %s\n", curl_easy_strerror(res)); goto cleanup_curl; }
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    if (status != 200) { fprintf(stderr, "HTTP %ld\n", status); goto cleanup_curl; }

    lxb_html_document_t *doc = lxb_html_document_create();
    lxb_css_parser_t *parser = lxb_css_parser_create();
    lxb_selectors_t *sel = lxb_selectors_create();
    lxb_css_selector_list_t *list = NULL;
    if (doc && parser && sel
        && lxb_html_document_parse(doc, (const lxb_char_t *) page.data, page.len) == LXB_STATUS_OK
        && lxb_css_parser_init(parser, NULL) == LXB_STATUS_OK
        && lxb_selectors_init(sel) == LXB_STATUS_OK
        && (list = lxb_css_selectors_parse(parser, (const lxb_char_t *) selector, sizeof selector - 1)) != NULL) {
        lxb_selectors_find(sel, lxb_dom_interface_node(doc), list, on_match, NULL);
        rc = EXIT_SUCCESS;
    }
    if (list) lxb_css_selector_list_destroy_memory(list);
    if (sel) lxb_selectors_destroy(sel, true);
    if (parser) lxb_css_parser_destroy(parser, true);
    if (doc) lxb_html_document_destroy(doc);
cleanup_curl:
    curl_easy_cleanup(curl);
cleanup_global:
    curl_global_cleanup();
    free(page.data);
    return rc;
}
gcc -std=c17 -Wall -Wextra scrape.c $(pkg-config --cflags --libs libcurl lexbor) -o scrape
./scrape

Saída (primeiras 3 de 20 linhas)

A Light in the Attic
Tipping the Velvet
Soumission
…

Se instalou as bibliotecas com o vcpkg, aponte PKG_CONFIG_PATH para vcpkg_installed/<triplet>/lib/pkgconfig primeiro, porque o vcpkg inclui libcurl.pc e lexbor.pc ficheiros nesse local.

Há duas armadilhas que apanham a maioria dos scrapers em C:

  • realloc pode falhar. Atribua o seu resultado a um ponteiro temporário e verifique-o antes de sobrescrever buf->data. Escrever buf->data = realloc(buf->data, …) provoca uma fuga do buffer antigo quando a memória se esgota.
  • A função de retorno de chamada de escrita deve devolver o número de bytes que processou. Qualquer outro valor indica à libcurl que algo correu mal, e esta interrompe a transferência. É também assim que a função de retorno de chamada reporta uma falha de alocação: devolve 0.

As goto etiquetas proporcionam à função um caminho de saída por recurso, pelo que os objetos libcurl e lexbor são libertados na ordem inversa, independentemente do local onde ocorra um erro.

Como saber se uma página precisa de JavaScript?

Recupere a página com a libcurl e conte os elementos que espera encontrar. Se a contagem for zero, mas o seu navegador mostrar dados, a página constrói o seu conteúdo com JavaScript. Antes de recorrer a um navegador, procure nas tags de script do HTML bruto e no separador «Rede». Muitas vezes, os dados já lá estão no formato JSON, que pode analisar diretamente.

A /js/ versão do «Quotes to Scrape» parece idêntica à versão normal num navegador. No entanto, se a aceder com o libcurl, os elementos de citação não estão presentes. As citações encontram-se num array de JavaScript dentro de uma <script> , e o próprio script da página constrói o HTML a partir dele após o carregamento.

Isso significa que, muitas vezes, pode ignorar completamente o navegador. Esta função localiza o script que define o array e passa-o para o nlohmann/json:

// 3. JavaScript pages, option A: the data is often already in the HTML, inside a <script>.
static nlohmann::json quotes_from_script(const std::string& html) {
    Html doc(html);
    for (Node script : doc.select("script")) {
        const std::string js = doc.text(script);
        const auto start = js.find("var data = [");
        if (start == std::string::npos) continue;
        const auto open = js.find('[', start);
        const auto close = js.find("];", open);   // first "];" after the array start ends the literal
        return nlohmann::json::parse(js.substr(open, close - open + 1));
    }
    return nlohmann::json::array();
}
    if (cmd == "js") {                     // live: plain fetch of a JavaScript page
        HttpClient http(kUserAgent);
        const std::string html = http.get("https://quotes.toscrape.com/js/").body;
        Html doc(html);
        std::cout << "div.quote elements in raw HTML: " << doc.select("div.quote").size() << '\n';
        auto quotes = quotes_from_script(html);
        std::cout << "quotes found in <script> JSON: " << quotes.size() << '\n'
                  << "first: " << quotes[0]["text"].get<std::string>() << " - "
                  << quotes[0]["author"]["name"].get<std::string>() << '\n';
    }

Resultado

div.quote elements in raw HTML: 0
quotes found in <script> JSON: 10
first: “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.” - Albert Einstein

Zero elementos no HTML, dez citações no script. A extração procura por var data = [, e depois retira tudo até ao primeiro ]; que se segue. Trata-se de um padrão específico desta página, não de um analisador JavaScript geral. Noutros sites, procure application/json ou application/ld+json etiquetas script, ou abra o separador «Rede» e aceda ao ponto final JSON que a própria página utiliza.

Como se extrai páginas renderizadas em JavaScript em C++?

Execute o Chrome sem interface gráfica com a opção --dump-dom para executar os scripts da página e apresentar o HTML final; em seguida, analise essa saída com o lexbor. Para cliques, inícios de sessão ou tempos de espera, controle o Chrome através do WebDriver ou do Protocolo DevTools por HTTP. O Playwright não tem uma ligação oficial para C++. Em grande escala, uma API de renderização elimina completamente a frota de navegadores.

Quando os dados não se encontram numa tag de script, deixe que um navegador real construa a página. Um navegador sem interface gráfica funciona sem janela, e a --dump-dom imprime o DOM renderizado na saída padrão, que o C++ consegue ler através de um pipe:

// 3. JavaScript pages, option B: let headless Chrome run the scripts, then parse the DOM it prints.
//    POSIX popen; the URL is a constant here. Never paste untrusted input into a shell command.
static std::string render_with_chrome(const std::string& url) {
    const char* chrome = std::getenv("CHROME");
    const std::string cmd = std::string(chrome ? chrome : "google-chrome") +
        " --headless --disable-gpu --dump-dom '" + url + "' 2>/dev/null";
    std::unique_ptr<FILE, int (*)(FILE*)> pipe(popen(cmd.c_str(), "r"), pclose);
    if (!pipe) throw std::runtime_error("could not start Chrome");
    std::string dom; char buf[8192];
    for (size_t n; (n = std::fread(buf, 1, sizeof buf, pipe.get())) > 0;) dom.append(buf, n);
    return dom;
}
    if (cmd == "render") {                 // live: headless Chrome, then lexbor
        Html doc(render_with_chrome("https://quotes.toscrape.com/js/"));
        auto quotes = doc.select("div.quote");
        std::cout << "div.quote elements after rendering: " << quotes.size() << '\n';
        if (!quotes.empty())
            std::cout << "first author: " << doc.text(doc.first("small.author", quotes[0])) << '\n';
    }

Saída (alguns segundos, incluindo o arranque do Chrome)

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

A mesma Html classe e os mesmos seletores CSS funcionam na saída do Chrome, porque o lexbor e o Chrome analisam o HTML da mesma forma. A documentação do modo sem interface gráfica do Chrome aborda os outros sinalizadores.

Tenha em mente três limites:

  • Nunca construa o comando a partir de entradas não confiáveis. A URL aqui é uma constante. Uma URL retirada de uma página ou fornecida por um utilizador pode conter sintaxe de shell, e popen executá-la-ia. No Windows, utilize _popen e _pclose com aspas duplas à volta da URL.
  • --dump-dom imprime o DOM após o evento «load». Não aguarda dados que cheguem posteriormente através de chamadas de rede adicionais. Quando uma página carregar conteúdo depois disso, ou se precisares de cliques, inícios de sessão ou deslocamento, controla o Chrome através do WebDriver ou do Protocolo Chrome DevTools, ambos os quais utilizam HTTP ou WebSocket, aos quais a libcurl tem acesso.
  • Defina a CHROME variável de ambiente para o caminho do navegador no macOS e no Windows, onde google-chrome não está no PATH.

Para muitas sessões de navegador com inícios de sessão e interações, a API de Navegador da WebScrapingAPI executa os navegadores por si.

Como se utiliza uma API de web scraping a partir de C++?

Envie um pedido GET com a libcurl para o ponto final da API Scraper da WebScrapingAPI, passando a sua chave de API, o URL de destino codificado em URL e opções como render_js=1 e country=us. O corpo da resposta é o HTML da página, pelo que o seu analisador lexbor existente funciona sem alterações. Leia a chave a partir da variável de ambiente WSA_API_KEY.

Executar o Chrome manualmente é suficiente para algumas páginas. Quando precisar de renderização de JavaScript, de um país específico ou de acesso a sites que bloqueiam scrapers ao longo de milhares de pedidos, uma API de scraping assume o controlo dos navegadores e dos proxies. Como devolve HTML, a alteração da sua parte resume-se a um único URL:

// 4. Hand-off: the same libcurl client, pointed at WebScrapingAPI's Scraper API.
static std::string scraper_api_url(const std::string& api_key, const std::string& target,
                                   bool render_js, const std::string& country) {
    std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> c(curl_easy_init(), &curl_easy_cleanup);
    auto escape = [&](const std::string& s) {
        char* e = curl_easy_escape(c.get(), s.c_str(), static_cast<int>(s.size()));
        std::string out(e); curl_free(e); return out;
    };
    std::string url = "https://api.webscrapingapi.com/v2?api_key=" + escape(api_key) + "&url=" + escape(target);
    if (render_js) url += "&render_js=1";
    if (!country.empty()) url += "&country=" + escape(country);
    return url;
}
    if (cmd == "api") {                    // Scraper API: needs WSA_API_KEY; prints a dry run without it
        const char* key = std::getenv("WSA_API_KEY");
        const std::string url = scraper_api_url(key ? key : "YOUR_API_KEY",
                                                "https://quotes.toscrape.com/js/", true, "us");
        if (!key) { std::cout << "dry run (set WSA_API_KEY to send it):\n" << url << '\n'; return 0; }
        HttpClient http(kUserAgent, 90);          // rendered requests take longer
        Response r = http.get(url, 1);
        if (r.status != 200) { std::cerr << "Scraper API returned HTTP " << r.status << ": " << r.body << '\n'; return 1; }
        Html doc(r.body);
        std::cout << "div.quote elements via Scraper API: " << doc.select("div.quote").size() << '\n';
    }

curl_easy_escape codifica o URL de destino, de modo que o seu próprio ? e & não possam ser confundidos com os parâmetros da API. O cliente tem um tempo limite de 90 segundos, porque a renderização de uma página demora mais do que a sua obtenção, e apenas uma tentativa de repetição, pelo que uma chave errada falha rapidamente. Mantenha a chave fora do código-fonte e dos ficheiros de histórico do shell que partilha:

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

Sem a variável, o comando exibe a solicitação que enviaria:

Saída sem WSA_API_KEY

dry run (set WSA_API_KEY to send it):
https://api.webscrapingapi.com/v2?api_key=YOUR_API_KEY&url=https%3A%2F%2Fquotes.toscrape.com%2Fjs%2F&render_js=1&country=us

Com uma chave errada, a API responde com um erro 401 em JSON, e o programa exibe-o em vez de tentar analisá-lo:

Saída com uma chave inválida

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

Exemplo de saída com uma chave válida (simulada para ilustração)

div.quote elements via Scraper API: 10

A página renderizada deve conter as mesmas dez citações que o Chrome produziu, e o mesmo seletor deve encontrá-las. A WebScrapingAPI também reporta os créditos utilizados em cada pedido nos cabeçalhos de resposta, prefixados com WSA, que pode ler com CURLOPT_HEADERFUNCTION se quiser acompanhar a utilização a partir de C++.

Que abordagem de scraping em C++ deve utilizar?

Utilize o libcurl e o lexbor para páginas estáticas e verifique as tags de script antes de adicionar um navegador. Utilize o Chrome sem interface gráfica quando a página necessitar de JavaScript e o volume for reduzido. Mude para uma API de scraping quando necessitar de renderização de JavaScript, segmentação geográfica ou rotação de proxies em grande escala sem ter de executar navegadores por conta própria.

Página ou necessidade

Utilizar

Porquê

Custo

HTML estático (livros para extrair)

libcurl + lexbor

Mais rápido, com menos componentes móveis

Apenas a sua largura de banda

Dados num <script> (Citações /js/)

libcurl + nlohmann/json

Não é necessário um navegador

Apenas a sua largura de banda

Conteúdo gerado após o carregamento, poucas páginas

Chrome sem interface gráfica --dump-dom + lexbor

Renderização real do navegador

Uma instalação do Chrome; alguns segundos por página

Cliques, inícios de sessão, tempos de espera

WebDriver ou Protocolo DevTools sobre HTTP

Controlo total do navegador

Frota de navegadores para executar e atualizar

JavaScript, segmentação geográfica ou bloqueios em grande escala

API de scraper (render_js, country)

Renderização e proxies executados automaticamente

Créditos por pedido

Como testar e depurar um scraper em C++?

Guarde páginas reais como fixtures e teste o seu analisador em modo offline com o CTest, para que as falhas indiquem problemas de análise e não da rede. Compile binários de teste com o AddressSanitizer e o UndefinedBehaviorSanitizer para detetar fugas de memória e erros de memória relacionados com chamadas à biblioteca C, e execute o ThreadSanitizer no rastreador simultâneo. Atualize os fixtures quando o site sofrer alterações.

Guarde as páginas de que o seu analisador depende com curl -o em fixtures/: uma página de listagem e uma página de produto são suficientes neste caso. O teste abaixo verifica o analisador, a resolução de URLs, a análise de preços, as aspas no CSV e as regras do robots.txt, sem recorrer à rede:

// parser_test.cpp: offline checks against saved pages, run with ctest
#include "books.hpp"
#include "csv.hpp"
#include "robots.hpp"
#include <fstream>
#include <iostream>
#include <sstream>

static int failures = 0;
#define CHECK(cond) do { if (!(cond)) { std::cerr << "FAIL line " << __LINE__ << ": " #cond "\n"; ++failures; } } while (0)

static std::string slurp(const std::string& p) { std::ifstream f(p, std::ios::binary); std::ostringstream s; s << f.rdbuf(); return s.str(); }

int main() {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    const std::string base = "https://books.toscrape.com/catalogue/page-1.html";
    auto page = parse_listing(slurp(FIXTURES "/books-page1.html"), base);
    CHECK(page.books.size() == 20);
    CHECK(page.books[0].title == "A Light in the Attic");
    CHECK(page.books[0].price_pence == 5177);
    CHECK(page.books[0].rating == 3);
    CHECK(page.books[0].in_stock);
    CHECK(page.books[0].url == "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html");
    CHECK(page.next_url == "https://books.toscrape.com/catalogue/page-2.html");

    // The same href means a different URL on the home page: resolve against the page you fetched.
    CHECK(resolve_url("https://books.toscrape.com/index.html", "catalogue/page-2.html")
          == "https://books.toscrape.com/catalogue/page-2.html");
    CHECK(resolve_url("https://books.toscrape.com/catalogue/page-2.html", "../index.html")
          == "https://books.toscrape.com/index.html");

    Book b; parse_detail(slurp(FIXTURES "/book-detail.html"), b);
    CHECK(b.upc == "a897fe39b1053632");
    CHECK(b.stock_count == 22);

    CHECK(parse_pence("£0.99") == 99);
    CHECK(!parse_pence("free").has_value());
    CHECK(csv_field("Starving Hearts (Triangular Trade Trilogy, #1)") == "\"Starving Hearts (Triangular Trade Trilogy, #1)\"");
    CHECK(csv_field("6\" ruler") == "\"6\"\" ruler\"");

    // robots.txt: a 404 means no rules; otherwise the most specific matching rule wins.
    CHECK(Robots("", "bookscraper").allowed("/catalogue/page-1.html"));
    const std::string txt = "User-agent: *\nDisallow: /private/\nAllow: /private/public-page\n\n"
                            "User-agent: bookscraper\nDisallow: /search\n";
    CHECK(Robots(txt, "bookscraper/1.0").allowed("/private/x"));          // our own group replaces "*"
    CHECK(!Robots(txt, "bookscraper/1.0").allowed("/search?q=c"));
    CHECK(!Robots(txt, "otherbot").allowed("/private/x"));
    CHECK(Robots(txt, "otherbot").allowed("/private/public-page"));

    curl_global_cleanup();
    std::cout << (failures ? "FAILED" : "all parser checks passed") << '\n';
    return failures ? 1 : 0;
}

Execute-o através do CTest e, em seguida, compile o mesmo teste com os sanitizadores. O ThreadSanitizer necessita de uma compilação própria, uma vez que não pode ser combinado com o AddressSanitizer:

ctest --test-dir build --output-on-failure

# Memory and undefined-behavior checks (GCC or Clang)
g++ -std=c++20 -g -fsanitize=address,undefined -Isrc -DFIXTURES='"fixtures"' tests/parser_test.cpp $(pkg-config --cflags --libs libcurl lexbor) -o parser_test_asan && ./parser_test_asan

# Data races in the crawler (separate build; TSan can't be combined with ASan)
g++ -std=c++20 -g -fsanitize=thread src/main.cpp $(pkg-config --cflags --libs libcurl lexbor) -o bookscraper_tsan && ./bookscraper_tsan crawl 1

Saída

all parser checks passed

100% tests passed, 0 tests failed out of 1

Uma execução limpa do sanitizador apresenta apenas a saída do próprio programa. Qualquer fuga de memória, leitura fora dos limites ou conflito de dados gera um relatório com a linha exata.

Dois hábitos de depuração poupam mais tempo. Primeiro, defina CURLOPT_VERBOSE em 1 no identificador para ver todos os cabeçalhos de pedidos e respostas, o que revela rapidamente redirecionamentos, compressão e códigos de estado que não esperava. Em segundo lugar, quando um seletor não devolve nada, escreva r.body num ficheiro e abra-o. O HTML que recebeu é frequentemente uma página de consentimento, uma página de erro ou um shell JavaScript, e não a página que viu no navegador.

Como se faz scraping de forma responsável com C ou C++?

Leia o ficheiro robots.txt antes de iniciar o rastreio e ignore os caminhos não autorizados, identifique o seu scraper com um User-Agent honesto que inclua um URL de contacto e espalhe as solicitações ao longo do tempo. Recolha apenas os dados públicos de que necessita, evite dados pessoais, a menos que tenha uma base legal para tal, e respeite os termos do site. Afaste-se imediatamente quando o servidor responder com o código 429.

O rastreador já faz a maior parte disto: envia um User-Agent identificativo, faz pausas entre as páginas listadas, limita-se a quatro trabalhadores e aguarda o tempo indicado pelo Retry-After. A última parte é o robots.txt. Este pequeno verificador segue a RFC 9309:

// robots.hpp: a small RFC 9309 check for one user agent (longest matching rule wins; Allow wins ties)
#pragma once
#include <algorithm>
#include <cctype>
#include <sstream>
#include <string>
#include <vector>

class Robots {
public:
    // body = robots.txt contents; pass an empty string when the file returns 404 (everything is allowed)
    Robots(const std::string& body, std::string agent) {
        std::transform(agent.begin(), agent.end(), agent.begin(), tolower_ascii);
        std::istringstream in(body);
        std::string line;
        bool in_group = false, group_has_rules = false, matched_specific = false;
        std::vector<Rule> star, mine;
        std::vector<Rule>* target = nullptr;
        while (std::getline(in, line)) {
            line = trim(line.substr(0, line.find('#')));
            const auto colon = line.find(':');
            if (colon == std::string::npos) continue;
            std::string key = trim(line.substr(0, colon)), value = trim(line.substr(colon + 1));
            std::transform(key.begin(), key.end(), key.begin(), tolower_ascii);
            if (key == "user-agent") {
                if (!in_group || group_has_rules) { target = nullptr; group_has_rules = false; }
                in_group = true;
                std::transform(value.begin(), value.end(), value.begin(), tolower_ascii);
                if (value == "*") { if (!target) target = &star; }
                else if (agent.find(value) != std::string::npos) { target = &mine; matched_specific = true; }
            } else if (key == "allow" || key == "disallow") {
                group_has_rules = true;
                if (target && !value.empty()) target->push_back({value, key == "allow"});
            }
        }
        rules_ = matched_specific ? mine : star;
    }

    bool allowed(const std::string& path) const {
        size_t best = 0; bool allow = true;
        for (const Rule& r : rules_)
            if (path.compare(0, r.prefix.size(), r.prefix) == 0 &&
                (r.prefix.size() > best || (r.prefix.size() == best && r.allow))) {
                best = r.prefix.size(); allow = r.allow;
            }
        return allow;
    }

private:
    struct Rule { std::string prefix; bool allow; };
    static char tolower_ascii(char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }
    static std::string trim(const std::string& s) {
        const auto b = s.find_first_not_of(" \t\r"), e = s.find_last_not_of(" \t\r");
        return b == std::string::npos ? "" : s.substr(b, e - b + 1);
    }
    std::vector<Rule> rules_;
};

Seleciona o grupo correspondente ao seu User-Agent ou, na ausência deste, recorre ao * grupo quando não houver nenhum, e a regra de correspondência mais longa prevalece, com Allow empates a serem resolvidos a favor da regra mais longa. Um ficheiro robots.txt que devolve o código de estado 404 significa que não existem regras, o que é o caso do «Books to Scrape»:

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

Este verificador é deliberadamente pequeno. Não lida com o * e $ caracteres curinga que a RFC 9309 permite dentro de percursos; por isso, utilize um analisador completo para rastreios em produção de sites que não controla.

O robots.txt é uma convenção de rastreamento, não uma licença. O que pode recolher depende também dos dados, dos termos do site e da legislação do local onde opera. Leia «O web scraping é legal?» antes de extrair qualquer informação para um projeto comercial.

Perguntas frequentes

O C++ é mais rápido do que o Python para a extração de dados da Web?

Para análise e processamento, normalmente sim: o C++ compilado com o lexbor utiliza menos CPU e memória por página do que a maioria das pilhas de Python. Para a maioria dos scrapers, no entanto, a latência de rede e os atrasos de cortesia dominam o tempo total, pelo que o ganho real é menor do que os benchmarks sugerem. Opte por C++ quando o volume de análise, os limites de memória ou uma base de código existente o justificarem.

Qual é a diferença entre C, C++ e C# para o web scraping?

O C e o C++ compilam-se em código nativo e utilizam bibliotecas C, como a libcurl e o lexbor, pelo que uma única pilha serve para ambos. O C# é executado no .NET e dispõe das suas próprias ferramentas, incluindo o HttpClient, o AngleSharp e o Playwright para .NET. Os nomes parecem-se, mas as bibliotecas não se sobrepõem. Os programadores de C# devem seguir o nosso guia específico para C#.

Qual é o melhor analisador HTML para C++?

O lexbor é a melhor opção por predefinição em 2026: implementa o algoritmo de análise do HTML5 da WHATWG, inclui um motor de seletores CSS, não tem dependências externas e instala-se através do vcpkg no Windows, macOS e Linux. Utilize o libxml2 quando precisar do XPath ou já depender dele, e o pugixml apenas para XML bem formado, não para HTML do mundo real.

Devo usar o cpr ou o libcurl para web scraping?

Qualquer um funciona, porque o cpr é um wrapper em C++ à volta da libcurl com uma API mais intuitiva, ao estilo do requests. A libcurl «a crua» oferece-lhe todas as opções, tais como a análise integrada de Retry-After e a API de URL, com menos uma dependência, e as mesmas chamadas funcionam em C puro. Escolha o cpr se preferir código mais curto e apenas escrever em C++.

O lexbor funciona no Windows e no Visual Studio?

Sim. A versão do lexbor para o vcpkg compila no ambiente de trabalho do Windows, no macOS e no Linux. Apenas as aplicações da Plataforma Universal do Windows (UWP) estão excluídas. No Visual Studio, abra a pasta do projeto, defina CMAKE_TOOLCHAIN_FILE para o ficheiro da cadeia de ferramentas do vcpkg e o manifesto instala a libcurl, o lexbor e o nlohmann/json na primeira configuração. Não são necessários caminhos manuais para as bibliotecas.

O C++ consegue extrair dados de sites em JavaScript?

Sim, mas não apenas com a libcurl, porque a libcurl não executa JavaScript. Verifique primeiro se os dados já se encontram numa tag script como JSON. Caso contrário, execute o Chrome sem interface gráfica com a opção --dump-dom, controle um navegador através do WebDriver ou do Protocolo DevTools, ou chame uma API de extração com a renderização de JavaScript ativada e analise o HTML devolvido.

Como evito ser bloqueado ao fazer scraping com C++?

Envie menos pedidos, mais lentamente, com um User-Agent válido, e respeite o Retry-After quando um servidor responder com o código de erro 429. Reutilize ligações em vez de abrir novas e não execute mais workers do que o site consegue suportar. Para sites protegidos, encaminhe os pedidos através de proxies rotativos com CURLOPT_PROXY ou utilize uma API de scraping que gere proxies e navegadores.

A extração de dados públicos é legal em muitas jurisdições, mas a resposta depende do que recolhe, de como o faz e dos termos do site. Evite dados pessoais sem fundamento legal, nunca contorne inícios de sessão ou barreiras de acesso, respeite o ficheiro robots.txt e procure aconselhamento jurídico para projetos comerciais. A linguagem de programação que utiliza não altera as regras.

Como utilizo um proxy com a libcurl?

Defina CURLOPT_PROXY com o URL do proxy, como http://host:port ou socks5h://host:port, e CURLOPT_PROXYUSERPWD com as credenciais, lidas a partir de variáveis de ambiente em vez de código-fonte. A libcurl suporta proxies HTTP, HTTPS e SOCKS. Para alternar endereços IP, aponte a opção para o gateway rotativo de um fornecedor e mantenha o resto do seu scraper inalterado.

Quantas threads deve um scraper em C++ utilizar?

Comece com cerca de quatro pedidos simultâneos por site e avalie o desempenho. São as esperas de rede, e não a CPU, que limitam a maioria dos scrapers; por isso, threads adicionais aumentam principalmente a carga no alvo e elevam as taxas de bloqueio. Atribua a cada thread o seu próprio identificador da libcurl. Para milhares de transferências paralelas, utilize a interface «multi» da libcurl em algumas threads, em vez de uma thread por pedido.

Extraia dados de sites com muito JavaScript e protegidos a partir de C++

Mantenha o seu código libcurl e lexbor e deixe que a WebScrapingAPI trate da renderização e da segmentação geográfica. Adicione render_js=1 e country a uma solicitação GET e analise o HTML que já conhece. A renderização de JavaScript e a segmentação geográfica não têm custos adicionais. As solicitações falhadas não são cobradas e o período de teste gratuito inclui 5 000 solicitações. Experimente a Scraper API

Sobre o autor

Raluca Penciuc, Desenvolvedor Full-Stack @ WebScrapingAPI

Raluca Penciuc

Desenvolvedor Full-Stack

Raluca Penciuc é programadora Full Stack na WebScrapingAPI, onde desenvolve scrapers, aperfeiçoa estratégias de evasão e procura formas fiáveis de reduzir a deteção nos sites-alvo.

Web Scraping com AWS Lambda: Guia para Python e Java 2026
Guias

Web Scraping com AWS Lambda: Guia para Python e Java 2026

TL;DR: Resumo: A extração de dados da Web com o AWS Lambda funciona melhor quando cada invocação é curta, delimitada e pode ser repetida de forma independente. Comece com HTTP direto, AWS SAM e S3 e, só depois, adicione SQS, contentores, renderização no navegador, proxies ou uma camada de recuperação gerida, apenas quando a carga de trabalho demonstrar que precisa deles.

Suciu Dan32 min read
Ler artigo
Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados
Guias

Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados

TL;DR: Resumo: O GoSpider é um rastreador de linha de comandos destinado a descobrir URLs, não um extrator completo de dados estruturados. Este guia sobre como utilizar o GoSpider mostra como realizar um rastreio limitado, gerir resultados de forma organizada, fazer a transferência de dados do Colly para CSV e seguir um percurso de diagnóstico para respostas 403 ou páginas que exijam renderização em JavaScript.

Suciu Dan23 min read
Ler artigo
Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade
Guias

Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade

TL;DR: A Redfin expõe pontos de extremidade de API ocultos que retornam JSON estruturado para listagens de propriedades, tornando possível ignorar totalmente a análise HTML frágil. Este guia orienta-o na construção de um scraper Python que extrai dados de aluguer e venda, pesquisa por localização, monitoriza novas listagens através de sitemaps XML e exporta resultados limpos para CSV ou JSON.

Suciu Dan14 min read
Ler artigo

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.