使用 Javascript 和 Node.js 进行网页抓取
JavaScript 和 Web 数据抓取技术正蓬勃发展。我们将结合两者,使用 Node.js 中的 JavaScript 从零开始构建一个简单的抓取工具和爬虫。
避免阻塞是网站抓取的关键环节,因此我们还将添加一些功能来辅助解决这一问题。最后,借助Node 的事件循环,我们可以将任务并行化,从而提高执行速度。
先决条件
要使代码正常运行,您需要安装Node(或nvm)和 npm。某些系统已预装。之后,运行 来安装所有必需的库npm install
。
npm install axios cheerio playwright
介绍
我们使用的是 Node v12,但您可以随时检查每个功能的兼容性。
Axios是一个“基于 Promise 的 HTTP 客户端”,我们将使用它从 URL 获取 HTML。它支持多种选项,例如标头和代理,我们将在后面介绍。如果您使用 TypeScript,它们“包含 TypeScript 定义和用于 Axios 错误的类型保护”。
Cheerio是“快速、灵活且精简的 jQuery 核心实现”。它允许我们使用选择器查找节点、获取文本或属性,以及许多其他功能。我们将 HTML 传递给 cheerio,然后像在浏览器环境中一样进行查询。
Playwright “是一个 Node.js 库,通过单一 API 实现 Chromium、Firefox 和 WebKit 的自动化。”当 Axios 不够用时,我们将使用无头浏览器获取 HTML 来执行 Javascript 并等待异步内容加载。
抓取基础知识
我们首先需要的是 HTML。为此,我们安装了 Axios,它的使用非常简单。我们将以scrapeme.live为例,这是一个准备用于抓取的虚拟网站。
const axios = require('axios'); | |
axios.get('https://scrapeme.live/shop/') | |
.then(({ data }) => console.log(data)); |
太棒了!然后,使用 cheerio,我们可以查询我们现在需要的两个东西:分页器链接和产品。为了了解如何操作,我们将打开 Chrome DevTools 查看页面。所有现代浏览器都提供类似的开发者工具。选择你最喜欢的。
我们用红色标记了感兴趣的部分,但您可以自行尝试。在这种情况下,所有CSS 选择器都很简单,无需嵌套。如果您需要其他结果或无法选择,请查看指南。您也可以使用 DevTools 获取选择器。
在“元素”选项卡上,右键单击节点 ➡ 复制 ➡ 复制选择器。
但结果通常与 HTML 高度耦合,例如本例:#main > div:nth-child(2) > nav > ul > li:nth-child(2) > a
。这种方法将来可能会出现问题,因为它会在任何微小的更改后停止工作。此外,它只会捕获其中一个分页链接,而不是全部。
我们可以捕获页面上的所有链接,然后按内容进行过滤。如果我们要编写一个全站爬虫,这将是正确的方法。在我们的例子中,我们只需要分页链接。使用提供的类 ,.page-numbers a
将捕获所有链接,然后href
从中提取 URL。选择器将匹配所有祖先包含 类 的链接节点page-numbers
。
对于产品(本例中为 Pokémon),我们将获取 id、name 和 price。查看下图了解选择器的详细信息,或者自行尝试。目前我们只会记录内容。请查看最终代码,了解如何将它们添加到数组中。
如上所示,所有产品都包含类product
,这简化了我们的工作。并且每个产品的h2
标签和price
节点都包含我们想要的内容。至于产品 ID,我们需要匹配属性,而不是类或节点类型。这可以使用语法来完成node[attribute="value"]
。我们只查找具有属性的节点,因此无需将其与任何特定值匹配。
如上所示,没有错误处理。为了简洁起见,我们将在代码片段中省略它,但在实际应用中会考虑到它。大多数情况下,返回默认值(例如空数组)就可以了。
关注链接
现在我们有了一些分页链接,我们也应该访问它们。如果你运行整个代码,你会看到它们出现了两次——有两个分页栏。
我们将添加两个集合来跟踪已访问的内容和新发现的链接。我们使用集合而不是数组来避免处理重复项,但两种方法都可以。为了避免爬取过多的内容,我们还将设置最大值。
在下一部分中,我们将使用async/await来避免回调和嵌套。异步函数是将基于 Promise 的函数编写成链式函数的另一种选择。在这种情况下,Axios 调用将保持异步。每个页面可能需要大约 1 秒,但我们按顺序编写代码,无需回调。
这里有一个小问题:await is only valid in async function
。这会强制我们将初始代码包装在一个函数中,具体来说,就是一个IIFE(立即调用函数表达式)。语法有点奇怪。它创建一个函数,然后立即调用它。
避免阻塞
如前所述,我们需要一些机制来规避拦截、验证码、登录墙以及其他一些防御技术。想要 100% 地阻止这些攻击非常复杂。但我们可以通过简单的努力获得较高的成功率。我们将采用两种策略:添加代理和设置完整的标头。
虽然我们不推荐使用免费代理,但它们确实存在。它们或许可以用来测试,但并不可靠。我们可以使用其中一些进行测试,具体示例如下。
请注意,这些免费代理可能不适合您使用。它们的有效期很短。
另一方面,付费代理服务提供 IP 轮换功能。这意味着我们的服务运作方式相同,但目标网站会收到不同的 IP 地址。在某些情况下,它们会针对每个请求或每隔几分钟轮换一次 IP 地址。无论如何,付费代理服务更难被封禁。而且,一旦轮换成功,我们很快就会获得一个新的 IP 地址。
我们将使用httpbin进行测试。它提供了多个端点,可以响应标头、IP 地址等信息。
下一步是检查我们的请求标头。最广为人知的是User-Agent(简称 UA),但还有很多其他标头。许多软件工具都有自己的标头,例如 Axios ( axios/0.21.1
)。通常,最好将实际标头与 UA 一起发送。这意味着我们需要一组实际的标头,因为并非所有浏览器和版本都使用相同的标头。我们在代码片段中包含了两个标头:Linux 机器上的 Chrome 92 和 Firefox 90。
无头浏览器
到目前为止,每个页面的访问都是使用 完成的axios.get
,这在某些情况下可能不够充分。假设我们需要 JavaScript 来加载、执行或以任何方式与浏览器交互(通过鼠标或键盘)。虽然出于性能考虑,避免使用 JavaScript 是更好的选择,但有时别无选择。Selenium 、 Puppeteer和Playwright是最常用和最知名的库。下面的代码片段仅显示了 User-Agent,但由于它是一个真实的浏览器,因此标头将包含所有信息(Accept、Accept-Encoding 等)。
这种方法本身也存在问题:看看 User-Agents 就知道了。Chromium 的 User-Agents 包含“HeadlessChrome”,它会告诉目标网站,这是一个无头浏览器。他们可能会据此采取行动。
与 Axios 一样,我们可以提供额外的标头、代理以及许多其他选项来自定义每个请求。这无疑是隐藏“HeadlessChrome”用户代理的绝佳选择。由于这是一个真实的浏览器,我们可以拦截请求、阻止其他请求(例如 CSS 文件或图片)、截取屏幕截图或视频等等。
现在,我们可以将获取 HTML 的代码拆分成几个函数,一个使用 Playwright,另一个使用 Axios。接下来,我们需要找到一种方法来选择最适合当前情况的函数。目前,它是硬编码的。顺便说一下,使用 Axios 时,输出结果相同,但速度更快。
使用 JavaScript 的 Async
我们已经在顺序爬取多个链接时引入了 async/await。如果我们要并行爬取它们,只需删除await
就够了,对吧?嗯……还没那么快。
该函数会调用第一个函数crawl
,并立即从集合中取出下一个项目toVisit
。问题在于,由于第一个页面的抓取尚未完成,因此集合为空。因此,我们没有向列表中添加任何新链接。该函数仍在后台运行,但我们已经退出了主函数。
为了正确执行此操作,我们需要创建一个队列,该队列将在可用任务时执行。为了避免同时处理大量请求,我们将限制其并发性。
如果你运行上面的代码,它会几乎立即打印出从 0 到 3 的数字(带有时间戳),并在 2 秒后打印从 4 到 7 的数字。这可能是最难理解的代码片段——请慢慢回顾。
我们在第 1-20 行中定义queue
。它将返回一个带有函数的对象,enqueue
用于将任务添加到列表中。然后,它检查我们是否超出了并发限制。如果没有超出,它将加一并running
进入一个循环,该循环获取一个任务并使用提供的参数运行它。直到任务列表为空,然后从中减一running
。这个变量标记了我们何时可以或不可以执行更多任务,只允许低于并发限制的任务。在第 23-28 行中,有辅助函数sleep
和printer
。在第 30 行实例化队列,并在第 32-34 行将项目入队(将开始运行 4)。
现在我们必须使用队列而不是 for 循环来并发运行多个页面。以下代码是部分更改部分。
请记住,Node 运行在单线程中,因此我们可以利用它的事件循环,但无法使用多个 CPU/线程。我们看到的情况运行良好,因为线程大部分时间处于空闲状态——网络请求不会消耗 CPU 时间。
为了进一步构建它,我们需要使用一些存储(数据库)或分布式队列系统。目前,我们依赖于变量,这些变量在 Node 的线程之间不共享。这并不太复杂,但我们在这篇博文中已经讲解得足够多了。
最终代码
结论
我们希望您分享以下四个要点:
- 了解网站解析和抓取的基础知识。
- 分离职责并在必要时使用抽象。
- 应用所需的技术来避免阻塞。
- 能够弄清楚以下步骤以扩大规模。
我们可以用 JavaScript 和 Node.js 构建一个自定义网页爬虫,并运用我们之前提到的组件。它可能无法扩展到数千个网站,但对于少数几个网站来说,它运行起来非常流畅。而且,转向分布式爬虫也并非遥不可及。
如果您喜欢它,您可能会对Python Web Scraping 指南感兴趣。
感谢阅读!你觉得内容有用吗?请传播并分享。👈
文章来源:https://dev.to/anderrv/web-scraping-with-javascript-and-node-js-2d