返回博客
指南
Raluca PenciucLast updated on Mar 31, 20262 min read

《C++ 网络爬虫终极指南》

《C++ 网络爬虫终极指南》

两百万年前,一群穴居人发现,把石头绑在棍子上或许挺管用。从那时起,事情就稍微有点失控了。如今,我们面临的问题已不再是逃离剑齿虎,而是当队友把提交命名为“did sutff”时该怎么办。

尽管困难的性质发生了变化,但我们对创造工具以克服困难的执着却从未改变。网络爬虫便是明证。它们是专为解决数字问题而设计的数字工具。

今天我们将从零开始构建一个新工具,不过这次不用石头和木棍,而是使用C++——这恐怕更难。所以,请继续关注,学习如何制作C++网页爬虫以及如何使用它!

理解网页抓取

无论你选择哪种编程语言,都必须理解网页抓取工具的工作原理。这一知识对于编写脚本并最终获得一个可运行的工具至关重要。

网络爬取的优势

不难想象,让机器人代劳所有调研工作,远比手动复制信息高效得多。若需处理海量数据,其优势将呈指数级增长。例如,在训练机器学习算法时,网络爬虫能为你节省数月的工作时间。

为了更清晰地说明,让我们逐一梳理网页爬虫能为您带来的帮助:

  • 减少繁琐工作的耗时——这是最显而易见的好处。爬虫提取数据的速度,与浏览器加载页面所需的时间相当。只需向机器人提供一组URL,您就能立即获取其内容。
  • 提高数据准确性——人工操作难免会出现人为失误。相比之下,机器人总能完整提取所有信息,完全按照页面呈现的形式,并根据您的指令进行解析。
  • 保持数据库井井有条——随着数据量的增加,管理起来会越来越困难。爬虫不仅能获取内容,还能将其存储在最适合您的格式中,让您的工作更轻松。
  • 使用最新信息——在许多情况下,数据会日新月异。例如,看看股市或网店商品列表就知道了。只需几行代码,您的机器人就能以固定间隔抓取相同页面,并用最新信息更新您的数据库。

WebScrapingAPI 的存在正是为了为您带来所有这些优势,同时帮助您绕过爬虫在网络上经常遇到的各种障碍。这一切听起来很棒,但如何将其应用到实际项目中呢?

网络爬虫的应用场景

网络爬虫的应用方式多种多样,可能性无穷无尽!不过,某些用例确实比其他更常见。

让我们详细探讨每种情况:

  • 搜索引擎优化 — 通过抓取包含您关键词的搜索结果页面,可以快速轻松地确定您需要在排名上超越哪些对手,以及什么类型的内容能助您实现这一目标。
  • 市场调研 — 在了解竞争对手的搜索表现后,您可以进一步分析其整体数字足迹。利用网络爬虫观察他们如何运用PPC广告、如何描述产品、采用何种定价模式等。基本上,任何公开信息都触手可及。
  • 品牌保护 —— 外界正在讨论您的品牌。理想情况下,这些都是正面评价,但这并非绝对。机器人能够高效爬取评论网站和社交媒体平台,并在您的品牌被提及时向您报告。无论人们如何评价,您都能随时维护品牌利益。
  • 潜在客户开发 —— 在互联网上架起销售团队与潜在客户之间的桥梁绝非易事。不过,一旦您使用网络爬虫机器人从《黄页》或领英等在线商业名录中收集联系信息,这一过程就会变得轻松许多。
  • 内容营销——毫无疑问,创作高质量内容需要大量工作。您不仅需要寻找可信的来源和海量信息,还需摸索受众喜爱的语气和表达方式。幸运的是,若有一款机器人代劳繁重的工作,这些步骤便变得轻而易举。

类似的例子不胜枚举。当然,您也可以开发出全新的数据提取应用方式,从而为己所用。我们始终鼓励用户对我们的API进行创新性的运用。

网络爬虫面临的挑战

尽管网络爬虫价值非凡,但并非一切都一帆风顺。有些网站并不欢迎机器人访问。虽然编程让爬虫避免给目标网站造成严重负担并不难,但网站对机器人的警惕也是有道理的。毕竟,网站可能无法区分一个彬彬有礼的机器人和一个恶意的机器人。

因此,爬虫在实际运行中会遇到一些挑战。

其中最棘手的障碍包括:

  • IP封禁 — 判断访问者是人类还是机器有多种方法。但一旦识别出机器人,后续的应对措施就简单得多——封禁IP。虽然只要采取必要的预防措施,这种情况通常不会发生,但准备一套代理池仍是明智之举,这样即使某个IP被封禁,你仍能继续提取数据。
  • 浏览器指纹识别 —— 您发送给网站的每次请求都会泄露部分信息。IP地址是最常见的例子,但请求头也会向网站透露您的操作系统、浏览器及其他细微信息。如果网站利用这些信息识别请求来源,并发现您的浏览速度远超人类正常水平,您很可能会被立即封禁。
  • JavaScript 渲染 — 大多数现代网站都是动态的。也就是说,它们使用 JavaScript 代码为用户定制页面,提供更个性化的体验。问题在于,简单的机器人会卡在这些代码上,因为它们无法执行这些代码。结果是:爬虫获取的是 JavaScript 代码,而不是包含相关信息的预期 HTML 内容。
  • 验证码 — 当网站无法确定访问者是人类还是机器人时,会将其重定向至验证码页面。对我们而言,这顶多是些许烦扰,但对机器人来说,这通常意味着彻底受阻;除非它们内置了解决验证码的脚本。
  • 请求限流 —— 防止访客压垮网站服务器的最简单方法,就是限制单个IP在固定时间内可发送的请求数量。一旦达到该数量,爬虫可能需要等待计时器倒计时结束,或者必须解决验证码。无论哪种情况,这对您的数据提取项目都无益。

理解网络

若要构建网络爬虫,您应了解互联网运作的几个关键概念。毕竟,您的机器人不仅要利用互联网收集数据,还必须融入其他数十亿用户之中。

我们常将网站比作书籍,认为“阅读某一页”仅需翻到相应页码即可。但实际上,这更像是你与书籍之间往来的电报。你请求一页内容,它便被发送给你;接着你请求下一页,同样的过程再次发生。

这一切的往来都通过 HTTP(超文本传输协议)实现。作为访问者的你,要查看一个页面,你的浏览器会向服务器发送一个 HTTP 请求,并收到来自服务器的 HTTP 响应。你想要的内容将包含在该响应中。

HTTP请求由四个组成部分构成:

  • URL(统一资源定位符)是你发送请求的目标地址。它指向一个唯一的资源,可以是整个网页、单个文件,或是介于两者之间的任何内容。
  • 方法(Method)传达了您希望执行的操作类型。最常见的方法是 GET,用于从服务器检索数据。以下是常见方法的列表。
  • 头部(Headers)是构成请求框架的元数据片段。例如,若需提供密码或凭证来访问数据,应将其放入相应的专用头部;若希望以 JSON 格式接收数据,则需在头部中注明。User-Agent 是网页抓取中最广为人知的头部之一,因为网站常利用它来识别访问者。
  • 请求主体是请求中用于存储数据的核心部分。若您正在获取内容,此处通常为空;反之,当您需要向服务器发送信息时,数据将存放在此处。

发送请求后,即使请求未能获取任何数据,你仍会收到响应。其结构如下:

  • 状态码(Status Code)是一个三位数的代码,能让你一目了然地了解发生了什么。简而言之,以“2”开头的代码通常表示成功,而以“4”开头的则表示失败。
  • 头部(Headers)的功能与服务器端头部相同。例如,若收到特定类型的文件,头部会告知其格式。
  • 正文(Body)也会存在。如果您使用 GET 方法请求内容且请求成功,您将在这里找到数据。

REST API 采用 HTTP 协议,这与网页浏览器类似。因此,如果您认为 C++ 网页爬虫过于繁琐,而选择使用 WebScrapingAPI(该工具已处理所有数据提取难题),这些信息对您同样大有裨益

了解 C++

在编程语言领域,C 和 C++ 堪称最经典的代表。这种语言诞生于 1985 年,是“带类的 C 语言”的完善版本。

虽然 C++ 适用于通用编程,但它并非开发网络爬虫时首先会考虑的语言。它虽有某些缺点,但这种思路并非毫无可取之处。让我们一起来探讨一下吧?

C++ 是一种面向对象的语言,这意味着它通过类、数据抽象和继承,使您的代码更易于复用并适应不同需求。由于数据被视为对象,您存储和解析数据会更加轻松。

许多开发者至少掌握一点 C++。即使你在大学里没学过,你(或你的队友)很可能也懂一点 C++。这种情况在整个软件开发社区都很普遍,因此你很容易就代码获得第二意见。

C++ 具有极高的可扩展性。如果您从小型项目起步,并决定将网络爬虫作为主要方向,大部分代码都是可复用的。只需在此处彼处稍作调整,您就能应对更大的数据量。

另一方面,C++ 是一种静态编程语言。虽然这能确保更好的数据完整性,但在处理互联网相关事务时,它不如动态语言灵活。

此外,C++ 并不适合构建爬虫。如果你仅需一个数据抓取工具,这可能不是问题。但如果你打算添加爬虫来生成 URL 列表,C++ 则不是一个理想的选择。

好,我们已经讨论了许多重要内容,现在让我们进入本文的核心——用 C++ 编写网页抓取工具。

使用 C++ 构建网页抓取工具

先决条件

  • C++ 集成开发环境(IDE)。在本指南中,我们将使用 Visual Studio
  • vcpkg 是由 Windows 开发并维护的 C/C++ 包管理器
  • cpr 是一个用于 HTTP 请求的 C/C++ 库,作为经典 cURL 的封装库构建,并受 Python requests 库的启发。
  • gumbo 是一个完全用 C 语言编写的 HTML 解析器,为多种编程语言(包括 C++)提供了封装库。

环境配置

1. 下载并安装 Visual Studio 后,使用“控制台应用程序”模板创建一个简单项目。

2. 接下来我们将配置包管理器。Vcpkg 提供了一份编写精良的教程,助您快速入门

注意:安装完成后,建议设置环境变量,以便您可以在计算机上的任何位置运行 vcpkg。

3. 现在是安装所需库的时候了。如果您已设置环境变量,请打开任意终端并运行以下命令:

> vcpkg install cpr
> vcpkg install gumbo
> vcpkg integrate install

若未设置环境变量,只需导航至安装 vcpkg 的文件夹,打开终端窗口并运行相同的命令。

前两个命令用于安装构建爬虫所需的包,第三个命令则能帮助我们轻松地将这些库集成到项目中。

选择一个网站并检查其 HTML

现在一切就绪!在构建网页抓取工具之前,我们需要选择一个网站并检查其 HTML 代码。

我们访问了维基百科,并从“你知道吗……”栏目中随机选取了一页。因此,今天要抓取的页面将是关于“罂粟籽防御”的维基百科条目,我们将从中提取部分内容。但首先,让我们看看页面的结构。在文章任意位置右键点击,然后选择“检查元素”,瞧!HTML 代码就呈现在眼前了。

提取标题

现在我们可以开始编写代码了。要提取信息,我们需要将 HTML 下载到本地。首先,导入我们刚刚下载的库:

#include <iostream>
#include <fstream>
#include "cpr/cpr.h"
#include "gumbo.h"

接着,我们向目标网站发起HTTP请求以获取HTML。

std::string extract_html_page()
{
    cpr::Url url = cpr::Url{"https://en.wikipedia.org/wiki/Poppy_seed_defence"};
    cpr::Response response = cpr::Get(url);
    return response.text;
}

int main()
{
    std::string page_content = extract_html_page();
}

现在 `page_content` 变量中存有文章的 HTML 内容,我们将进一步利用它来提取所需数据。此时 gumbo 库就派上用场了。

我们使用 gumbo_parse 方法将之前的 page_content 字符串转换为 HTML 树,然后对根节点调用我们实现的 search_for_title 函数。

int main()

{

    std::string page_content = extract_html_page();

    GumboOutput* parsed_response = gumbo_parse(page_content.c_str());

    search_for_title(parsed_response->root);

    // free the allocated memory

    gumbo_destroy_output(&kGumboDefaultOptions, parsed_response);

}

被调用的函数将通过递归调用,对 HTML 树进行深度遍历以查找 <h1> 标签。当找到标题时,它会在控制台显示该标题并退出执行。

void search_for_title(GumboNode* node)
{
    if (node->type != GUMBO_NODE_ELEMENT)
        return;

    if (node->v.element.tag == GUMBO_TAG_H1)
    {
        GumboNode* title_text = static_cast<GumboNode*>(node->v.element.children.data[0]);
        std::cout << title_text->v.text.text << "\n";
        return;
    }

    GumboVector* children = &node->v.element.children;
    for (unsigned int i = 0; i < children->length; i++)
        search_for_title(static_cast<GumboNode*>(children->data[i]));
}

提取链接

对于其余标签,同样遵循这一核心原则:遍历树结构并获取目标内容。现在让我们获取所有链接并提取其 href 属性。

void search_for_links(GumboNode* node)
{
    if (node->type != GUMBO_NODE_ELEMENT)
        return;

    if (node->v.element.tag == GUMBO_TAG_A)
    {
        GumboAttribute* href = gumbo_get_attribute(&node->v.element.attributes, "href");
        if (href)
            std::cout << href->value << "\n";
    }

    GumboVector* children = &node->v.element.children;
    for (unsigned int i = 0; i < children->length; i++)
    {
        search_for_links(static_cast<GumboNode*>(children->data[i]));
    }
}

看到了吗?除了要查找的标签不同外,代码几乎完全相同。gumbo_get_attribute 方法可以提取我们指定的任何属性。因此,您可以使用它来查找类名、ID 等。

在显示属性值之前,必须对其进行空值检查。在高级编程语言中,这通常是多余的,因为系统会自动显示空字符串;但在 C++ 中,程序会因此崩溃。

写入 CSV 文件

文中散布着许多链接,内容交织在一起。而且这篇文章本身也很短。所以让我们把它们全部保存到外部,看看如何区分它们。

首先,我们创建并打开一个 CSV 文件。这一步应在函数外部进行,紧邻导入语句处。由于我们的函数是递归的,如果每次调用都创建新文件,将会产生大量文件。

std::ofstream writeCsv("links.csv");

接着,在主函数中,我们在首次调用该函数之前写入 CSV 文件的第一行。执行完毕后,切记关闭文件。

writeCsv << "type,link" << "\n";
search_for_links(parsed_response->root);
writeCsv.close();

现在,我们来写内容。在 search_for_links 函数中,当发现 <a> 标签时,不再将其显示在控制台,而是这样做:

if (node->v.element.tag == GUMBO_TAG_A)
{
    GumboAttribute* href = gumbo_get_attribute(&node->v.element.attributes, "href");
    if (href)
    {
        std::string link = href->value;
        if (link.rfind("/wiki") == 0)
            writeCsv << "article," << link << "\n";
        else if (link.rfind("#cite") == 0)
            writeCsv << "cite," << link << "\n";
        else
            writeCsv << "other," << link << "\n";
    }
}

我们通过这段代码获取 href 属性的值,并将其分为三类:文章、引用和其他。

当然,你可以进一步扩展,定义自己的链接类型,例如那些看起来像文章但实际上是文件的链接。

网络爬虫的替代方案

现在你已经知道如何用 C++ 构建自己的网页抓取工具了。很棒,对吧?

不过,你可能曾想过“这简直是得不偿失!”,我们完全理解这种感受。说实话,编写爬虫程序虽是绝佳的学习体验,且适用于小规模数据集,但仅此而已。

若您需要一款快速、可靠且可扩展的数据提取工具,Web 爬取 API 便是最佳选择。因为它集成了您所需的所有功能,例如轮换代理池、JavaScript 渲染、验证码破解、地理定位选项等。

不相信吗?那就亲自体验一下吧!立即开始免费试用 WebScrapingAPI,亲身体验网页抓取是多么简单易行!

关于作者
Raluca Penciuc, 全栈开发工程师 @ WebScrapingAPI
Raluca Penciuc全栈开发工程师

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。