Fetch API的引入改变了Javascript开发者进行HTTP调用的方式。这意味着开发者不再需要下载第三方软件包就能进行HTTP请求。虽然这对前端开发者来说是个好消息,因为fetch
只能在浏览器中使用,但后端开发者仍然不得不依赖不同的第三方包。直到node-fetch
的出现,它旨在提供与浏览器支持的相同的fetch API。在这篇文章中,我们将看看如何使用node-fetch
来帮助你爬取网页。
准备条件
要获得本文的全部好处,你应该有。
- 有一些编写ES6 Javascript的经验。
- 对承诺有正确的理解,对async/await有一些经验。
什么是Fetch API?
Fetch
是一个规范,旨在规范请求、响应以及两者之间的一切,该标准声明为fetching
(因此称为fetch
)。浏览器的fetch
API和node-fetch
是这个规范的实现。fetch
和它的前身XHR
之间最大和最重要的区别是,它是围绕承诺(Promises)建立的。这意味着开发者不必再担心XHR
所拥有的回调地狱、混乱的代码和极其冗长的API。
还有一些技术上的差异:例如,当一个请求以HTTP状态代码404
返回时,从fetch
调用中返回的承诺不会被拒绝。
node-fetch
将所有这些都带到了服务器端。这意味着开发人员不再需要学习不同的API,它们的各种术语,或在幕后如何获取
实际发生的HTTP请求从服务器端执行。这就像运行npm install node-fetch
和编写HTTP请求一样简单,与你在浏览器中的做法几乎一样。
用node-fetch
和cheerio
进行网页爬取
要开始工作,你必须首先安装cheerio
和node-fetch
。虽然node-fetch
允许我们获得任何页面的HTML,但由于其结果只是一堆文本,你需要一些工具来从中提取你需要的东西。cheerio
可以帮助你,它提供了一个非常直观的类似JQuery的
API,并允许你从node-fetch
收到的HTML中提取数据。
请确保你有一个package.json
,如果没有的话。
- 通过运行
npm init
生成一个 - 然后通过运行以下命令安装
cheerio
和node-fetch
:npm install cheerio node-fetch
为了这篇文章的目的,我们将爬取reddit
。
const fetch = require('node-fetch'); const getReddit = async () => { const response = await fetch('https://reddit.com/'); const body = await response.text(); console.log(body); // prints a chock full of HTML richness return body; };
fetch
有一个强制参数,即资源URL
。当fetch
被调用时,它返回一个承诺,一旦服务器响应了头信息,它就会解析为一个Response
对象。在这一点上,正文
还不可用。被返回的承诺会被解析,请求是否失败并不重要。承诺只会因为网络错误(如连接问题)而被拒绝,这意味着即使服务器响应500服务器错误,承诺也可以解析。
响应
类实现了Body
类,它是一个ReadableStream
,提供了一组方便的基于承诺的方法,旨在用于流消费。
Body.text()
是其中之一,由于Response
实现了Body
,Body
拥有的所有方法都可以被Response
实例使用。调用这些方法中的任何一个都会返回一个承诺,该承诺最终会解析为数据。
有了这些数据,在这种情况下,就是HTML文本,我们可以用cheerio
来创建一个DOM,然后查询它来提取你感兴趣的东西。例如,如果你想得到feed中所有帖子的列表,你可以得到帖子列表的选择器(使用浏览器的开发工具),然后像这样使用cheerio
。
const fetch = require('node-fetch'); const cheerio = require('cheerio'); const getReddit = async () => { // get html text from reddit const response = await fetch('https://reddit.com/'); // using await to ensure that the promise resolves const body = await response.text(); // parse the html text and extract titles const $ = cheerio.load(body); const titleList = []; // using CSS selector $('._eYtD2XCVieq6emjKBH3m').each((i, title) => { const titleNode = $(title); const titleText = titleNode.text(); titleList.push(titleText); }); console.log(titleList); }; getReddit();
cheerio.load(
)允许你将任何HTML文本解析为可查询的DOM。cheerio
提供了各种方法来从现在构建的DOM中提取组件,其中之一是each()
,这个方法允许你遍历一个节点的列表。我们怎么知道我们得到的是一个列表呢?我们要找的是Reddit主页上的帖子的标题列表,目前这样一个标题的类名是._eYtD2XCVieq6emjKBH3m
,但将来可能会改变。
通过使用each()
对列表进行迭代,你可以得到每个HTML元素,你可以将其再次送入cheerio
,它将允许你再次从每个标题中提取文本。
这个过程是相当直观的,可以在任何网站上进行,只要该网站没有反爬取机制来节制、限制或阻止你的爬取。虽然这可以解决,但这样做所需的努力和开发时间可能根本无法承受。在这种情况下,本指南可以帮助你解决这个问题。
在node-fetch
中使用选项参数
fetch
有一个强制参数和一个可选参数,那就是options
对象。选项
对象允许你自定义HTTP请求以满足你的需求,无论是发送cookie还是POST
请求(fetch默认为GET
请求),你都需要定义选项
参数。
你将利用的最常见的属性是。
method
-,请求的HTTP方法,它默认设置为GET
。headers
– 你想和请求一起传递的头信息。body
– 你的请求的主体,如果你是在做例如POST请求,你会使用body属性。
你可以在这里找到其他可用的属性来定制你的HTTP请求。现在把这一切放在一起,让我们发送一个带有一些cookies和一些查询参数的POST请求。
const fetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); (async () => { const url = new URL('https://some-url.com'); const params = { param: 'test'}; const queryParams = new URLSearchParams(params).toString(); url.search = queryParams; const fetchOptions = { method: 'POST', headers: { 'cookie': '', }, body: JSON.string({ hello: 'world' }), }; await fetch(url, fetchOptions); })();
使用URL
模块,可以非常容易地将查询参数附加到你想爬取的网站URL上。特别是URLSearchParams
类在这方面很有用。
要发送一个HTTPPOST
请求,你必须简单地将方法
属性设置为POST
。对于任何其他的HTTP请求方法,如PUT
或DELETE
,你也可以这么做。要在请求的同时发送任何cookies,你必须使用cookie
头。
以并行方式发出fetch请求
有时,你可能想同时对不同的URL进行多个不同的获取
调用。 一个接一个地做最终会导致糟糕的性能,从而使你的终端用户有很长的等待时间。
为了解决这个问题,你应该将你的代码并行化。发送一个HTTP请求所消耗的计算机资源非常少,它所花费的时间只是因为你的计算机在等待,闲置,等待服务器的响应。我们把这种任务称为 “io绑定”,而那些因为消耗大量计算能力而变得缓慢的任务则是 “CPU绑定”。
“io绑定 “的任务可以用promise有效地并行化。由于fetch
是基于承诺的,你可以利用Promise.all
来同时进行多个fetch调用,就像这样:
const newProductsPagePromise = fetch('https://some-website.com/new-products'); const recommendedProductsPagePromise = fetch('https://some-website.com/recommended-products'); // Returns a promise that resolves to a list of the results Promise.all([newProductsPagePromise, recommendedProductsPagePromise]); [文中代码源自Scrapingbee]
总 结
这样一来,你就掌握了网页爬取的node-fetch方法
。尽管fetch
对于简单的使用案例来说是很好的,但当你必须处理使用Javascript渲染大部分页面的单页应用程序时,它可能会变得有点困难。像同时爬取之类的挑战性任务应该由手工完成,因为node-fetch
只是一个HTTP请求客户端,与其他客户端一样。
使用node-fetch
的另一个好处是,它比使用无头浏览器要有效得多。即使是提交表单这样的简单任务,无头浏览器也很慢,而且会占用大量的服务器资源。