Оглавление
- Введение: зачем парсить на C++ и о чём важно помнить
- Как забираем страницу: HTTP-клиенты
- Работа с HTTPS / SSL
- Получение статуса ответа и заголовков
- Библиотеки для парсинга содержимого
- Решение проблем с кириллицей и кодировками
- Работа с cookie и сессиями
- Использование прокси
- Парсинг через TOR
- Многопоточность и
curl_multi - Хранение URL и очередей (обзорно)
- Что ещё нужно учесть
- Основные плюсы и минусы реализации на C++
- Итоговый рекомендуемый стек
1. Введение
Парсинг (web scraping) — это автоматическое получение веб-страниц и извлечение из них структурированных данных. Технически задача распадается на два независимых этапа:
- Сетевой этап — скачать HTML/JSON по HTTP(S).
- Этап разбора — превратить «сырой» текст в дерево (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 на колбэках, многословный.
#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, активно поддерживается (документация). Тот же функционал, но код короче в разы:
#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.
#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). Главное — проверка сертификатов:
// ВКЛЮЧЕНО ПО УМОЛЧАНИЮ — менять только осознанно.
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
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 — всё уже разобрано
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).
#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-выражения.
#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. Как определить исходную кодировку
Источники (в порядке приоритета):
- HTTP-заголовок
Content-Type: text/html; charset=windows-1251. - HTML-мета:
<meta charset="...">или<meta http-equiv="Content-Type" content="...; charset=...">. - BOM в начале файла (для UTF-8/16).
- Эвристика по содержимому (если ничего нет).
Удобно, что lexbor умеет определять кодировку по байтовому потоку сам — это снимает бóльшую часть проблемы ещё на этапе парсинга.
6.2. Конвертация в UTF-8
Вариант А — iconv (GNU libiconv), есть почти везде:
#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_*):
#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».
7. Cookie и сессии
Многие сайты требуют сессий: логин, корзина, антибот-проверки, пагинация за авторизацией. Cookie нужно принимать, хранить и отправлять обратно.
libcurl имеет встроенный «cookie engine»:
// Включить движок и хранить 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:
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-прокси «из коробки».
// HTTP-прокси с авторизацией:
curl_easy_setopt(curl, CURLOPT_PROXY, "http://user:pass@proxyhost:8080");
// SOCKS5 (и резолв DNS на стороне прокси — важно для анонимности):
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://proxyhost:1080");
В cpr:
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). Достаточно
направить запросы туда:
// Весь трафик через сеть 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:
AUTHENTICATE "пароль"
SIGNAL NEWNYM
QUIT
Это позволяет менять выходной IP между «волнами» запросов. На практике учитывайте:
- Между
NEWNYMTor вводит небольшой кулдаун — не дёргайте сигнал слишком часто. - Tor медленный: высокая латентность, узкий канал. Для массового скрейпинга это скорее инструмент анонимности, чем производительности.
- Многие сайты блокируют известные exit-узлы Tor целиком (
403/CAPTCHA). - Не пускайте через Tor задачи, где вы и так авторизуетесь под реальным аккаунтом — анонимность теряется на уровне приложения.
Технически слой кода тот же, что и для прокси (раздел 8); меняется только адрес прокси и добавляется логика общения с control-портом.
10. Многопоточность
Скрейпинг — задача I/O-bound: основное время уходит на ожидание сети. Параллелизм даёт кратный прирост. Есть два принципиально разных подхода.
10.1. Пул потоков + по handle на поток
Классика: пул рабочих потоков, общая потокобезопасная очередь URL, у каждого потока — свой
CURL-handle.
#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 + один поток — и наращивайте сложность (прокси,
многопоточность, очереди) только под реальную нагрузку.