使用JS和NodeJS爬取Web内容

2020 年 6 月 15 日

使用JS和NodeJS爬取Web内容

这些年来 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');
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}/`);
});

如果你已安装 NodeJS,运行 node < YourFileNameHere>.js(去掉 <> 号),然后打开浏览器并导航到 localhost:3000,就能看到“HelloWorld”的文本了。NodeJS 非常适合 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 ({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 来代替:

复制代码
async function getForum() {
try {
const response = await axios.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 请求的方式如下:

复制代码
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)
}

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

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

正则表达式:困难的方法

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

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

复制代码
const htmlString = '<label>Username: John Doe</label>'
const result = 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。

复制代码
const cheerio = 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 的新文件,并复制 / 粘贴以下代码:

复制代码
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));

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 } = new JSDOM(
'<h2 class="title">Hello world</h2>'
).window
const heading = 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")
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));

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

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

创建 DOM 后,你将使用相同的 DOM 方法获取第一篇文章的点赞按钮,然后单击它。要验证单击操作是否有效,可以检查 classList 中是否有一个名为 upmod 的类。如果此类存在于 classList 中,则返回一条消息。

现在,你可以启动一个终端并运行 node crawler.js,然后你将看到一段文字,告诉你帖子是否已被点赞。尽管这个示例用例很简单,但是你可以在此基础上创建一些功能强大的事物,例如一个对特定用户的帖子投票的 bot。

如果你觉得 JSDOM 缺乏表现力,而且你的爬网工作很依赖这类操作,或者需要创建许多各不相同的 DOM,那么以下选项将是更好的选择。

Puppeteer:无头浏览器

顾名思义,Puppeteer 允许你以编程方式操控浏览器,就像木偶师操纵木偶一样。它默认为开发人员提供了一个高级 API 来控制 Chrome 的一个无头版本,并可以配置为无头运行。

Puppeteer 比之前提到的那些工具有用得多,因为它能让你的爬网操作就像真人与浏览器交互一样。这就开拓了很多前所未有的可能性:

  • 你可以获取屏幕截图或生成页面 PDF。
  • 你可以抓取单页应用程序并生成预渲染的内容。
  • 自动执行许多不同的用户交互,例如键盘输入、表单提交、导航等。

它也能在爬网之外的其他许多任务中发挥重要作用,例如 UI 测试、辅助性能优化等。

你经常需要截取网站的截图,也许是为了了解竞争对手的产品目录,那么就可以使用 puppeteer 来做到这一点。首先必须安装 puppeteer,请运行以下命令:npm install puppeteer

这将下载 Chromium 的一个打包版本,根据你的操作系统版本,这个版本大约需要 180MB 到 300MB。如果你不想用它,而是将 puppeteer 指向一个已经下载的 chromium 版本,则必须设置一些环境变量,但我不建议这样做。如果你确实不想为了这篇教程去下载 Chromium 和 puppeteer,那么还可以用一个 puppeteer playground( https://try-puppeteer.appspot.com/ )。

下面我们尝试在 Reddit 中获取 r/programming 论坛的屏幕截图和 PDF,创建一个名为 crawler.js 的新文件,然后复制 / 粘贴以下代码:

复制代码
const puppeteer = require('puppeteer')
async function getVisual() {
try {
const URL = 'https://www.reddit.com/r/programming/'
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto(URL)
await page.screenshot({ path: 'screenshot.png' })
await page.pdf({ path: 'page.pdf' })
await browser.close()
} catch (error) {
console.error(error)
}
}
getVisual()

getVisual() 是一个异步函数,它将获取分配给 URL 变量的值的屏幕快照和 pdf。首先运行 puppeteer.launch() 创建浏览器实例,然后创建一个新页面。可以将该页面视为常规浏览器中的选项卡。接着使用 URL 作为参数调用 page.goto(),将先前创建的页面定向到指定的 URL。最终,浏览器实例与页面一起被销毁。
完成此操作并完成页面加载后,将分别使用 page.screenshot() 和 page.pdf() 截取屏幕截图和 pdf。你也可以侦听 Javascript 加载事件并执行这些操作,在生产环境中强烈建议后面这种方法。

在终端上输入 node crawler.js 来运行代码,几秒钟后会创建 2 个文件,分别名为 screenshot.jpg 和 page.pdf。

Nightmare:Puppeteer 的替代品

Nightmare 也是像 Puppeteer 这样的高级浏览器自动化库,它用的是 Electron,但据说比以前的 PhantomJS 快大约一倍,并且更加现代化。

如果你出于种种原因不喜欢 Puppeteer,或者觉得 Chromium 包太大了,那么 Nightmare 就是你理想的选择。首先运行以下命令来安装 nightmare 库:npm install nightmare

待其下载完毕,我们将使用它通过谷歌引擎找到 ScrapingBee 的网站。为此创建一个名为 crawler.js 的文件,然后将以下代码复制 / 粘贴到其中:

复制代码
const Nightmare = require('nightmare')
const nightmare = Nightmare()
nightmare
.goto('https://www.google.com/')
.type("input[title='Search']", 'ScrapingBee')
.click("input[value='Google Search']")
.wait('#rso > div:nth-child(1) > div > div > div.r > a')
.evaluate(
() =>
document.querySelector(
'#rso > div:nth-child(1) > div > div > div.r > a'
).href
)
.end()
.then((link) => {
console.log('Scraping Bee Web Link': link)
})
.catch((error) => {
console.error('Search failed:', error)
})

这里首先会创建一个 Nighmare 实例,然后调用 goto() 将该实例定向到谷歌搜索引擎;加载后,使用其选择器获取搜索框,然后更改搜索框的值(一个输入标签)到“ScrapingBee”。完成后,单击搜索按钮提交搜索表单。接着 Nightmare 会等到第一个链接加载完毕,一旦加载完成,将使用 DOM 方法获取包含该链接的 anchor 标记的 href 属性值。
最后,完成所有操作后,这个链接将打印到控制台。要运行代码,请在终端中输入 node crawler.js。

小结

文章好长啊!但现在你了解了 NodeJS 的不同用法,以及它丰富的库生态系统,这样就能用各种姿势随意爬网了。总结一下,你知道了:

  • NodeJS 是 Javascript 运行时,允许 Javascript 在服务端运行。感谢事件循环,它天生是非阻塞的。
  • HTTP 客户端(例如 Axios、Superagent 和 Request)用于将 HTTP 请求发送到服务器并接收响应。
  • Cheerio 从 JQuery 摄取精华,以在服务端运行来爬取 Web 内容,但不能执行 Javascript 代码。
  • JSDOM 根据标准 Javascript 规范从 HTML 字符串中创建 DOM,并允许你对其执行 DOM 操作。
  • Puppeteer 和 Nightmare 是高级浏览器自动化库,可让你以编程方式操纵 Web 应用程序,就像真实的人在与之交互一样。

资源

原文链接:

https://www.scrapingbee.com/blog/web-scraping-javascript/

2020 年 6 月 15 日 17:56 1780

评论

发布
暂无评论
发现更多内容

知识点:操作系统异常的分类

王坤祥

操作系统 异常

不想做经理的程序员

escray

面试 学习笔记 面试现场

java安全编码指南之:基础篇

程序那些事

Java 安全编码 安全编码指南

新晋“网红”Cat1 是什么

华为云开发者社区

后端 物联网 华为云 无线通信 Cat.1

ARTS 挑战打卡第十一周(200720-200726)

老胡爱分享

ARTS 打卡计划

ARTS 挑战打卡第十周(200713-200719)

老胡爱分享

ARTS 打卡计划

linux入门系列11--Centos7网络服务管理

黑马腾云

Linux centos 网络配置 运维工程师

百度联合发布全球最大中文自然语言处理数据共建计划“千言”

百度大脑

人工智能 百度 nlp 百度大脑

七夕节来啦!AI一键生成情诗,去发给你的女朋友吧!

华为云开发者社区

AI 智能高效 华为云 modelarts 七夕

惠普精灵家族助力IMC上海站,极致体验尽享电竞狂欢

intel001

13年毕业,用两年时间从外包走进互联网大厂!

小傅哥

成长 小傅哥 经历 工作 入职

一行代码实现简易服务器并共享文件

王坤祥

Python 共享文件

ARTS 挑战打卡第十二周(200727-200802)

老胡爱分享

ARTS 打卡计划

7. Jackson用树模型处理JSON是必备技能,不信你看

YourBatman

json Jackson ObjectMapper 树模型

英特尔大小核试水 将推出8+8+1架构酷睿处理器

intel001

linux入门系列12--磁盘管理之分区、格式化与挂载

黑马腾云

Linux centos 编辑器 linux运维 vi/vim

linux入门系列13--磁盘管理之RAID、LVM技术

黑马腾云

Linux centos raid lvm 磁盘挂载

SpreadJS 纯前端表格控件应用案例:货运代理客户服务平台

Geek_Willie

3.7亿条保单数据怎么分析?这个大数据平台有绝招

华为云开发者社区

大数据 hadoop 数据湖 FusionInsight Kyligence

ARTS 挑战打卡第十三周(200803-200809)

老胡爱分享

ARTS 打卡计划

七夕情人节,程序员的表白方式简直太秀了!

程序员生活志

程序员 七夕

【程序员自救指南】一个证书,让我哄好了小师妹

华为云开发者社区

网络安全 浏览器 华为云 SSL证书 安全证书

Docker 最常用的镜像命令和容器命令

哈喽沃德先生

Docker 容器 微服务 容器技术 容器化

追逐影子的人,最终只会是影子

小隐乐乐

正向代理与反向代理

王坤祥

nginx 反向代理 代理 正向代理与反向代理 Proxy

Python实现一个计时功能的装饰器

王坤祥

Python 装饰器

区块链数字钱包开发方案,持币生息理财钱包搭建

WX13823153201

钱包系统开发

高效程序员的45个习惯:敏捷开发修炼之道(5)

石云升

敏捷开发 熵增 用代码沟通

LeetCode题解:20. 有效的括号,for循环replace,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

linux入门系列14--ssh服务及主机远程管理

黑马腾云

Linux centos linux运维 红帽认证

linux入门系列15--文件传输之vsftp服务

黑马腾云

Linux centos linux操作 linux运维

使用JS和NodeJS爬取Web内容-InfoQ