nodejs抓取动态网页( 如何在NodeJS的活跃生态系统帮助下高效地抓取Web内容)
优采云 发布时间: 2022-02-06 01:15nodejs抓取动态网页(
如何在NodeJS的活跃生态系统帮助下高效地抓取Web内容)
作者 | Shenesh Perera 翻译 | 王强策划| 李俊辰 Javascript 多年来发展迅速,引入了一个叫做 NodeJS 的运行时,因此它已经成为最流行和使用最广泛的语言之一。无论您是在编写 Web 应用程序还是移动应用程序,您都可以在 Javascript 生态系统中找到合适的工具。本文是关于如何借助 NodeJS 的活跃生态系统有效地抓取 Web 内容,以满足大多数相关需求。
本文首发于网站,经网站授权由InfoQ中文站翻译分享。
前提
本文章主要针对有一定Javascript开发经验的开发者。但是,如果您熟悉 Web 内容抓取,那么您可以从本文中学到很多东西,而无需任何 Javascript 经验。
成就
阅读此 文章 可以帮助读者:
了解 NodeJS:简介
Javascript 是一种简单而现代的语言,最初是为了向 网站 浏览器访问添加动态行为而创建的。网站加载后,Javascript 通过浏览器的 JS 引擎运行并翻译成一堆计算机可以理解的代码。为了让 Javascript 与您的浏览器交互,后者提供了一个运行时环境(文档、窗口等)。
换句话说,Javascript 编程语言不能直接与计算机或它们的资源交互,或操纵它们。例如,在 Web 服务器中,服务器必须能够与文件系统交互才能读取文件或将记录存储在数据库中。
NodeJS 的想法是让 Javascript 不仅可以在客户端运行,还可以在服务器端运行。为此,资深开发人员 Ryan Dahl 将 Google Chrome 的 v8 JS 引擎嵌入到名为 Node.js 的 C++ 程序中。所以 NodeJS 是一个运行时环境,它允许用 Javascript 编写的应用程序也可以在服务器上运行。
大多数语言(例如 C 或 C++)使用多个线程来处理并发,相比之下 NodeJS 只使用单个主线程,并借助 Event Loop 以非阻塞的方式使用它来执行任务。我们可以轻松地设置一个简单的 Web 服务器,如下所示:
1const http = require('http');
2const PORT = 3000;
3const server = http.createServer((req, res) => {
4 res.statusCode = 200;
5 res.setHeader('Content-Type', 'text/plain');
6 res.end('Hello World');
7});
8server.listen(port, () => {
9 console.log(`Server running at PORT:${port}/`);
10});
11
如果您安装了 NodeJS,请运行 node.js(删除符号),然后打开浏览器并导航到 localhost:3000 以查看文本“HelloWorld”。NodeJS 非常适合 I/O 密集型应用程序。HTTP 客户端:查询 Web
HTTP 客户端是一种向服务器发送请求,然后从服务器接收响应的工具。本文中讨论的大多数工具在后台使用 HTTP 客户端来查询您将尝试抓取的 网站 服务器。
RequestRequest 是 Javascript 生态系统中使用最广泛的 HTTP 客户端之一,但是现在 Request 库的作者已经正式表示不建议大家继续使用它。不是说不能用,还有很多库还在用,真的很好用。使用 Request 发出 HTTP 请求非常简单:
1const request = require('request')
2request('https://www.reddit.com/r/programming.json', function (
3 error,
4 response,
5 body) {
6 console.error('error:', error)
7 console.log('body:', body)
8})
9
你可以在 Github 上找到 Request 库并运行 npm install request 来安装它。在这里您可以参考弃用通知和详细信息:
AxiosAxios 是一个基于 Promise 的 HTTP 客户端,在浏览器和 NodeJS 中运行。如果你使用 Typescript,axios 可以覆盖内置类型。通过axios发起HTTP请求非常简单。默认情况下它具有内置的 Promise 支持,不像 Request,它必须使用回调:
1const axios = require('axios')
2axios
3 .get('https://www.reddit.com/r/programming.json')
4 .then((response) => {
5 console.log(response)
6 })
7 .catch((error) => {
8 console.error(error)
9 });
10
1如果你喜欢 Promises API 的 async/await 语法糖,那么也可以用它们,但由于顶级的 await 仍处于第 3 阶段,我们只能用 Async Function 来代替:
2
1async function getForum() {
2 try {
3 const response = await axios.get(
4 'https://www.reddit.com/r/programming.json'
5 )
6 console.log(response)
7 } catch (error) {
8 console.error(error)
9 }
10}
11
你只需调用getForum!你可以在 Github 上找到 Axios 库并运行 npm install axios 来安装它。
超级代理
与 Axios 类似,Superagent 是另一个强大的 HTTP 客户端,支持 Promises 和 async/await 语法糖。它的 API 和 Axios 一样简单,但 Superagent 依赖较多,不太流行。
在 Superagent 中,HTTP 请求是使用 Promise、async/await 或回调发出的,如下所示:
1const superagent = require("superagent")
2const forumURL = "https://www.reddit.com/r/programming.json"
3// callbacks
4superagent
5 .get(forumURL)
6 .end((error, response) => {
7 console.log(response)
8 })
9// promises
10superagent
11 .get(forumURL)
12 .then((response) => {
13 console.log(response)
14 })
15 .catch((error) => {
16 console.error(error)
17 })
18// promises with async/await
19async function getForum() {
20 try {
21 const response = await superagent.get(forumURL)
22 console.log(response)
23 } catch (error) {
24 console.error(error)
25 }
26
你可以在 Github 上找到 Superagent 库并运行 npm install superagent 来安装它。
对于下面介绍的网页抓取工具,本文将使用 Axios 作为 HTTP 客户端。正则表达式:艰难之路
在没有任何依赖关系的情况下开始抓取 Web 内容的最简单方法是在使用 HTTP 客户端查询网页时收到的 HTML 字符串上应用一组正则表达式 - 但这种方法绕路太远了。正则表达式不是那么灵活,许多专业人士和爱好者很难编写正确的正则表达式。
对于复杂的网页抓取任务,正则表达式很快就会成为瓶颈。无论如何,让我们先尝试一下。假设有一个带有用户名的标签,我们需要用户名在其中,那么使用正则表达式时的方法几乎是这样的:
1const htmlString = 'Username: John Doe'
2const result = htmlString.match(/(.+)/)
3console.log(result[1], result[1].split(": ")[1])
4// Username: John Doe, John Doe
5
在 Javascript 中,match() 通常返回一个收录与正则表达式匹配的所有内容的数组。第二个元素(在索引 1 处)将找到标签的 textContent 或 innerHTML,这正是我们想要的。但是这个结果将收录一些我们不需要的文本(“用户名:”),必须将其删除。如您所见,这种方法对于一个非常简单的用例来说很麻烦。所以我们应该使用 HTML 解析器之类的工具,这些工具将在后面讨论。Cheerio:在其核心遍历 DOM JQuery Cheerio 是一个高效且轻量级的库,允许您在服务器端使用 JQuery 丰富而强大的 API。如果您以前使用过 JQuery,那么使用 Cheerio 很容易上手。它消除了 DOM 的所有不一致和与浏览器相关的特性,并公开了一个用于解析和操作 DOM 的高效 API。
1const cheerio = require('cheerio')
2const $ = cheerio.load('
3
你好世界
')
$('h2.title').text('你好!')
$('h2').addClass('欢迎')
$.html()
//
你好呀!
如您所见,Cheerio 的工作方式与 JQuery 非常相似。但是,它与 Web 浏览器的工作方式不同,这意味着它不能:
因此,如果您尝试抓取的 网站 或 Web 应用程序有很多 Javascript 内容(例如“单页应用程序”),那么 Cheerio 不是您的最佳选择,您可能不得不依赖以下讨论一些其他选项。
为了展示 Cheerio 的强大功能,我们将尝试爬取 Reddit 中的 r/programming 论坛以获取帖子标题列表。
首先,运行以下命令安装 Cheerio 和 axios:npm install Cheerio axios。然后创建一个名为 crawler.js 的新文件并复制/粘贴以下代码:
1const axios = require('axios');
2const cheerio = require('cheerio');
3const getPostTitles = async () => {
4 try {
5 const { data } = await axios.get(
6 'https://old.reddit.com/r/programming/'
7 );
8 const $ = cheerio.load(data);
9 const postTitles = [];
10 $('div > p.title > a').each((_idx, el) => {
11 const postTitle = $(el).text()
12 postTitles.push(postTitle)
13 });
14 return postTitles;
15 } catch (error) {
16 throw error;
17 }
18};
19getPostTitles()
20.then((postTitles) => console.log(postTitles));
21
getPostTitles() 是一个异步函数,用于抓取旧 reddit 的 r/programming 论坛。首先,使用来自 axios HTTP 客户端库的简单 HTTP GET 请求获取 网站 的 HTML,然后使用cheerio.load() 函数将 html 数据提供给 Cheerio。
接下来,使用浏览器的开发工具,您可以获得通常可以针对所有明信片的选择器。如果您使用过 JQuery,那么 $('div > p.title > a') 非常熟悉。这将获取所有帖子,因为您只想获取每个帖子的标题,您必须遍历每个帖子(使用 each() 函数进行迭代)。
要从每个标题中提取文本,必须在 Cheerio 的帮助下获取 DOM 元素(当前元素的 el)。然后在每个元素上调用 text() 以获取文本。
现在,您可以弹出一个终端并运行 node crawler.js,您会看到一长串大约 25 或 26 个帖子标题。虽然这是一个非常简单的用例,但它显示了 Cheerio 提供的 API 是多么容易使用。
如果您的用例需要执行 Javascript 和加载外部资源,这里有几个选项可供考虑。JSDOM:节点的 DOM
JSDOM 是 NodeJS 中使用的文档对象模型 (DOM) 的纯 Javascript 实现。如前所述,DOM 不适用于 Node,而 JSDOM 是最接近的替代品。它或多或少地模拟了浏览器的机制。
一旦创建了 DOM,我们就可以通过编程方式与要抓取的 Web 应用程序或 网站 进行交互,还可以完成单击按钮之类的操作。如果您熟悉 DOM 的工作原理,那么 JSDOM 也非常易于使用。
1const { JSDOM } = require('jsdom')
2const { document } = new JSDOM(
3 '
4
你好世界
'
)。窗户
constheading = document.querySelector('.title')
heading.textContent = '你好!'
heading.classList.add('欢迎')
标题.innerHTML
//
你好呀!
如您所见,JSDOM 创建了一个 DOM,然后您可以使用与浏览器 DOM 相同的方法和属性对其进行操作。为了演示如何使用 JSDOM 与 网站 交互,我们将在 Redditr/programming 论坛上发表第一篇文章,点赞它,然后我们将验证该帖子是否已被点赞。首先运行以下命令安装jsdom和axios:
1npm install jsdom axios
2
1然后创建一个名为 rawler.js 的文件,并复制 / 粘贴以下代码:
2
1const { JSDOM } = require("jsdom")
2const axios = require('axios')
3const upvoteFirstPost = async () => {
4 try {
5 const { data } = await axios.get("https://old.reddit.com/r/programming/");
6 const dom = new JSDOM(data, {
7 runScripts: "dangerously",
8 resources: "usable"
9 });
10 const { document } = dom.window;
11 const firstPost = document.querySelector("div > div.midcol > div.arrow");
12 firstPost.click();
13 const isUpvoted = firstPost.classList.contains("upmod");
14 const msg = isUpvoted
15 ? "Post has been upvoted successfully!"
16 : "The post has not been upvoted!";
17 return msg;
18 } catch (error) {
19 throw error;
20 }
21};
22upvoteFirstPost().then(msg => console.log(msg));
23
upvoteFirstPost() 是一个异步函数,它将获得 r/programming 中的第一个帖子并对其进行投票。为此,axios 发送一个 HTTP GET 请求以获取指定 URL 的 HTML。然后将先前获取的 HTML 馈送到 JSDOM 以创建新的 DOM。JSDOM 构造函数将 HTML 作为第一个参数,将选项作为第二个参数。添加的 2 个选项执行以下功能: