39

使用 JS 和 NodeJS 爬取 Web 内容

 3 years ago
source link: https://www.infoq.cn/article/LDq3m75kvNkYPxfEPAdB
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这些年来 Javascript 进步飞快,又引入了称为 NodeJS 的运行时,所以已经成为了最流行和使用最广泛的语言之一。不管你要写的是 Web 应用还是移动应用,都能在 Javascript 生态中找到合适的工具。本文要介绍的是如何在 NodeJS 的活跃生态系统帮助下高效地抓取 Web 内容,以满足大多数相关需求。

本文最初发布于 scrapingbee.com 网站,经网站授权由 InfoQ 中文站翻译并分享。

前提

这篇文章主要针对拥有一定 Javascript 开发经验的开发人员。但如果你很熟悉 Web 内容爬取,那么就算没有 Javascript 的相关经验,也能从本文中学到很多知识。

  • JS 语言开发背景
  • 使用 DevTools 提取元素选择器(selector)的经验
  • 与 ES6 Javascript 相关的经验(可选)

成果

阅读这篇文章能够帮助读者:

  • 了解 NodeJS 的功能
  • 使用多个 HTTP 客户端来辅助 Web 抓取工作
  • 利用多个经过实战检验的现代库来抓取 Web 内容

了解 NodeJS:简介

Javascript 是一种简单而现代化的语言,最初是为了向浏览器访问的网站添加动态行为而创建的。网站加载后,Javascript 通过浏览器的 JS 引擎运行,并转换为计算机可以理解的一堆代码。为了让 Javascript 与你的浏览器交互,后者提供了一个运行时环境(文档,窗口等)。

换句话说 Javascript 这种编程语言无法直接与计算机或其资源交互,抑或操纵它们。例如,在 Web 服务器中服务器必须能够与文件系统交互,才能读取文件或将记录存储在数据库中。

NodeJS 的理念是让 Javascript 不仅能运行在客户端,还能运行在服务端。为了做到这一点,资深开发人员 Ryan Dahl 采用了谷歌 Chrome 浏览器的 v8 JS 引擎,并将其嵌入了到名为 Node 的 C++ 程序中。因此 NodeJS 是一个运行时环境,它让使用 Javascript 编写的应用程序也能运行在服务器上。

大多数语言(例如 C 或 C++)使用多个线程来处理并发,相比之下 NodeJS 只使用单个主线程,并在事件循环(Event Loop)的帮助下用它以非阻塞方式执行任务。

我们很容易就能建立一个简单的 Web 服务器,如下所示:

复制代码

const http = require('http');
constPORT= 3000;
constserver= 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}/`);
});

如果你已安装 NodeJS,运行 node < YourFileNameHere>.js(去掉 <> 号),然后打开浏览器并导航到 localhost:3000,就能看到“HelloWorld”的文本了。NodeJS 非常适合 I/O 密集型应用程序。

HTTP 客户端:查询 Web

HTTP 客户端是将请求发送到服务器,然后从服务器接收响应的工具。本文要讨论的工具大都在后台使用 HTTP 客户端来查询你将尝试抓取的网站服务器。

Request

Request 是 Javascript 生态系统中使用最广泛的 HTTP 客户端之一,不过现在 Request 库的作者已正式声明,不推荐大家继续使用它了。这并不是说它就不能用了,还有很多库仍在使用它,并且它真的很好用。使用 Request 发出 HTTP 请求非常简单:

复制代码

constrequest =require('request')
request('https://www.reddit.com/r/programming.json',function({1}
error,
response,
body
{1}) {
console.error('error:', error)
console.log('body:', body)
})

你可以在 Github 上找到 Request 库( https://github.com/request/request ),运行 npm install request 就能安装完成。这里可以参考弃用通知及细节( https://github.com/request/request/issues/3142 )。如果你因为这个库过时了而觉得不放心,后面还有更多推荐!

Axios

Axios 是基于 promise 的 HTTP 客户端,可在浏览器和 NodeJS 中运行。如果你使用 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)
});

如果你喜欢 Promises API 的 async/await 语法糖,那么也可以用它们,但由于顶级的 await 仍处于第 3 阶段( https://github.com/tc39/proposal-top-level-await ),

我们只能用 Async Function 来代替:

复制代码

asyncfunctiongetForum(){
try{
constresponse =awaitaxios.get(
'https://www.reddit.com/r/programming.json'
)
console.log(response)
}catch(error) {
console.error(error)
}
}

你只需调用 getForum 即可!你可以在 Github 上找到 Axios 库( https://github.com/axios/axios ),运行 npm install axios 即可安装。

Superagent

类似 Axios,Superagent 是另一款强大的 HTTP 客户端,它支持 Promise 和 async/await 语法糖。它的 API 像 Axios 一样简单,但 Superagent 的依赖项更多,并且没那么流行。

在 Superagent 中,使用 promise、async/await 或 callbacks 发出 HTTP 请求的方式如下:

复制代码

constsuperagent =require("superagent")
constforumURL ="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
asyncfunctiongetForum(){
try{
constresponse =awaitsuperagent.get(forumURL)
console.log(response)
}catch(error) {
console.error(error)
}

你可以在 Github 上找到 Superagent 库( https://github.com/visionmedia/superagent ),运行 npm install superagent 即可安装。

对于下文介绍的 Web 抓取工具,本文将使用 Axios 作为 HTTP 客户端。

正则表达式:困难的方法

在没有任何依赖项的情况下开始抓取 Web 内容,最简单的方法是:使用 HTTP 客户端查询网页时,在收到的 HTML 字符串上应用一组正则表达式——但这种方法绕的路太远了。正则表达式没那么灵活,并且很多专业人士和业余爱好者都很难写出正确的正则表达式。

对于复杂的 Web 抓取任务来说,正则表达式很快就会遇到瓶颈了。不管怎样我们先来试一下。假设有一个带用户名的标签,我们需要其中的用户名,那么使用正则表达式时的方法差不多是这样:

复制代码

consthtmlString = '<label>Username: JohnDoe</label>'
constresult = htmlString.match(/<label>(.+)<\/label>/)
console.log(result[1], result[1].split(": ")[1])
// Username: John Doe, John Doe

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

如你所见,对于一个非常简单的用例,这种方法用起来都很麻烦。所以我们应该使用 HTML 解析器之类的工具,后文具体讨论。

Cheerio:用于遍历 DOM 的核心 JQuery

Cheerio 是一个高效轻便的库,它允许你在服务端使用 JQuery 的丰富而强大的 API。如果你以前使用过 JQuery,那么很容易就能上手 Cheerio。它把 DOM 所有不一致性和浏览器相关的特性都移除掉了,并公开了一个高效的 API 来解析和操作 DOM。

复制代码

constcheerio =require('cheerio')
const$ = cheerio.load('<h2 class="title">Hello world</h2>')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
// <h2 class="title welcome">Hello there!</h2>

如你所见,Cheerio 用起来和 JQuery 很像。

但是,它的工作机制和 Web 浏览器是不一样的,这意味着它不能:

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

因此,如果你试图爬取的网站或 Web 应用程序有很多 Javascript 内容(例如“单页应用程序”),那么 Cheerio 并不是你的最佳选择,你可能还得依赖后文讨论的其他一些选项。

为了展示 Cheerio 的强大能力,我们将尝试在 Reddit 中爬取 r/programming 论坛,获取其中的帖子标题列表。

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

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

复制代码

constaxios =require('axios');
constcheerio =require('cheerio');
constgetPostTitles =async() => {
try{
const{ data } =awaitaxios.get(
'https://old.reddit.com/r/programming/'
);
const$ = cheerio.load(data);
constpostTitles = [];
$('div > p.title > a').each((_idx, el) =>{
constpostTitle = $(el).text()
postTitles.push(postTitle)
});
returnpostTitles;
}catch(error) {
throwerror;
}
};
getPostTitles()
.then((postTitles) =>console.log(postTitles));

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

接下来使用浏览器的开发工具,你可以获得通常可以定位所有 postcard 的选择器。如果你用过 JQuery,肯定非常熟悉 $(‘div > p.title > a’)。这将获取所有帖子,因为你只想获得每个帖子的标题,所以必须遍历每个帖子(使用 each() 函数来遍历)。

要从每个标题中提取文本,必须在 Cheerio 的帮助下获取 DOM 元素(el 表示当前元素)。然后在每个元素上调用 text() 以获取文本。

现在,你可以弹出一个终端并运行 node crawler.js,然后你将看到一个由大约 25 或 26 个帖子标题组成的长长的数组。尽管这是一个非常简单的用例,但它展示了 Cheerio 提供的 API 用起来是多么简单。

如果你的用例需要执行 Javascript 并加载外部资源,那么可以考虑以下几个选项。

JSDOM:给 Node 用的 DOM

JSDOM 是用在 NodeJS 中的,文档对象模型(DOM)的纯 Javascript 实现,如前所述,DOM 对 Node 不可用,而 JSDOM 就是最近似的替代品。它多少模拟了浏览器的机制。

创建了一个 DOM 后,我们就可以通过编程方式与要爬取的 Web 应用程序或网站交互,像点击按钮这样的操作也能做了。如果你熟悉 DOM 的操作方法,那么 JSDOM 用起来也会很简单。

复制代码

const{ JSDOM } = require('jsdom')
const{document} =newJSDOM(
'<h2 class="title">Hello world</h2>'
).window
constheading =document.querySelector('.title')
heading.textContent ='Hello there!'
heading.classList.add('welcome')
heading.innerHTML
// <h2 class="title welcome">Hello there!</h2>

如你所见,JSDOM 创建了一个 DOM,然后你就可以像操纵浏览器 DOM 那样,用相同的方法和属性来操纵这个 DOM。

为了演示如何使用 JSDOM 与网站交互,我们将获取 Redditr/programming 论坛的第一篇帖子,并对其点赞,然后我们将验证该帖子是否已被点赞。

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

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

复制代码

const{ JSDOM } = require("jsdom")
constaxios = require('axios')
constupvoteFirstPost =async() => {
try{
const{ data } =awaitaxios.get("https://old.reddit.com/r/programming/");
constdom =newJSDOM(data, {
runScripts:"dangerously",
resources:"usable"
});
const{document} = dom.window;
constfirstPost =document.querySelector("div > div.midcol > div.arrow");
firstPost.click();
constisUpvoted = firstPost.classList.contains("upmod");
constmsg = isUpvoted
?"Post has been upvoted successfully!"
:"The post has not been upvoted!";
returnmsg;
}catch(error) {
throwerror;
}
};
upvoteFirstPost().then(msg => console.log(msg));

upvoteFirstPost() 是一个异步函数,它将在 r/programming 中获取第一个帖子,然后对其点赞。为此,axios 发送 HTTP GET 请求以获取指定 URL 的 HTML。然后向 JSDOM 提供先前获取的 HTML 来创建新的 DOM。JSDOM 构造器将 HTML 作为第一个参数,将选项作为第二个参数,添加的 2 个选项会执行以下函数:

  • runScripts :设置为“dangerously”时,它允许执行事件处理程序和任何 Javascript 代码。如果你不清楚应用程序将运行的脚本是否可信,则最好将 runScripts 设置为“outside-only”,这会将所有 Javascript 规范提供的全局变量附加到 window 对象,从而防止任何脚本在内部执行。
  • resources :设置为“usable”时,它允许加载使用

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK