作者 | Shenesh Perera译者 | 王强策划 | 李俊辰这些年来 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 .js(去掉<>号),然后打开浏览器并导航到 localhost:3000,就能看到“HelloWorld”的文本了。NodeJS 非常适合 I/O 密集型应用程序。

HTTP 客户端:查询 Web

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

RequestRequest 是 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 库,运行 npm install request 就能安装完成。这里可以参考弃用通知及细节:

https://github.com/request/request/issues/3142

AxiosAxios 是基于 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 阶段,我们只能用 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 库,运行 npm install axios 即可安装。

https://github.com/axios/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 库,运行 npm install superagent 即可安装。

https://github.com/visionmedia/superagent

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

正则表达式:困难的方法

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

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

const htmlString = 'Username: John Doe'const result = htmlString.match(/(.+)/)console.log(result[1], result[1].split(": ")[1])// Username: John Doe, John Doe

在 Javascript 中,match() 通常返回一个数组,该数组包含与正则表达式匹配的所有内容。第二个元素(在索引 1 中)将找到 textContent 或标签的 innerHTML,这正是我们想要的。但是这个结果会包含一些我们不需要的文本(“Username: ”),必须将其删除。如你所见,对于一个非常简单的用例,这种方法用起来都很麻烦。所以我们应该使用 HTML 解析器之类的工具,后文具体讨论。

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

const cheerio = require('cheerio')const $ = cheerio.load('

Hello world

')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
//

Hello there!

如你所见,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(    '

Hello world

'
).window
const heading = document.querySelector('.title')
heading.textContent = 'Hello there!'
heading.classList.add('welcome')
heading.innerHTML
//

Hello there!

如你所见,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”时,它允许加载使用

node.js request get 请求怎么拿到返回的数据_使用JS和NodeJS爬取Web内容相关推荐

  1. node.js request get 请求怎么拿到返回的数据_从零开始用nodejs写一个简单的静态服务器

    nodejs搭建服务器第一步 const http = require("http")const PORT = 8000 const server = http.createSer ...

  2. node.js request get 请求怎么拿到返回的数据_NodeJS运维: 从 0 开始 Prometheus + Grafana 业务性能指标监控...

    为什么需要指标监控告警 一个复杂的应用,往往由很多个模块组成,而且往往会存在各种各样奇奇怪怪的使用场景,谁也不能保证自己维护的服务永远不会出问题,等用户投诉才发现问题再去处理问题就为时已晚,损失已无法 ...

  3. Hystrix面试 - 基于 request cache 请求缓存技术优化批量商品数据查询接口

    Hystrix面试 - 基于 request cache 请求缓存技术优化批量商品数据查询接口 Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是否有缓 ...

  4. java 微信报关_微信开放平台,_请求微信报关接口返回的数据,微信开放平台 - phpStudy...

    请求微信报关接口返回的数据 请求微信报关 接口错误 552E051CB6F4DBC6029B8218DBD5A52A gh_4dbf09a0a18e 1342661701 20160825113538 ...

  5. js插件---WebUploader 如何接收服务端返回的数据

    js插件---WebUploader 如何接收服务端返回的数据 一.总结 一句话总结: uploadSuccess有两个参数,一个是file(上传的文件信息),一个是response(服务器返回的信息 ...

  6. vue获取商品数据接口_基于 request cache 请求缓存技术优化批量商品数据查询接口...

    Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是否有缓存. 首先,有一个概念,叫做 Request Context 请求上下文,一般来说,在一个 we ...

  7. Ajax入门-搭建服务器并使用ajax技术向服务器发送一个请求并获得服务器返回的数据

    今天刚入坑学习ajax,刚开始就遇到服务器这一知识盲区,经过解决各种问题,成功运行的代码,下面就分享给你们,希望能够帮你们解决问题. (一)node.js安装 1.进入官网,下载对应版本 2.下载了. ...

  8. 根据id删除localstorage数据_原生js利用localstorage实现简易TODO list应用

    前言:小生不才,只懂得一些皮毛,我努力以最简单的语言将心中的想法表述出来,让更多人能够很轻松的弄明白.文章里面有不足之处望各位大牛指出,使得后面的文章能够朝着更好的方向发展.另外,大家记得点赞哟! 欢 ...

  9. 关于对发送HTTP请求以及解析服务器返回的数据操作的提取到一个公共类中进行封装

    创建一个名为HttpUtil的类并提供名为sendHttpRequest静态方法. 相关代码如下: package com.hzy.networktest; import java.io.Buffer ...

最新文章

  1. Python的输出:Python2.7和Python3.7的区别
  2. Struts2中Action接收参数
  3. mysql查询两个日期之间的数据
  4. kettle使用_ETL工具(kettle)-《PentahoKettle解决方案-使用PDI构建开源ETL解决方案》
  5. 如何在两个服务器之间迁移MySQL数据库
  6. 雪城大学信息安全讲义 五、竞态条件
  7. OAF_开发系列18_实现OAF页面跳转setForwardURL / forwardImmediately(案例)
  8. async await 的用法
  9. 如何学会阅读源码,看这篇就够了!
  10. 17.卷2(进程间通信)---后记
  11. jquery中的trigger和triggerHandler区别
  12. WinHex license添加(v19测试可用)
  13. 地理必修一三大类岩石_中图版高中地理(必修一)知识归纳——第二章
  14. 对链表进行插入排序。从第一个元素开始,该链表可以被认为已经部分排序。每次迭代时,从输入数据中移除一个元素,并原地将其插入到已排好序的链表中。
  15. web版Project简介
  16. 在线购物系统——设计类
  17. k8s拉取镜像规则_【大强哥-k8s从入门到放弃13】Service详解
  18. 《高等数学A》课堂笔记——高分必过
  19. 估计的商是什么意思_《商》字意思读音、组词解释及笔画数 - 新华字典 - 911查询...
  20. 一车abs线路怎么量_abs传感器怎么测量好坏

热门文章

  1. 安装caffe(CPU版本)的一些参考和问题的解决
  2. 解决webpack打包bootstrap报字体不能解析问题
  3. hive导出数据到本地文件报错解决方法
  4. DotNet Core 2.2 MVC Razor 页面编译为 View.dll 文件的解决方法
  5. IDEA中引用不到HttpServlet的解决方案
  6. 解决异常:“The last packet sent successfully to the server was 0 milliseconds ago. ”的办法
  7. Windows魔法堂:解决“由于启动计算机时出现页面文件配置问题.......”
  8. Android:禁用listView上的突出显示单击
  9. 人工神经网络相对于支持向量机有什么优势? [关闭]
  10. 无法将stdClass类型的对象用作数组?