使用 JS 和 NodeJS 爬取 Web 内容-阿里云开发者社区

开发者社区> 云栖号资讯小哥> 正文

使用 JS 和 NodeJS 爬取 Web 内容

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

云栖号资讯:【点击查看更多行业资讯
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!

前提
这篇文章主要针对拥有一定 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"
// 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”时,它允许加载使用

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/zhibo

立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-06-15
本文作者:Shenesh Perera
本文来自:“InfoQ ”,了解相关信息可以关注“InfoQ

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Oracle的sequence
  概述   Oracle的sequence,就是序列号,它提供一系列的按照事先指定的方式进行增长的数字。oracle sequence的最大值是38个整数。【 Sequences are database objects from which multiple users can generate unique integers.】。一般来说,sequence常用于生成数据库的主键。   Oracle将sequence的定义存储在数据字典之中,因此,所有的sequence都在数据库的SYSTEM表空间里面。
2 0
【七天入门Go语言】程序结构 && 数据类型 | 第二天
目录 1. 程序结构 1.1 名称 1.2 声明 1.3 注释 1.4 单双引号 1.5 输出 2. 数据类型 2.1 整型 2.2 浮点型 2.3 复数 2.4 布尔型 2.5 字符串 2.6 常量 2.7 数组 2.8 切片 2.9 map 2.10 结构体 2.11 JSON 3. 流程控制 3.1 条件语句 3.2 选择语句 3.3 循环语句 最后
4 0
JPA和MyBatis的优缺点对比,你是不是都不知道有哪些?
  # 什么是JPA   JPA是一种规范,它简化了现有持久化的开发,并且充分吸收了Hibernate、TopLInk、JDO等框架。SpringData JPA是全自动框架,不需要自己写sql,当然也可以自己写sql实现。而自动生成sql这点是优点,也是缺点,因为生成的sql可读性差,而且一些业务比如执行逻辑删除等还是需要自己来实现sql。   # 什么是Mybatis   Mybatis是一种半自动的ORM框架,它简单易上手,没有第三方依赖,支持对象与数据库的ORM关系映射,将sql代码与业务代码分离,使得开发人员可以更自如的写出高效的sql,不过反过来说不像SpringData J
4 0
Kafka -- 幂等生产者 + 事务生产者
  消息交付可靠性保障:Kafka对Producer和Consumer要处理的消息所提供的承诺常见的承诺最多一次(at most once):消息可能会丢失,但绝不会被重复发送至少一次 (at least once):消息不会丢失,但有可能被重复发送精确一次(exactly once):消息不会丢失,也不会被重复发送Kafka默认提供的交付可靠性保障:至少一次只有Broker成功提交 消息且Producer接到Broker的应答才会认为该消息成功发送如果Broker成功提交消息,但Broker的应答没有成功送回Producer端,Producer只能选择重试最多一次Kafka也可以提供最多一
4 0
linux安装oracle client客户端远程连接数据库
  linux安装oracle client客户端远程连接数据库。   1.到oracle官网下载basic,sqlplus,devel三个软件包   oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.tar   oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.tar   oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.tar   2.到root用户下创建一个oracle文件夹
4 0
JDBC基础入门视频教程,轻松掌握jdbc基础+核心技术,超全面
  JDBC是什么?   JDBC是Sun提供的一套数据库编程接口API函数,由Java语言编写的类、界面组成。   JDBC API 的设计初衷就是为了让简单的事情更简单,这意味着JDBC使得执行所有数据库任务都更容易.   用JDBC写的程序能够自动地将SQL语句传送给相应的数据库管理系统。不但如此,使用Java编写的应用程序可以在任何支持Java的平台上运行,不必在不同的平台上编写不同的应用。   Java和JDBC的结合可以让开发人员在开发数据库应用程序时真正实现“WriteOnce,RunEverywhere!”   JDBC的用途是什么?
4 0
Linux目录结构介绍
  根文件系统   /bin   这一目录中存放了供所有用户使用的完成基本维护任务的命令。其中bin是binary的缩写,表示二进制文件,通常为可执行文件。一些常用的系统命令,如cp、ls等保存在该目录中。
4 0
Linux系统下安装Redis数据库教程
  Linux系统下安装Redis数据库。   1.首先将我们的压缩包放到opt文件夹下。redis-3.0.4.tar.gz这是我的压缩包。   2.然后使用命令进行解压,tar -zxvf 压缩包   tar -zxvf redis-3.0.4.tar.gz //解压Redis包   3.使用make命令进行安装。   如果失败的话应该是没安装上gcc编辑器,一般都会报这个错。
5 0
JDBC从入门到精通视频教程,建议收藏!非常实用
  JDBC (Java Database Connectivity)   JDBC本质上属于一种服务,服务的特征,必须按照指定的规范进行操作   JDBC相关概念核心包java.sqlDriverManagerConnectionStatementPreparedStatementResultSetJDBC学习教程   分享给大家JDBC的学习教程,在视频中详细讲解了Java语言如何连接数据库,对数据库中的数据进行增删改查操作,适合于已经学习过Java编程基础以及数据库的同学。
4 0
Linux之whereis命令
  whereis命令只能用于程序名的搜索,而且只搜索二进制文件(参数-b)、man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。   和find相比,whereis查找的速度非常快,这是因为linux系统会将系统内的所有文件都记录在一个数据库文件中,当使用whereis和下面即将介绍的locate时,会从数据库中查找数据,而不是像find命令那样,通过遍历硬盘来查找,效率自然会很高。
4 0
+关注
云栖号资讯小哥
云栖号小编在此 ^o^
1403
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载