如何使用C++ 实现网页数据抓取?
C++ 仍然是一种高效的语言。如果您必须解析大量页面或非常大的页面,C++ 网络抓取的性能可能会让您感到惊讶!在本分步教程中,您将学习如何使用 libcurl 和 libxml2 库在 C++ 中进行数据抓取。
C++ 适合网页抓取吗?
C++ 是网页抓取的可行选择,尤其是当资源使用很重要时。与此同时,大多数开发人员倾向于选择其他语言。这是因为它们更容易使用,拥有更大的社区,并且附带更多的库。
例如, Python 网页抓取因其广泛的软件包而成为流行的选择。JavaScript和 Node.js 也很常用。您可以在我们的文章中了解有关网络抓取的最佳编程语言的更多信息。
当性能至关重要时,使用 C++ 可以发挥重要作用,因为它的低级性质使其快速且高效。它是处理大规模网络抓取任务的非常合适的工具。
C++ 网页抓取库:先决条件
C++ 不是专为 Web 设计的语言,但存在一些很好的工具可以从 Internet 中提取数据。
要使用 C++ 构建网络抓取工具,您需要以下内容:
- libcurl:一个开源且易于使用的 HTTP 客户端,用于构建在 cURL 之上的 C 和 C++。
- libxml2:一个 HTML 和 XML 解析器,具有基于 XPath 的完整元素选择 API。
libcurl 将帮助您从网络上检索网页。然后,您可以解析它们的 HTML 内容并使用 libxml2 从中提取数据。
在了解如何安装它们之前,请在 IDE 中初始化一个 C++ 项目。
在 Windows 上,您可以依赖Visual Studio 和 C++ . 带有 C/C++ 扩展的 Visual Studio Code 可在 macOS 或 Linux 上运行。按照说明并根据本地编译器创建项目。
使用以下scraper.cpp
文件对其进行初始化:
#include <iostream> int main() { std::cout << "Hello, World!"; return 0; }
该main()
函数将包含抓取逻辑。
接下来,安装 C++ 包管理器vcpkg
并在 VS IDE 中进行设置,如官方指南中所述。
要安装 libcurl,请运行:
vcpkg install curl
然后,要安装 libxml2,请启动:
vcpkg install libxml2
libxml2 公开了许多功能,但您只需要这两个头文件中的函数:
HTMLparser.h
:HTML 解析器的接口。XPath.h
:用于执行 XPath 查询的 API。
第一个将帮助您解析 HTML 文档,第二个将从中选择所需的元素。
通过在文件顶部添加以下三行来导入两个库scraper.cpp
:
#include <curl/curl.h> #include "libxml/HTMLparser.h" #include "libxml/xpath.h"
如何用 C++ 进行网页抓取
要使用 C++ 进行网页抓取,您需要:
- 使用 libcurl 下载目标页面。
- 解析检索到的 HTML 文档并使用 libxml2 从中抓取数据。
- 将收集的数据导出到文件。
作为目标网站,我们将使用ScrapeMe,这是一个电子商务网站,其中包含受 Pokémon 启发的产品的分页列表:
您要构建的 C++ 蜘蛛将能够检索所有产品数据。
第 1 步:通过请求目标页面来抓取
使用 libcurl 发出请求涉及您希望避免每次重复的样板操作。将它们封装在一个可重用函数中,该函数初始化 cURL 实例并使用它对GET
作为参数传递的 URL 运行 HTTP 请求。然后,它将服务器返回的 HTML 文档作为字符串返回。
std::string get_request(std::string url) { // initialize curl locally CURL *curl = curl_easy_init(); std::string result; if (curl) { // perform the GET request curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, [](void *contents, size_t size, size_t nmemb, std::string *response) { ((std::string*) response)->append((char*) contents, size * nmemb); return size * nmemb; }); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result); curl_easy_perform(curl); // free up the local curl resources curl_easy_cleanup(curl); } return result; }
如果您对 libcurl 的工作原理有任何疑问,请查看文档中的官方教程。
main()
现在,在函数中使用它scraper.cpp
以字符串形式检索 HTML 内容:
#include <iostream> #include <curl/curl.h> #include "libxml/HTMLparser.h" #include "libxml/xpath.h" // std::string get_request(std::string url) { ... } int main() { // initialize curl globally curl_global_init(CURL_GLOBAL_ALL); // download the target HTML document // and print it std::string html_document = get_request("https://scrapeme.live/shop/"); std::cout << html_document; // scraping logic... // free up the global curl resources curl_global_cleanup(); return 0; }
该脚本打印如下:
<!doctype html> <html lang="en-GB"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2.0"> <link rel="profile" href="http://gmpg.org/xfn/11"> <link rel="pingback" href="https://scrapeme.live/xmlrpc.php"> <title>Products – ScrapeMe</title> <!-- Omitted for brevity... -->
这就是目标页面的 HTML 代码!
第 2 步:使用 C++ 解析所需的 HTML 数据
检索 HTML 文档后,将其提供给 libxml2:
htmlDocPtr doc = htmlReadMemory(html_document.c_str(), html_document.length(), nullptr, nullptr, HTML_PARSE_NOERROR);
htmlReadMemory()
解析 HTML 字符串并构建一个可以在其上应用 XPath 选择器的树。
检查目标站点以定义有效的选择策略。在浏览器中,右键单击产品 HTML 节点,然后选择“检查”选项。将打开以下 DevTools 部分:
分析 HTML 代码并注意,您可以li.product
使用下面的 XPath 选择器获取所有元素:
//li[contains(@class, 'product')]
使用它来检索所有 HTML 产品:
xmlXPathContextPtr context = xmlXPathNewContext(doc); xmlXPathObjectPtr product_html_elements = xmlXPathEvalExpression((xmlChar *) "//li[contains(@class, 'product')]", context);
xmlXPathNewContext()
将 XPath 上下文设置为整个文档。接下来,xmlXPathEvalExpression()
应用上面定义的选择器策略。
给定一个产品,需要抓取的有用信息是:
- 中的产品 URL
<a>
。 - 中的产品图片
<img>
。 - 中的产品名称
<h2>
。 - 产品价格在
<span>
.
为了存储这些数据,您需要定义一个新的数据结构:
struct PokemonProduct { std::string url; std::string image; std::string name; std::string price; };
在 C++ 中,astruct
是分组在同一名称下的不同数据字段的集合。
由于页面上有多种产品,因此您需要一系列PokemonProduct
:
std::vector<PokemonProduct> pokemon_products;
不要忘记在 C++ 中启用矢量功能:
#include <vector>
迭代产品节点列表并提取所需的数据:
for (int i = 0; i < product_html_elements->nodesetval->nodeNr; ++i) { // get the current element of the loop xmlNodePtr product_html_element = product_html_elements->nodesetval->nodeTab[i]; // set the context to restrict XPath selectors // to the children of the current element xmlXPathSetContextNode(product_html_element, context); xmlNodePtr url_html_element = xmlXPathEvalExpression((xmlChar *) ".//a", context)->nodesetval->nodeTab[0]; std::string url = std::string(reinterpret_cast<char *>(xmlGetProp(url_html_element, (xmlChar *) "href"))); xmlNodePtr image_html_element = xmlXPathEvalExpression((xmlChar *) ".//a/img", context)->nodesetval->nodeTab[0]; std::string image = std::string(reinterpret_cast<char *>(xmlGetProp(image_html_element, (xmlChar *) "src"))); xmlNodePtr name_html_element = xmlXPathEvalExpression((xmlChar *) ".//a/h2", context)->nodesetval->nodeTab[0]; std::string name = std::string(reinterpret_cast<char *>(xmlNodeGetContent(name_html_element))); xmlNodePtr price_html_element = xmlXPathEvalExpression((xmlChar *) ".//a/span", context)->nodesetval->nodeTab[0]; std::string price = std::string(reinterpret_cast<char *>(xmlNodeGetContent(price_html_element))); PokemonProduct pokemon_product = {url, image, name, price}; pokemon_products.push_back(pokemon_product); }
pokemon_products
将包含所有感兴趣的产品数据!
循环结束后,记得释放libxml2分配的资源:
// free up libxml2 resources xmlXPathFreeContext(context); xmlFreeDoc(doc);
现在,当我们能够提取所需的数据时,下一步就是获取输出。接下来我们将看到这一点以及最终的代码。
第 3 步:将数据导出到 CSV
剩下的就是将数据导出为更有用的格式,例如 CSV。您不需要额外的库。您只需打开.csv
文件,将PokemonProduct
结构转换为 CSV 记录,然后将其附加到文件中。
// create the CSV file of output std::ofstream csv_file("products.csv"); // populate it with the header csv_file << "url,image,name,price" << std::endl; // populate the CSV output file for (int i = 0; i < pokemon_products.size(); ++i) { // transform a PokemonProduct instance to a // CSV string record PokemonProduct p = pokemon_products.at(i); std::string csv_record = p.url + "," + p.image + "," + p.name + "," + p.price; csv_file << csv_record << std::endl; } // free up the resources for the CSV file csv_file.close();
将它们放在一起,您将获得抓取工具的最终代码:
#include <iostream> #include <curl/curl.h> #include "libxml/HTMLparser.h" #include "libxml/xpath.h" #include <vector> std::string get_request(std::string url) { // initialize curl locally CURL *curl = curl_easy_init(); std::string result; if (curl) { // perform the GET request curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, [](void *contents, size_t size, size_t nmemb, std::string *response) { ((std::string*) response)->append((char*) contents, size * nmemb); return size * nmemb; }); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result); curl_easy_perform(curl); // free up the local curl resources curl_easy_cleanup(curl); } return result; } // to store the scraped data of interest // for each product struct PokemonProduct { std::string url; std::string image; std::string name; std::string price; }; int main() { // initialize curl globally curl_global_init(CURL_GLOBAL_ALL); // retrieve the HTML content of the target page std::string html_document = get_request("https://scrapeme.live/shop/"); // parse the HTML document returned by the server htmlDocPtr doc = htmlReadMemory(html_document.c_str(), html_document.length(), nullptr, nullptr, HTML_PARSE_NOERROR); // initialize the XPath context for libxml2 // to the entire document xmlXPathContextPtr context = xmlXPathNewContext(doc); // get the product HTML elements xmlXPathObjectPtr product_html_elements = xmlXPathEvalExpression((xmlChar *) "//li[contains(@class, 'product')]", context); // to store the scraped products std::vector<PokemonProduct> pokemon_products; // iterate the list of product HTML elements for (int i = 0; i < product_html_elements->nodesetval->nodeNr; ++i) { // get the current element of the loop xmlNodePtr product_html_element = product_html_elements->nodesetval->nodeTab[i]; // set the context to restrict XPath selectors // to the children of the current element xmlXPathSetContextNode(product_html_element, context); xmlNodePtr url_html_element = xmlXPathEvalExpression((xmlChar *) ".//a", context)->nodesetval->nodeTab[0]; std::string url = std::string(reinterpret_cast<char *>(xmlGetProp(url_html_element, (xmlChar *) "href"))); xmlNodePtr image_html_element = xmlXPathEvalExpression((xmlChar *) ".//a/img", context)->nodesetval->nodeTab[0]; std::string image = std::string(reinterpret_cast<char *>(xmlGetProp(image_html_element, (xmlChar *) "src"))); xmlNodePtr name_html_element = xmlXPathEvalExpression((xmlChar *) ".//a/h2", context)->nodesetval->nodeTab[0]; std::string name = std::string(reinterpret_cast<char *>(xmlNodeGetContent(name_html_element))); xmlNodePtr price_html_element = xmlXPathEvalExpression((xmlChar *) ".//a/span", context)->nodesetval->nodeTab[0]; std::string price = std::string(reinterpret_cast<char *>(xmlNodeGetContent(price_html_element))); PokemonProduct pokemon_product = {url, image, name, price}; pokemon_products.push_back(pokemon_product); } // free up libxml2 resources xmlXPathFreeContext(context); xmlFreeDoc(doc); // create the CSV file of output std::ofstream csv_file("products.csv"); // populate it with the header csv_file << "url,image,name,price" << std::endl; // populate the CSV output file for (int i = 0; i < pokemon_products.size(); ++i) { // transform a PokemonProduct instance to a // CSV string record PokemonProduct p = pokemon_products.at(i); std::string csv_record = p.url + "," + p.image + "," + p.name + "," + p.price; csv_file << csv_record << std::endl; } // free up the resources for the CSV file csv_file.close(); // free up the global curl resources curl_global_cleanup(); return 0; }
运行网页抓取 C++ 脚本以生成products.csv
文件。打开它,您将看到以下输出:
使用 C++ 进行网页抓取
目标网站有几个产品页面,对吗?要完全抓取它,您必须发现并访问所有页面。这就是网络爬行的意义所在。
首先,您需要找到一种方法来查找所有分页页面。首先使用 DevTools 检查任何分页号 HTML 元素:
您可以通过以下方式选择它们:
//li/a[contains(@class, 'page-numbers')]
要实现网络爬行,步骤如下:
- 访问一个页面。
- 获取分页链接元素。
- 将新发现的 URL 添加到队列中。
- 使用新页面重复该循环。
为此,您需要一些支持数据结构以避免两次访问同一页面(详细信息如下):
#include <iostream> #include <curl/curl.h> #include "libxml/HTMLparser.h" #include "libxml/xpath.h" #include <vector> // std::string get_request(std::string url) { ... } // struct PokemonProduct { ... } int main() { // initialize curl globally curl_global_init(CURL_GLOBAL_ALL); std::vector<PokemonProduct> pokemon_products; // web page to start scraping from std::string first_page = "https://scrapeme.live/shop/page/1/"; // initialize the list of pages to scrape std::vector<std::string> pages_to_scrape = {first_page}; // initialize the list of pages discovered std::vector<std::string> pages_discovered = {first_page}; // current iteration int i = 1; // max number of iterations allowed int max_iterations = 5; // until there is still a page to scrape or // the limit gets hit while (!pages_to_scrape.empty() && i <= max_iterations) { // get the first page to scrape // and remove it from the list std::string page_to_scrape = pages_to_scrape.at(0); pages_to_scrape.erase(pages_to_scrape.begin()); std::string html_document = get_request(pages_to_scrape); htmlDocPtr doc = htmlReadMemory(html_document.c_str(), html_document.length(), nullptr, nullptr, HTML_PARSE_NOERROR); // scraping logic... // re-initialize the XPath context to // restore it to the entire document context = xmlXPathNewContext(doc); // extract the list of pagination links xmlXPathObjectPtr pagination_html_elements = xmlXPathEvalExpression((xmlChar *)"//a[@class='page-numbers']", context); // iterate over it to discover new links to scrape for (int i = 0; i < pagination_html_elements->nodesetval->nodeNr; ++i) { xmlNodePtr pagination_html_element = pagination_html_elements->nodesetval->nodeTab[i]; // extract the pagination URL xmlXPathSetContextNode(pagination_html_element, context); std::string pagination_link = std::string(reinterpret_cast<char *>(xmlGetProp(pagination_html_element, (xmlChar *) "href"))); // if the page discovered is new if (std::find(pages_discovered.begin(), pages_discovered.end(), pagination_link) == pages_discovered.end()) { // if the page discovered should be scraped pages_discovered.push_back(pagination_link); if (std::find(pages_to_scrape.begin(), pages_to_scrape.end(), pagination_link) == pages_to_scrape.end()) { pages_to_scrape.push_back(pagination_link); } } } // free up libxml2 resources xmlXPathFreeContext(context); xmlFreeDoc(doc); // increment the iteration counter i++; } // export logic... return 0; }
此 C++ 数据抓取脚本会抓取网页、抓取它并获取新的分页 URL。如果这些链接未知,则会将它们添加到抓取队列中。它重复此逻辑,直到队列为空或达到限制max_iterations
。
在周期结束时while
,pokemon_products
将包含在访问的页面中发现的所有产品。换句话说,您刚刚抓取了 ScrapeMe!恭喜!
C++ 中的无头浏览器抓取
有些网站依赖 JavaScript 进行渲染或数据检索。在这种情况下,您无法使用简单的 HTML 解析器从中提取数据,并且需要一个可以在浏览器中呈现网页的工具。它存在并且被称为无头浏览器。
Selenium 是最流行的无头浏览器之一,并且webdriverxx
是它的 C++ 绑定。
要安装它,请在项目的根文件夹中启动以下命令:
git clone https://github.com/durdyev/webdriverxx cd webdriverxx mkdir build cd build && cmake .. sudo make && sudo make install
如果您是 Windows 用户,请在适用于 Linux 的 Windows 子系统(WSL)中启动它们。
然后,下载Selenium Grid 服务器,安装、设置并运行它。这是 所要求的唯一先决条件webdriverxx
。
使用以下代码控制 Chrome 实例从 ScrapeMe 中提取数据。它是前面看到的 Web 抓取 C++ 逻辑的翻译。请注意,FindElements()
和FindElement()
方法允许您使用 来选择 HTML 元素webdriverxx
。
#include <iostream> #include <vector> #include <webdriverxx/webdriver.h> struct PokemonProduct { std::string url; std::string image; std::string name; std::string price; }; int main() { webdriverxx::WebDriver driver = Start(Chrome()); // visit the target page in the controlled browser driver.Navigate("https://scrapeme.live/shop/"); // perform an XPath query webdriverxx::WebElements product_html_elements = driver.FindElements(webdriverxx::By::XPath("//li[contains(@class, 'product')]")); // To store the scraped products std::vector<PokemonProduct> pokemon_products; // Iterate over the list of product HTML elements for (auto& product_html_element : product_html_elements) { std::string url = product_html_element.FindElement(webdriverxx::By::XPath(".//a")).GetAttribute("href"); std::string image = product_html_element.FindElement(webdriverxx::By::XPath(".//a/img")).GetAttribute("src"); std::string name = product_html_element.FindElement(webdriverxx::By::XPath(".//a/h2")).GetText(); std::string price = product_html_element.FindElement(webdriverxx::By::XPath(".//a/span")).GetText(); PokemonProduct pokemon_product = {url, image, name, price}; pokemon_products.push_back(pokemon_product); } // stop the Chrome driver driver.Stop(); // create the CSV file of output std::ofstream csv_file("products.csv"); // populate it with the header csv_file << "url,image,name,price" << std::endl; // populate the CSV output file for (int i = 0; i < pokemon_products.size(); ++i) { // transform a PokemonProduct instance to a // CSV string record PokemonProduct p = pokemon_products.at(i); std::string csv_record = p.url + "," + p.image + "," + p.name + "," + p.price; csv_file << csv_record << std::endl; } // free up the resources for the CSV file csv_file.close(); return 0; }
请记住,无头浏览器是一个强大的工具,可以在页面上执行任何人类交互。它还能够执行 HTTP 客户端和解析器只能梦想的操作。例如,您可以使用webdriverxx
以下方式截取当前视口的屏幕截图:
webdriverxx::WebDriver driver = Start(Chrome()); // visit the target page in the controlled browser driver.Navigate("https://scrapeme.live/shop/"); // take a screenshot in Base64 webdriverxx::string screenshot_data = driver.GetScreenshot(); // initialize the screenshot file std::ofstream screenshot_file("screenshot.png"=); csv_file << screenshot_data; csv_file.close();
这会产生这样的结果:
您现在知道如何使用 C++ 在动态内容网站上进行网页抓取。
C++ 中网页抓取的挑战
使用 C++ 进行网页抓取绝对高效,但并非完美无缺。许多网站依靠反机器人解决方案来保护其数据。您的请求可能会因为这些技术而被阻止。
这是一个巨大的问题,也是使用脚本从互联网获取数据时面临的最大挑战。当然,有一些解决方案。请查看我们关于如何在不被阻止的情况下进行网页抓取的深入指南。这些技术中的大多数都是变通方法和技巧,可能只能暂时有效。
结论
本分步教程解释了如何使用 C++ 执行网页抓取。您了解了基础知识并深入研究了更复杂的主题。您已成为 C++ 数据提取专家了!
现在你知道了:
- 为什么 C++ 非常适合高效抓取。
- C++ 中的抓取基础知识。
- 如何用 C++ 进行网页抓取。
- 如何使用 C++ 中的无头浏览器从 JavaScript 渲染的网站中提取数据。