Парсинг (веб-скрейпинг) на C++ — полное обзорное руководство

Веб-скрапинг на C++: libcurl, HTML-парсеры, когда оправдана низкоуровневая разработка и что она даёт по скорости.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 24 марта 2025

Оглавление

  1. Введение: зачем парсить на C++ и о чём важно помнить
  2. Как забираем страницу: HTTP-клиенты
  3. Работа с HTTPS / SSL
  4. Получение статуса ответа и заголовков
  5. Библиотеки для парсинга содержимого
  6. Решение проблем с кириллицей и кодировками
  7. Работа с cookie и сессиями
  8. Использование прокси
  9. Парсинг через TOR
  10. Многопоточность и curl_multi
  11. Хранение URL и очередей (обзорно)
  12. Что ещё нужно учесть
  13. Основные плюсы и минусы реализации на C++
  14. Итоговый рекомендуемый стек

1. Введение

Парсинг (web scraping) — это автоматическое получение веб-страниц и извлечение из них структурированных данных. Технически задача распадается на два независимых этапа:

  1. Сетевой этап — скачать HTML/JSON по HTTP(S).
  2. Этап разбора — превратить «сырой» текст в дерево (DOM) и достать нужные узлы.

C++ для этого выбирают не ради удобства (на Python или Go скрейпер пишется в разы быстрее), а ради производительности и контроля: десятки тысяч соединений на одном ядре, минимальное потребление памяти, предсказуемая латентность, отсутствие GC-пауз, лёгкая интеграция в существующий C/C++-бэкенд.

Прежде чем писать код, помните про правовую и этическую сторону: уважайте robots.txt, не создавайте чрезмерную нагрузку (rate limiting), читайте условия использования сайта и законодательство о персональных данных. Технику обхода блокировок (прокси, TOR) ниже мы рассматриваем как инструмент, а не как призыв нарушать правила площадок.


2. Как забираем страницу

Это фундамент всего скрейпера. Вариантов в C++ несколько — от низкоуровневой классики до удобных «питоноподобных» обёрток.

2.1. libcurl — отраслевой стандарт

libcurl — де-факто стандарт сетевых запросов в C/C++. Поддерживает HTTP/1.1, HTTP/2, HTTP/3, HTTPS, прокси, SOCKS5, cookie, сжатие, таймауты — буквально всё, что нужно для скрейпинга, в одной библиотеке. Минус — сишный API на колбэках, многословный.

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

// Колбэк: curl вызывает его по мере прихода данных, мы накапливаем их в строку.
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);     // идти по редиректам
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");    // gzip/deflate/br автоматом
    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) {
        // обработать curl_easy_strerror(res)
    }
    curl_easy_cleanup(curl);
    return body;
}

Обратите внимание на CURLOPT_ACCEPT_ENCODING, "" — это включает прозрачную распаковку gzip/deflate/brotli, иначе вы получите бинарный мусор вместо HTML.

2.2. cpr — «Curl for People»

cpr (C++ Requests) — современная обёртка над libcurl в духе Python Requests. Требует C++17, активно поддерживается (документация). Тот же функционал, но код короче в разы:

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;                   // тело ответа

Для большинства проектов это лучший старт: вы получаете всю мощь libcurl (прокси, cookie, SSL, asynchronous-запросы) с удобным API. Подключается через CMake FetchContent, vcpkg или Conan.

2.3. cpp-httplib — header-only

cpp-httplib — один заголовочный файл, никаких внешних зависимостей (для HTTPS нужен OpenSSL). Идеально, когда не хочется тянуть curl. Минусы: только HTTP/1.1, нет встроенной поддержки SOCKS-прокси и сжатия 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 — низкий уровень и асинхронность

Boost.Beast построена поверх Boost.Asio и даёт полный контроль над HTTP/WebSocket на уровне сокетов с асинхронной моделью (корутины, future, колбэки). Это путь для тех, кому нужны десятки тысяч одновременных соединений и кастомная сетевая логика. Цена — заметно больше кода; HTTPS придётся настраивать через Asio SSL-stream вручную.

Что выбрать

Сценарий Рекомендация
Быстро начать, типовой скрейпер cpr
Максимум контроля и фич, готовый «стандарт» libcurl напрямую
Минимум зависимостей, простой клиент cpp-httplib
Десятки тысяч асинхронных соединений Boost.Beast / Asio

3. Работа с HTTPS / SSL

Сегодня практически весь веб — HTTPS, так что TLS не опция, а норма.

3.1. Через libcurl/cpr

libcurl сам использует слой TLS (по умолчанию OpenSSL, но сборки бывают с GnuTLS, mbedTLS, BoringSSL, Schannel на Windows, Secure Transport на macOS). Главное — проверка сертификатов:

c++
// ВКЛЮЧЕНО ПО УМОЛЧАНИЮ — менять только осознанно.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // проверять цепочку сертификатов
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // проверять имя хоста
// Указать собственный CA-bundle, если системный не найден:
curl_easy_setopt(curl, CURLOPT_CAINFO, "/path/to/cacert.pem");

Никогда не отключайте VERIFYPEER/VERIFYHOST в продакшене ради «починки» ошибок сертификата — это открывает дверь для MITM-атак. Если системный набор корневых сертификатов отсутствует (часто на Windows/в контейнерах), скачайте актуальный cacert.pem (его публикует проект curl) и пропишите через CURLOPT_CAINFO.

В cpr поведение по умолчанию безопасное; при необходимости настраивается через cpr::SslOptions.

3.2. Тонкости

  • SNI (Server Name Indication) включён по умолчанию — нужен для виртуальных хостов.
  • Версия TLS: имеет смысл форсировать минимум TLS 1.2 (CURLOPT_SSLVERSION = CURL_SSLVERSION_TLSv1_2).
  • TLS-fingerprinting: продвинутые антибот-системы умеют различать клиентов по JA3/JA4-отпечатку TLS-рукопожатия. Обычный libcurl-отпечаток отличается от браузерного — это отдельная большая тема (вплоть до сборок curl с патчами под браузерный ClientHello).

4. Статус ответа и заголовки

Скрейпер обязан реагировать на HTTP-коды: 200 — ок, 301/302 — редирект, 403/429 — заблокировали/рейт-лимит, 5xx — ошибка сервера (повторить позже).

4.1. Код ответа и заголовки в 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);

// Полный набор заголовков ловим отдельным колбэком:
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers_map);

Полезные getinfo-поля: CURLINFO_RESPONSE_CODE, CURLINFO_CONTENT_TYPE, CURLINFO_EFFECTIVE_URL (финальный URL после редиректов), CURLINFO_REDIRECT_COUNT, CURLINFO_TOTAL_TIME, CURLINFO_SIZE_DOWNLOAD.

4.2. В cpr — всё уже разобрано

c++
cpr::Response r = cpr::Get(cpr::Url{"https://example.com"});
r.status_code;               // 200
r.reason;                    // "OK"
r.header["content-type"];    // удобная map заголовков
r.url;                       // финальный URL
r.elapsed;                   // время запроса

Зачем это парсеру: из заголовка Content-Type: text/html; charset=windows-1251 вытаскивается кодировка (см. раздел про кириллицу), а из Retry-After при 429 — сколько ждать перед повтором.


5. Библиотеки для парсинга содержимого

Скачали HTML — теперь нужно построить DOM и достать узлы по селекторам. Не парсите HTML регулярными выражениями — реальная вёрстка с незакрытыми тегами, вложенностью и комментариями ломает любой regex; используйте полноценный парсер.

5.1. lexbor — современный выбор №1

lexbor (GitHub) — быстрый, соответствующий стандарту WHATWG HTML5-парсер на чистом C, без внешних зависимостей. Поддерживает CSS-селекторы, определение кодировки по байтовому потоку, работу с DOM. Это фактический преемник myhtml/Modest и сейчас лучший вариант для нового кода (его, к слову, использует встроенный DOM-парсер PHP 8.4).

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

lxb_html_document_t* doc = lxb_html_document_create();
lxb_html_document_parse(doc, (const lxb_char_t*)html.data(), html.size());
// далее обход DOM или поиск через модуль selectors (CSS-селекторы)
lxb_html_document_destroy(doc);

Для удобной C++-обёртки поверх lexbor существуют сторонние проекты (например, sprexer).

5.2. libxml2 + XPath

libxml2 — зрелая, проверенная временем библиотека. Её htmlReadMemory() разбирает «грязный» HTML, а XPath даёт мощные выборки (CSS-селекторов из коробки нет, но XPath выразительнее). Отличный выбор, если данные хорошо ложатся на 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);
// обойти res->nodesetval->nodeTab

5.3. Gumbo — классика, но архивирована

Gumbo от Google долго был стандартом HTML5-парсинга в C/C++, но репозиторий архивирован (read-only с января 2026 г.). Развитие продолжает форк сообщества на Codeberg. Для нового кода предпочтительнее lexbor; Gumbo упоминается, потому что вы встретите его во множестве существующих проектов.

5.4. Прочие инструменты

  • htmlcxx — простой C++ HTML/CSS-парсер; удобен для лёгких задач, но давно не развивается.
  • pugixml и RapidXML — для строгого XML (RSS, sitemap, SOAP), не для произвольного HTML.
  • JSON: многие сайты отдают данные через API/встроенный JSON. Берите nlohmann/json (удобство) или RapidJSON (скорость).

Что выбрать

Задача Рекомендация
Новый HTML5-скрейпер, нужны CSS-селекторы lexbor
Сложные выборки, привычен XPath libxml2
Поддержка legacy-кода Gumbo / форк на Codeberg
Строгий XML (RSS/sitemap) pugixml
JSON-API nlohmann/json или RapidJSON

6. Кириллица и кодировки

Классическая боль: страница приходит «кракозябрами». Причина почти всегда одна — несовпадение кодировок. Рунет встречается в UTF-8, windows-1251 (cp1251), реже KOI8-R. Внутри программы держите всё в UTF-8, конвертируя на входе.

6.1. Как определить исходную кодировку

Источники (в порядке приоритета):

  1. HTTP-заголовок Content-Type: text/html; charset=windows-1251.
  2. HTML-мета: <meta charset="..."> или <meta http-equiv="Content-Type" content="...; charset=...">.
  3. BOM в начале файла (для UTF-8/16).
  4. Эвристика по содержимому (если ничего нет).

Удобно, что lexbor умеет определять кодировку по байтовому потоку сам — это снимает бóльшую часть проблемы ещё на этапе парсинга.

6.2. Конвертация в UTF-8

Вариант А — iconv (GNU libiconv), есть почти везде:

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

Вариант Б — ICU (International Components for Unicode) — самый мощный и надёжный путь: огромный список кодировок, нормализация Unicode, детектор кодировки (ucsdet_*):

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

Вариант В — UTF8-CPP (utfcpp) — лёгкая header-only библиотека для валидации/итерации/преобразований уже UTF-8/UTF-16/UTF-32 (она не перекодирует cp1251, но незаменима для корректной работы с самим UTF-8).

6.3. Практические грабли

  • Не печатайте UTF-8 в Windows-консоль без SetConsoleOutputCP(CP_UTF8) — увидите мусор, хотя данные корректны.
  • На Windows для имён файлов с кириллицей используйте wide-API (std::wstring/UTF-16).
  • std::string хранит байты, а не «символы»; для подсчёта именно символов кириллицы считайте по кодпойнтам (utfcpp/ICU), а не по .size().
  • Всегда фиксируйте инвариант: «на входе — детект и перекодирование в UTF-8, дальше по всему пайплайну только UTF-8».

Многие сайты требуют сессий: логин, корзина, антибот-проверки, пагинация за авторизацией. Cookie нужно принимать, хранить и отправлять обратно.

libcurl имеет встроенный «cookie engine»:

c++
// Включить движок и хранить cookie в файле между запусками:
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); // читать (пустая строка — просто включить движок в памяти)
curl_easy_setopt(curl, CURLOPT_COOKIEJAR,  "cookies.txt"); // записать при cleanup

// Передать конкретную cookie вручную:
curl_easy_setopt(curl, CURLOPT_COOKIE, "session=abc123; lang=ru");

В пределах одной «сессии» переиспользуйте один и тот же CURL-handle (или один cpr::Session) — тогда cookie, keep-alive соединения и TLS-сессии сохраняются между запросами, что и быстрее, и правильнее с точки зрения логики сайта.

В cpr:

c++
cpr::Session session;
session.SetUrl(cpr::Url{"https://example.com/login"});
session.SetCookies(cpr::Cookies{{"session", "abc123"}});
cpr::Response r = session.Get();
cpr::Cookies received = r.cookies; // полученные сервером cookie

Подводный камень: для корректного редиректа с переносом cookie между поддоменами включайте cookie engine до запроса и не пересоздавайте handle на каждый шаг.


8. Прокси

Прокси нужны для обхода гео-ограничений, распределения нагрузки и снижения шанса блокировки по IP. libcurl поддерживает HTTP-, HTTPS- и SOCKS5-прокси «из коробки».

c++
// HTTP-прокси с авторизацией:
curl_easy_setopt(curl, CURLOPT_PROXY, "http://user:pass@proxyhost:8080");

// SOCKS5 (и резолв DNS на стороне прокси — важно для анонимности):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://proxyhost:1080");

В cpr:

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

Ротация прокси

Для масштабного скрейпинга держат пул прокси и ротируют их: по кругу, случайно или с учётом «здоровья» (бан → временно исключить). Простейшая стратегия — словарь {прокси → счётчик ошибок/время простоя} и выбор живого прокси перед каждым запросом. Под каждый прокси разумно держать отдельный cookie-jar и User-Agent, чтобы «личности» не пересекались.

Префикс socks5h:// (с буквой h) означает, что DNS-резолвинг идёт через прокси, а не локально — иначе ваш реальный DNS-запрос выдаст вас. Это критично и для прокси, и особенно для TOR (ниже).


9. Парсинг через TOR

TOR — частный случай SOCKS5-прокси: локальный Tor-демон поднимает SOCKS5 обычно на 127.0.0.1:9050 (Tor Browser — на 9150). Достаточно направить запросы туда:

c++
// Весь трафик через сеть Tor; 'h' — резолвим DNS внутри Tor (обязательно!):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");

Смена «личности» (нового выходного узла)

Через ControlPort Tor (порт 9051) можно по сигналу NEWNYM получить новую цепочку, то есть фактически новый IP. Для этого открываем TCP-сокет к control-порту и отправляем команды по Tor Control Protocol:

code
AUTHENTICATE "пароль"
SIGNAL NEWNYM
QUIT

Это позволяет менять выходной IP между «волнами» запросов. На практике учитывайте:

  • Между NEWNYM Tor вводит небольшой кулдаун — не дёргайте сигнал слишком часто.
  • Tor медленный: высокая латентность, узкий канал. Для массового скрейпинга это скорее инструмент анонимности, чем производительности.
  • Многие сайты блокируют известные exit-узлы Tor целиком (403/CAPTCHA).
  • Не пускайте через Tor задачи, где вы и так авторизуетесь под реальным аккаунтом — анонимность теряется на уровне приложения.

Технически слой кода тот же, что и для прокси (раздел 8); меняется только адрес прокси и добавляется логика общения с control-портом.


10. Многопоточность

Скрейпинг — задача I/O-bound: основное время уходит на ожидание сети. Параллелизм даёт кратный прирост. Есть два принципиально разных подхода.

10.1. Пул потоков + по handle на поток

Классика: пул рабочих потоков, общая потокобезопасная очередь URL, у каждого потока — свой CURL-handle.

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

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

void worker() {
    CURL* curl = curl_easy_init();   // СВОЙ handle на поток
    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);
        // ... парсинг ...
    }
    curl_easy_cleanup(curl);
}

int main() {
    curl_global_init(CURL_GLOBAL_ALL); // ОДИН раз, до старта потоков!
    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();
}

Критично: curl_global_init() вызывается один раз в главном потоке до запуска рабочих; CURL-handle нельзя шарить между потоками — у каждого потока свой (см. curl: threadsafe).

10.2. curl_multi — много соединений в одном потоке

curl_multi ведёт сотни/тысячи параллельных передач в одном потоке через мультиплексирование (epoll/poll). Это эффективнее по памяти, чем «поток на соединение», и идеально масштабируется. Минус — сложнее код (цикл curl_multi_perform + обработка событий). Можно комбинировать: несколько потоков, в каждом свой multi-стек.

10.3. Готовые примитивы параллелизма

  • std::thread / std::async — базовые средства STL.
  • oneTBB (Intel Threading Building Blocks) — высокоуровневые параллельные алгоритмы, конвейеры (parallel_pipeline хорошо ложится на схему «скачать → распарсить → сохранить»), потокобезопасные контейнеры.
  • OpenMP — простое распараллеливание циклов прагмами; чаще полезно для CPU-тяжёлого парсинга, чем для сетевого ожидания.

Рекомендация

Для большинства проектов: пул потоков (16–64) + по handle на поток. Когда упрётесь в память/число потоков при тысячах соединений — переходите на curl_multi.


11. Хранение URL и очередей

(Раздел обзорный — это уже архитектура краулера.) Как только парсер начинает переходить по ссылкам, появляется задача управления frontier — очередью ещё не посещённых URL — и дедупликацией.

Ключевые подзадачи:

  • Очередь заданий (frontier). В простом случае — std::queue в памяти. Для устойчивости и распределённости — внешний брокер: RabbitMQ (клиент rabbitmq-c) или Redis как очередь/множество (клиент hiredis).
  • Дедупликация URL. Чтобы не качать одно и то же: std::unordered_set нормализованных URL в памяти; при больших объёмах — Bloom-фильтр (компактно, ценой редких ложноположительных) или ключи в Redis.
  • Нормализация URL. Приведение к канону (схема, регистр хоста, сортировка query, убирание #fragment, разрешение относительных ссылок). Поможет Boost.URL.
  • Персистентность. Состояние обхода и результаты — в БД: SQLite для одной машины, PostgreSQL/ClickHouse — для масштаба.
  • Политика обхода. BFS/DFS, приоритеты по доменам, per-host rate limiting и вежливые задержки, ограничение глубины.

Типовой конвейер: frontier → загрузчик (пул/multi) → парсер → экстрактор ссылок → нормализация → дедуп → обратно во frontier, а извлечённые данные — в хранилище.


12. Что ещё нужно учесть

Темы, которые часто забывают, но без них «боевой» скрейпер быстро ломается:

  • robots.txt и вежливость. Парсьте и уважайте robots.txt (есть официальный парсер google/robotstxt); ставьте задержки и ограничивайте RPS на домен. Это и этично, и снижает риск бана.

  • Антибот-защита и отпечатки. Сайты анализируют User-Agent, набор и порядок заголовков, TLS-fingerprint (JA3/JA4), HTTP/2-фрейминг, поведение. Минимум — задавайте правдоподобные заголовки (Accept, Accept-Language, User-Agent) и не «стучите» одинаково. CAPTCHA/JS-челленджи — отдельный пласт проблем.

  • JavaScript-рендеринг. libcurl/lexbor видят только исходный HTML, без выполнения JS. Для SPA-сайтов (React/Vue), где контент подгружается скриптами, нужен либо headless-браузер (управление Chromium по Chrome DevTools Protocol из C++), либо — что чаще проще — найти и дёрнуть тот же внутренний JSON-API, который вызывает фронтенд.

  • Сжатие контента. Включайте Accept-Encoding (gzip/deflate/brotli) — экономит трафик в разы; libcurl распаковывает прозрачно при CURLOPT_ACCEPT_ENCODING. Для ручной распаковки — zlib и brotli.

  • Повторы и backoff. Сеть нестабильна. Реализуйте retry с экспоненциальной задержкой и джиттером, уважайте заголовок Retry-After при 429/503, ставьте разумные таймауты (CURLOPT_TIMEOUT, CURLOPT_CONNECTTIMEOUT).

  • Память и ресурсы. Закрывайте handle, освобождайте DOM-деревья (*_destroy), следите за Content-Length/лимитом размера ответа (CURLOPT_MAXFILESIZE), чтобы случайно не выкачать гигабайтный файл в память.

  • Логирование и мониторинг. Считайте коды ответов, скорость, долю банов по доменам — иначе непонятно, когда скрейпер «тихо умер».


13. Плюсы и минусы

Плюсы реализации на C++

  • Производительность и низкая латентность. Близкая к «железу» скорость, минимум накладных расходов на запрос.
  • Эффективность по памяти. Можно вести тысячи соединений на скромных ресурсах (особенно curl_multi), нет пауз сборщика мусора.
  • Полный контроль. Точная настройка TLS, сокетов, таймаутов, памяти — там, где высокоуровневые языки прячут детали.
  • Зрелая экосистема C-библиотек. libcurl, OpenSSL, libxml2, ICU, lexbor — промышленный, проверенный фундамент.
  • Лёгкая интеграция в существующий C/C++-бэкенд без межъязыковых мостов.

Минусы

  • Дольше и дороже разработка. То, что в Python пишется за вечер, в C++ требует больше кода и аккуратности.
  • Ручное управление ресурсами. Утечки памяти, висящие handle, гонки в многопоточности — ваша ответственность.
  • Сишные API на колбэках (libcurl, libxml2) многословны; обёртки (cpr) частично спасают.
  • Слабая «батарейка» для JS-сайтов. Нет нативного эквивалента Selenium/Playwright; headless-браузер из C++ — это боль.
  • Меньше готовых скрейпинг-фреймворков, чем в Python (нет аналога Scrapy «из коробки») — больше пишется руками.

Вывод: C++ оправдан, когда критичны масштаб, скорость и потребление ресурсов (высоконагруженные краулеры, продукт на C++-стеке). Для разовых задач и прототипов быстрее взять Python/Go.


14. Итоговый стек

Сбалансированный набор для production-скрейпера на C++:

Слой Библиотека
HTTP-клиент cpr (поверх libcurl)
TLS OpenSSL (через libcurl)
HTML-парсинг lexbor (или libxml2 под XPath)
JSON nlohmann/json / RapidJSON
Кодировки ICU или iconv
Многопоточность пул std::thread + curl_multi, при необходимости oneTBB
Прокси/TOR libcurl (socks5h://) + Tor ControlPort
Очередь/дедуп Redis / RabbitMQ, Bloom-фильтр
Хранилище SQLite / PostgreSQL
URL-нормализация Boost.URL

Начните с малого — cpr + lexbor + один поток — и наращивайте сложность (прокси, многопоточность, очереди) только под реальную нагрузку.