Maison> interface Web> js tutoriel> le corps du texte

怎样用 Node.js 高效地从 Web 爬取数据?

青灯夜游
Libérer: 2021-12-21 09:55:10
avant
3342 Les gens l'ont consulté

怎样用 Node.js 高效地从 Web 爬取数据?

相关推荐:《nodejs 教程

由于Javascript有了巨大的改进,并且引入了称为NodeJS的运行时,因此它已成为最流行和使用最广泛的语言之一。 无论是Web应用程序还是移动应用程序,Javascript现在都具有正确的工具。本文讲解怎样用 Node.js 高效地从 Web 爬取数据。

前提条件

本文主要针对具有一定 JavaScript 经验的程序员。如果你对 Web 抓取有深刻的了解,但对 JavaScript 并不熟悉,那么本文仍然能够对你有所帮助。

  • ✅ 会 JavaScript
  • ✅ 会用 DevTools 提取元素选择器
  • ✅ 会一些 ES6 (可选)

你将学到

通过本文你将学到:

  • 学到更多关于 Node.js 的东西
  • 用多个 HTTP 客户端来帮助 Web 抓取的过程
  • 利用多个经过实践考验过的库来爬取 Web

了解 Node.js

Javascript 是一种简单的现代编程语言,最初是为了向浏览器中的网页添加动态效果。当加载网站后,Javascript 代码由浏览器的 Javascript 引擎运行。为了使 Javascript 与你的浏览器进行交互,浏览器还提供了运行时环境(document、window等)。

这意味着 Javascript 不能直接与计算机资源交互或对其进行操作。例如在 Web 服务器中,服务器必须能够与文件系统进行交互,这样才能读写文件。

Node.js 使 Javascript 不仅能够运行在客户端,而且还可以运行在服务器端。为了做到这一点,其创始人 Ryan Dahl 选择了Google Chrome 浏览器的 v8 Javascript Engine,并将其嵌入到用 C++ 开发的 Node 程序中。所以 Node.js 是一个运行时环境,它允许 Javascript 代码也能在服务器上运行。

与其他语言(例如 C 或 C++)通过多个线程来处理并发性相反,Node.js 利用单个主线程并并在事件循环的帮助下以非阻塞方式执行任务。

要创建一个简单的 Web 服务器非常简单,如下所示:

const http = require('http'); const PORT = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); }); server.listen(port, () => { console.log(`Server running at PORT:${port}/`); });
Copier après la connexion

如果你已安装了 Node.js,可以试着运行上面的代码。 Node.js 非常适合 I/O 密集型程序。

HTTP 客户端:访问 Web

HTTP 客户端是能够将请求发送到服务器,然后接收服务器响应的工具。下面提到的所有工具底的层都是用 HTTP 客户端来访问你要抓取的网站。

Request

Request 是 Javascript 生态中使用最广泛的 HTTP 客户端之一,但是 Request 库的作者已正式声明弃用了。不过这并不意味着它不可用了,相当多的库仍在使用它,并且非常好用。用 Request 发出 HTTP 请求是非常简单的:

const request = require('request') request('https://www.reddit.com/r/programming.json', function ( error, response, body ) { console.error('error:', error) console.log('body:', body) })
Copier après la connexion

你可以在Github上找到 Request 库,安装它非常简单。你还可以在https://github.com/request/re...找到弃用通知及其含义。

Axios

Axios 是基于 promise 的 HTTP 客户端,可在浏览器和 Node.js 中运行。如果你用 Typescript,那么 axios 会为你覆盖内置类型。通过 Axios 发起 HTTP 请求非常简单,默认情况下它带有 Promise 支持,而不是在 Request 中去使用回调:

const axios = require('axios') axios .get('https://www.reddit.com/r/programming.json') .then((response) => { console.log(response) }) .catch((error) => { console.error(error) });
Copier après la connexion

如果你喜欢 Promises API 的 async/await 语法糖,那么你也可以用,但是由于顶级 await 仍处于stage 3,所以我们只好先用异步函数来代替:

async function getForum() { try { const response = await axios.get( 'https://www.reddit.com/r/programming.json' ) console.log(response) } catch (error) { console.error(error) } }
Copier après la connexion

你所要做的就是调用getForum!可以在https://github.com/axios/axios上找到Axios库。

Superagent

与 Axios 一样,Superagent 是另一个强大的 HTTP 客户端,它支持 Promise 和 async/await 语法糖。它具有像 Axios 这样相当简单的 API,但是 Superagent 由于存在更多的依赖关系并且不那么流行。

用 promise、async/await 或回调向 Superagent 发出HTTP请求看起来像这样:

const superagent = require("superagent") const forumURL = "https://www.reddit.com/r/programming.json" // callbacks superagent .get(forumURL) .end((error, response) => { console.log(response) }) // promises superagent .get(forumURL) .then((response) => { console.log(response) }) .catch((error) => { console.error(error) }) // promises with async/await async function getForum() { try { const response = await superagent.get(forumURL) console.log(response) } catch (error) { console.error(error) } }
Copier après la connexion

可以在https://github.com/visionmedi...找到 Superagent。

正则表达式:艰难的路

在没有任何依赖性的情况下,最简单的进行网络抓取的方法是,使用 HTTP 客户端查询网页时,在收到的 HTML 字符串上使用一堆正则表达式。正则表达式不那么灵活,而且很多专业人士和业余爱好者都难以编写正确的正则表达式。

让我们试一试,假设其中有一个带有用户名的标签,我们需要该用户名,这类似于你依赖正则表达式时必须执行的操作

const htmlString = '' const result = htmlString.match(/
Copier après la connexion

在 Javascript 中,match()通常返回一个数组,该数组包含与正则表达式匹配的所有内容。第二个元素(在索引1中)将找到我们想要的标记的textContentinnerHTML。但是结果中包含一些不需要的文本( “Username: “),必须将其删除。

如你所见,对于一个非常简单的用例,步骤和要做的工作都很多。这就是为什么应该依赖 HTML 解析器的原因,我们将在后面讨论。

Cheerio:用于遍历 DOM 的核心 JQuery

Cheerio 是一个高效轻便的库,它使你可以在服务器端使用 JQuery 的丰富而强大的 API。如果你以前用过 JQuery,那么将会对 Cheerio 感到很熟悉,它消除了 DOM 所有不一致和与浏览器相关的功能,并公开了一种有效的 API 来解析和操作 DOM。

const cheerio = require('cheerio') const $ = cheerio.load('

Hello world

') $('h2.title').text('Hello there!') $('h2').addClass('welcome') $.html() //

Hello there!

Copier après la connexion

如你所见,Cheerio 与 JQuery 用起来非常相似。

但是,尽管它的工作方式不同于网络浏览器,也就这意味着它不能:

  • 渲染任何解析的或操纵 DOM 元素
  • 应用 CSS 或加载外部资源
  • 执行 JavaScript

因此,如果你尝试爬取的网站或 Web 应用是严重依赖 Javascript 的(例如“单页应用”),那么 Cheerio 并不是最佳选择,你可能不得不依赖稍后讨论的其他选项。

为了展示 Cheerio 的强大功能,我们将尝试在 Reddit 中抓取r/programming论坛,尝试获取帖子名称列表。

首先,通过运行以下命令来安装 Cheerio 和 axios:npm install cheerio axios

然后创建一个名为crawler.js的新文件,并复制粘贴以下代码:

const axios = require('axios'); const cheerio = require('cheerio'); const getPostTitles = async () => { try { const { data } = await axios.get( 'https://old.reddit.com/r/programming/' ); const $ = cheerio.load(data); const postTitles = []; $('div > p.title > a').each((_idx, el) => { const postTitle = $(el).text() postTitles.push(postTitle) }); return postTitles; } catch (error) { throw error; } }; getPostTitles() .then((postTitles) => console.log(postTitles));
Copier après la connexion

getPostTitles()是一个异步函数,将对旧的 reddit 的 r/programming 论坛进行爬取。首先,用带有 axios HTTP 客户端库的简单 HTTP GET 请求获取网站的 HTML,然后用cheerio.load()函数将 html 数据输入到 Cheerio 中。

然后在浏览器的 Dev Tools 帮助下,可以获得可以定位所有列表项的选择器。如果你使用过 JQuery,则必须非常熟悉$('div> p.title> a')。这将得到所有帖子,因为你只希望单独获取每个帖子的标题,所以必须遍历每个帖子,这些操作是在each()函数的帮助下完成的。

要从每个标题中提取文本,必须在 Cheerio 的帮助下获取 DOM元素(el指代当前元素)。然后在每个元素上调用text()能够为你提供文本。

现在,打开终端并运行node crawler.js,然后你将看到大约存有标题的数组,它会很长。尽管这是一个非常简单的用例,但它展示了 Cheerio 提供的 API 的简单性质。

如果你的用例需要执行 Javascript 并加载外部源,那么以下几个选项将很有帮助。

JSDOM:Node 的 DOM

JSDOM 是在 Node.js 中使用的文档对象模型的纯 Javascript 实现,如前所述,DOM 对 Node 不可用,但是 JSDOM 是最接近的。它或多或少地模仿了浏览器。

由于创建了 DOM,所以可以通过编程与要爬取的 Web 应用或网站进行交互,也可以模拟单击按钮。如果你熟悉 DOM 操作,那么使用 JSDOM 将会非常简单。

const { JSDOM } = require('jsdom') const { document } = new JSDOM( '

Hello world

' ).window const heading = document.querySelector('.title') heading.textContent = 'Hello there!' heading.classList.add('welcome') heading.innerHTML //

Hello there!

Copier après la connexion

代码中用 JSDOM 创建一个 DOM,然后你可以用和操纵浏览器 DOM 相同的方法和属性来操纵该 DOM。

为了演示如何用 JSDOM 与网站进行交互,我们将获得 Reddit r/programming 论坛的第一篇帖子并对其进行投票,然后验证该帖子是否已被投票。

首先运行以下命令来安装 jsdom 和 axios:npm install jsdom axios

然后创建名为crawler.js的文件,并复制粘贴以下代码:

const { JSDOM } = require("jsdom") const axios = require('axios') const upvoteFirstPost = async () => { try { const { data } = await axios.get("https://old.reddit.com/r/programming/"); const dom = new JSDOM(data, { runScripts: "dangerously", resources: "usable" }); const { document } = dom.window; const firstPost = document.querySelector("div > div.midcol > div.arrow"); firstPost.click(); const isUpvoted = firstPost.classList.contains("upmod"); const msg = isUpvoted ? "Post has been upvoted successfully!" : "The post has not been upvoted!"; return msg; } catch (error) { throw error; } }; upvoteFirstPost().then(msg => console.log(msg));
Copier après la connexion

upvoteFirstPost()是一个异步函数,它将在 r/programming 中获取第一个帖子,然后对其进行投票。axios 发送 HTTP GET 请求获取指定 URL 的HTML。然后通过先前获取的 HTML 来创建新的 DOM。 JSDOM 构造函数把HTML 作为第一个参数,把 option 作为第二个参数,已添加的 2 个 option 项执行以下功能:

  • runScripts:设置为dangerously时允许执行事件 handler 和任何 Javascript 代码。如果你不清楚将要运行的脚本的安全性,则最好将 runScripts 设置为“outside-only”,这会把所有提供的 Javascript 规范附加到 “window” 对象,从而阻止在inside上执行的任何脚本。
  • resources:设置为“usable”时,允许加载用