Web-скрапинг с помощью C++ (cpp)
Веб-скрапинг — это распространенная техника сбора данных в Интернете, при которой клиент HTTP обрабатывает запрос пользователя на получение данных и использует парсер HTML для извлечения этой информации. Это помогает программистам более легко получать необходимую информацию для своих проектов.
Существует множество вариантов использования веб-скрапинга. Он позволяет получить доступ к данным, которые могут быть недоступны через API, а также к данным из нескольких разрозненных источников. С помощью этого метода можно собрать и проанализировать мнения пользователей о продукте, а также получить представление о состоянии рынка, например, о волатильности цен или проблемах дистрибуции. Однако собрать эти данные или интегрировать их в свои проекты не всегда было просто.
К счастью, веб-скрапинг стал более совершенным, и ряд языков программирования поддерживают его, включая C++. Этот популярный язык системного программирования также обладает рядом особенностей, которые делают его полезным для веб-скрапинга, например, скоростью, строгой статической типизацией и стандартной библиотекой, включающей в себя вывод типов, шаблоны для общего программирования, примитивы для параллелизма и лямбда-функции.
В этом руководстве вы узнаете, как использовать C++ для реализации веб-скрапинга с помощью библиотек libcurl и gumbo. Вы можете следить за развитием событий на GitHub.
Предварительные условия
Для этого урока вам понадобится следующее:
- базовое понимание HTTP
- C++ 11 или новее, установленный на вашей машине
- g++ 4.8.1 или новее
- библиотеки libcurl и gumbo C
- ресурс с данными для скрапинга (вы будете использовать сайт Merriam-Webster)
О веб-скрапинге
На каждый запрос HTTP, сделанный клиентом (например, браузером), сервер выдает ответ. И запросы, и ответы сопровождаются заголовками, которые описывают аспекты данных, которые клиент намерен получить, и объясняют все нюансы отправленных данных для сервера.
Например, допустим, вы сделали запрос на сайт Merriam-Webster для получения определений слова «эзотерический», используя cURL в качестве клиента:
GET /dictionary/esoteric HTTP/2 Host: www.merriam-webster.com user-agent: curl/7.68.0 accept: */*
Сайт Merriam-Webster будет отвечать заголовками, чтобы идентифицировать себя как сервер, HTTP-кодом ответа для обозначения успеха (200), форматом данных ответа — HTML в данном случае — в заголовке content-type, директивами кэширования и дополнительной метаданными CDN. Это может выглядеть примерно так:
HTTP/2 200 content-type: text/html; charset=UTF-8 date: Wed, 11 May 2022 11:16:20 GMT server: Apache cache-control: max-age=14400, public pragma: cache access-control-allow-origin: * vary: Accept-Encoding x-cache: Hit from cloudfront via: 1.1 5af4fdb44166a881c2f1b1a2415ddaf2.cloudfront.net (CloudFront) x-amz-cf-pop: NBO50-C1 x-amz-cf-id: HCbuiqXSALY6XbCvL8JhKErZFRBulZVhXAqusLqtfn-Jyq6ZoNHdrQ== age: 5787 <!DOCTYPE html> <html lang="en"> <head> <!--rest of it goes here-->
Вы должны получить аналогичные результаты после создания своего скрапера. Одна из двух библиотек, которые вы будете использовать в этом учебнике, — libcurl, на основе которой написан cURL.
Создание веб-скрапера
Скрапер, который вы собираетесь создать на C++, будет получать определения слов с сайта Merriam-Webster, избавляя вас от необходимости набирать текст, связанный с обычным поиском слов. Вместо этого вы сведете процесс к одному набору нажатий клавиш.
В этом руководстве вы будете работать в каталоге с именем scraper и с единственным одноименным файлом C++: scraper.cc.
Настройка библиотек
Две библиотеки на языке C, которые вы собираетесь использовать, libcurl и gumbo, работают здесь благодаря тому, что C++ хорошо взаимодействует с C. В то время как libcurl — это API, которое позволяет использовать несколько функций, связанных с URL и HTTP, и используется в клиенте с тем же именем, который был использован в предыдущем разделе, gumbo — это легковесный парсер HTML-5 с привязками к нескольким языкам, совместимым с С.
Использование vcpkg
Разработанный компанией Microsoft, vcpkg является кроссплатформенным менеджером пакетов для проектов C/C++. Следуйте этому руководству, чтобы настроить vcpkg на вашей машине. Вы можете установить libcurl и gumbo, набрав в консоли следующее:
$ vcpkg install curl $ vcpkg install gumbo
Если вы работаете в среде IDE, а именно в Visual Studio Code, выполните следующий фрагмент в корневом каталоге вашего проекта, чтобы интегрировать пакеты:
$ vcpkg integrate install
Использование apt
Если вы работали с Linux, вы должны быть знакомы с apt, которая позволяет удобно находить источники и управлять библиотеками, установленными в системе. Чтобы установить libcurl и gumbo с помощью apt, введите в консоль следующее:
$ sudo apt install libcurl4-openssl-dev libgumbo-dev
Установка библиотек
Вместо того чтобы выполнять установку вручную, вы можете воспользоваться методом, показанным ниже.
Сначала клонируйте репозиторий curl и установите его глобально:
$ git clone https://github.com/curl/curl.git <directory> $ cd <directory> $ autoreconf -fi $ ./configure $ make
Затем клонируйте репозиторий gumbo и установите пакет:
$ sudo apt install libtool $ git clone https://github.com/google/gumbo-parser.git <directory> $ cd <directory> $ ./autogen.sh $ ./configure $ make && sudo make install
Пишем парсер (скрапер)
Первым шагом в написании парсера является создание инструмента для выполнения HTTP-запроса. Артефакт — функция с названием request — позволит инструменту для скрапинга словаря получать разметку с сайта Merriam-Webster.
В функции request, определенной в файле scraper.cc, в приведенном ниже фрагменте кода, определены неизменяемые примитивы — имя клиента, которое идентифицирует скрапер через заголовок user-agent, и артефакты языка для записи разметки ответа сервера в память. Единственным параметром является слово, которое является частью пути URL, определения которого получает скрапер.
typedef size_t( * curl_write)(char * , size_t, size_t, std::string * ); std::string request(std::string word) { CURLcode res_code = CURLE_FAILED_INIT; CURL * curl = curl_easy_init(); std::string result; std::string url = "https://www.merriam-webster.com/dictionary/" + word; curl_global_init(CURL_GLOBAL_ALL); if (curl) { curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, static_cast < curl_write > ([](char * contents, size_t size, size_t nmemb, std::string * data) -> size_t { size_t new_size = size * nmemb; if (data == NULL) { return 0; } data -> append(contents, new_size); return new_size; })); curl_easy_setopt(curl, CURLOPT_WRITEDATA, & result); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_USERAGENT, "simple scraper"); res_code = curl_easy_perform(curl); if (res_code != CURLE_OK) { return curl_easy_strerror(res_code); } curl_easy_cleanup(curl); } curl_global_cleanup(); return result; }
Не забудьте включить соответствующие заголовки в преамбулу вашего файла .cc или .cpp для библиотеки curl и библиотеки строк C++. Это позволит избежать проблем с компиляцией при подключении библиотек.
#include “curl/curl.h” #include “string”
Следующий шаг, разбор разметки, требует выполнения четырех функций: scrape, find_definitions, extract_text и str_replace. Поскольку gumbo занимает центральное место в разборе разметки, добавьте соответствующий заголовок библиотеки следующим образом:
#include “gumbo.h”
Функция scrape передает разметку из запроса в find_definitions для выборочного итеративного обхода DOM. В этой функции вы будете использовать парсер gumbo, который возвращает строку, содержащую список определений слов:
std::string scrape(std::string markup) { std::string res = ""; GumboOutput *output = gumbo_parse_with_options(&kGumboDefaultOptions, markup.data(), markup.length()); res += find_definitions(output->root); gumbo_destroy_output(&kGumboDefaultOptions, output); return res; }
Функция find_definitions ниже рекурсивно собирает определения из элементов HTML span с уникальным идентификатором класса «dtText«. Она извлекает текст определения с помощью функции extract_text на каждой успешной итерации из каждого HTML-узла, в котором находится этот текст.
std::string find_definitions(GumboNode *node) { std::string res = ""; GumboAttribute *attr; if (node->type != GUMBO_NODE_ELEMENT) { return res; } if ((attr = gumbo_get_attribute(&node->v.element.attributes, "class")) && strstr(attr->value, "dtText") != NULL) { res += extract_text(node); res += "\n"; } GumboVector *children = &node->v.element.children; for (int i = 0; i < children->length; ++i) { res += find_definitions(static_cast<GumboNode *>(children->data[i])); } return res; }
Далее, функция extract_text ниже извлекает текст из каждого узла, который не является тегом script или style. Функция направляет текст на процедуру str_replace, которая заменяет ведущее двоеточие на бинарный символ >.
std::string extract_text(GumboNode *node) { if (node->type == GUMBO_NODE_TEXT) { return std::string(node->v.text.text); } else if (node->type == GUMBO_NODE_ELEMENT && node->v.element.tag != GUMBO_TAG_SCRIPT && node->v.element.tag != GUMBO_TAG_STYLE) { std::string contents = ""; GumboVector *children = &node->v.element.children; for (unsigned int i = 0; i < children->length; ++i) { std::string text = extract_text((GumboNode *)children->data[i]); if (i != 0 && !text.empty()) { contents.append(""); } contents.append(str_replace(":", ">", text)); } return contents; } else { return ""; } }
Функция str_replace (вдохновленная одноименной функцией PHP) заменяет каждый экземпляр заданной строки поиска в большей строке на другую строку. Она выглядит следующим образом:
std::string str_replace(std::string search, std::string replace, std::string &subject) { size_t count; for (std::string::size_type pos{}; subject.npos != (pos = subject.find(search.data(), pos, search.length())); pos += replace.length(), ++count) { subject.replace(pos, search.length(), replace.data(), replace.length()); } return subject; }
Поскольку обход и замена в приведенной выше функции зависят от примитивов, определенных в библиотеке алгоритмов, вам также потребуется включить эту библиотеку:
#include ”algorithm”
Далее вы добавите динамическость в скрейпер, позволяя ему возвращать определения для каждого слова, переданного в качестве аргумента командной строки. Для этого вы определите функцию, которая преобразует каждый аргумент командной строки в его эквивалент в нижнем регистре, что минимизирует возможность ошибок запросов из-за перенаправлений и ограничивает ввод до одного аргумента командной строки.
Добавьте функцию, которая преобразует входные строки в их эквиваленты в нижнем регистре:
std::string strtolower(std::string str) { std::transform(str.begin(), str.end(), str.begin(), ::tolower); return str; }
Далее следует логика ветвления, которая выборочно анализирует один аргумент командной строки:
if (argc != 2) { std::cout << "Please provide a valid English word" << std::endl; exit(EXIT_FAILURE); }
Основная функция в вашем скрапере должна выглядеть так, как показано ниже:
int main(int argc, char **argv) { if (argc != 2) { std::cout << "Please provide a valid English word" << std::endl; exit(EXIT_FAILURE); } std::string arg = argv[1]; std::string res = request(arg); std::cout << scrape(res) << std::endl; return EXIT_SUCCESS; }
Вы должны включить библиотеку iostream C++, чтобы гарантировать, что примитивы ввода/вывода (IO), определенные в функции main, работают ожидаемым образом:
#include “iostream”
Чтобы запустить ваш скрапер, скомпилируйте его с помощью g++. Введите в консоль следующий текст, чтобы скомпилировать и запустить ваш скрапер. Он должен извлечь шесть перечисленных определений слова «эзотерика»:
$ g++ scraper.cc -lcurl -lgumbo -std=c++11 -o scraper $ ./scraper esoteric
Вы должны увидеть следующее:
Заключение
Как вы увидели в этой статье, C++, который обычно используется для системного программирования, также хорошо подходит для веб-скрапинга благодаря своей способности анализировать HTTP. Эта дополнительная функциональность может помочь расширить ваши знания в C++.
Обратите внимание, что этот пример был относительно простым и не рассматривал, как будет работать скрапинг для сайта с более сложной JavaScript-кодом, например, использующего Selenium. Чтобы выполнять скрапинг на сайте с более динамической отрисовкой, можно использовать безголовый браузер с библиотекой C++ для Selenium. Эта тема будет рассмотрена в будущей статье.
Чтобы проверить свою работу по этой статье, обратитесь к GitHub gist.
Ранее мы уже писали Что такое веб-скрапинг: Руководство для начинающих