如何轻松和安全地构建的满足合规要求的智能产品,实现业务需求?4月26日,告诉你答案! 了解详情
写点什么

使用 JS 和 NodeJS 爬取 Web 内容

  • 2020 年 6 月 15 日
  • 本文字数:7555 字

    阅读完需:约 25 分钟

使用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 (  error,  response,  body) {  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"// callbackssuperagent  .get(forumURL)  .end((error, response) => {    console.log(response)  })// promisessuperagent  .get(forumURL)  .then((response) => {    console.log(response)  })  .catch((error) => {    console.error(error)  })// promises with async/awaitasync 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>').windowconst 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:562800

评论 1 条评论

发布
用户头像
js 爬虫
2020 年 12 月 22 日 13:30
回复
没有更多了
发现更多内容

抗疫物资分配难?区块链来帮忙!

旺链科技

区块链 产业区块链 物资溯源

一级等保怎么做?要收费吗?等保要求是什么?

行云管家

网络安全 等保 等保2.0 一级等保

如何以Sonar为例创建一个适用与所有企业的测试步骤

阿里云云效

阿里云 运维 测试 sonar 研发测试

10个比较有用的jQuery插件

小谷哥

阿里云云效研发协同服务相关协议条款 |云效

阿里云云效

云计算 阿里云 运维 研发 研发协同

智慧党建系统开发,智慧组工党员信息管理平台建设

WX13823153201

用数字“钥匙”打开发展新空间

CECBC

Android ANR分析(trace文件的产生流程)

北洋

4月月更

关于Signal Catcher线程中对线程的理解

北洋

4月月更

解锁OpenHarmony技术日!年度盛会,即将揭幕!

OpenHarmony开发者社区

OpenHarmony 技术日

数字化时代,SaaS软件如何成为国产化替代的轻骑兵?

小炮

LLVM之父Chris Lattner:编译器的黄金时代

OneFlow

编程语言 编译器 LLVM 加速器 MLIR

swap交易所套利夹子机器人系统开发详情

NFT元宇宙平台开发

游戏+NFT,脱虚向实外的另一可行场景

CECBC

区块链+数字资产,未来财富的新起点

CECBC

索信达获金融街资本1亿元投资

索信达控股

赏金猎人自动交易机器人开发模式分析

NFT元宇宙平台开发

架构实战毕业总结

王大胖

scala安装及环境配置

小谷哥

海口等保测评公司有几家?具体在哪里?哪里可以查到?

行云管家

网络安全 等保 等保测评 海南 海口

Redis的五种数据结构分析

小谷哥

数据仓库架构演变和建设思路

五分钟学大数据

数据仓库 4月月更

如何选择合适的 Neo4j 版本(2022)

Shiny亮小猪

neo4j 图数据库 图算法

使用JS和NodeJS爬取Web内容_语言 & 开发_Shenesh Perera_InfoQ精选文章