in

如何使用Node-Fetch进行网页爬取?

如何使用Node-Fetch进行网页爬取

Fetch API的引入改变了Javascript开发者进行HTTP调用的方式。这意味着开发者不再需要下载第三方软件包就能进行HTTP请求。虽然这对前端开发者来说是个好消息,因为fetch只能在浏览器中使用,但后端开发者仍然不得不依赖不同的第三方包。直到node-fetch的出现,它旨在提供与浏览器支持的相同的fetch API。在这篇文章中,我们将看看如何使用node-fetch来帮助你爬取网页。

准备条件

要获得本文的全部好处,你应该有。

  • 有一些编写ES6 Javascript的经验。
  • 对承诺有正确的理解,对async/await有一些经验。

什么是Fetch API?

Fetch是一个规范,旨在规范请求、响应以及两者之间的一切,该标准声明为fetching(因此称为fetch)。浏览器的fetchAPI和node-fetch是这个规范的实现。fetch和它的前身XHR之间最大和最重要的区别是,它是围绕承诺(Promises)建立的。这意味着开发者不必再担心XHR所拥有的回调地狱、混乱的代码和极其冗长的API。

还有一些技术上的差异:例如,当一个请求以HTTP状态代码404返回时,从fetch调用中返回的承诺不会被拒绝。

node-fetch将所有这些都带到了服务器端。这意味着开发人员不再需要学习不同的API,它们的各种术语,或在幕后如何获取实际发生的HTTP请求从服务器端执行。这就像运行npm install node-fetch和编写HTTP请求一样简单,与你在浏览器中的做法几乎一样。


node-fetchcheerio进行网页爬取

要开始工作,你必须首先安装cheerionode-fetch。虽然node-fetch允许我们获得任何页面的HTML,但由于其结果只是一堆文本,你需要一些工具来从中提取你需要的东西。cheerio可以帮助你,它提供了一个非常直观的类似JQuery的API,并允许你从node-fetch收到的HTML中提取数据。

请确保你有一个package.json,如果没有的话。

  • 通过运行npm init生成一个
  • 然后通过运行以下命令安装cheerionode-fetchnpm 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实现了BodyBody拥有的所有方法都可以被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请求方法,如PUTDELETE,你也可以这么做。要在请求的同时发送任何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的另一个好处是,它比使用无头浏览器要有效得多。即使是提交表单这样的简单任务,无头浏览器也很慢,而且会占用大量的服务器资源。

blank

Written by 爬取 大师

阿里P12级别选手,能够突破各种反爬, 全能的爬取大师,擅长百万级的数据抓取!没有不能爬,只有你不敢想,有爬取项目可以联系我邮箱 [email protected] (带需求和预算哈, 不然多半不回复)