谁说只有python可以爬虫,Node专题系列之cheerio实现爬虫

2,116 阅读7分钟

爬虫

什么是爬虫

网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

爬虫的分类

  • 通用网络爬虫(全网爬虫)

    爬行对象从一些 种子URL 扩充到整个 Web,主要为门户站点搜索引擎和大型 Web 服务提供商采集数据。

  • 聚焦网络爬虫(主题网络爬虫)

    是 指选择性 地爬行那些与预先定义好的主题相关页面的网络爬虫。

  • 增量式网络爬虫

    指对已下载网页采取增量式更新和 只爬行新产生的或者已经发生变化网页 的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面。

  • Deep Web爬虫

    爬行对象是一些在用户填入关键字搜索或登录后才能访问到的深层网页信息的爬虫。

爬虫的爬行策略

  • 通用网络爬虫(全网爬虫)

深度优先策略、广度优先策略

  • 聚焦网络爬虫(主题网络爬虫)

基于内容评价的爬行策略(内容相关性),基于链接结构评价的爬行策略、基于增强学习的爬行策略(链接重要性),基于语境图的爬行策略(距离,图论中两节点间边的权重)

  • 增量式网络爬虫

统一更新法、个体更新法、基于分类的更新法、自适应调频更新法

  • Deep Web 爬虫

Deep Web 爬虫爬行过程中最重要部分就是表单填写,包含两种类型:基于领域知识的表单填写、基于网页结构分析的表单填写

现代的网页爬虫的行为通常是四种策略组合的结果:

选择策略:决定所要下载的页面; 重新访问策略:决定什么时候检查页面的更新变化; 平衡礼貌策略:指出怎样避免站点超载; 并行策略:指出怎么协同达到分布式抓取的效果;

简单的网页爬虫流程

  1. 确定爬取对象(网站/页面)
  2. 分析页面内容(目标数据/DOM结构)
  3. 确定开发语言、框架、工具等
  4. 编码 测试,爬取数据
  5. 优化

实战:百度新闻爬虫

确定爬取对象(网站/页面)

百度新闻 (news.baidu.com/

分析页面内容(目标数据/DOM结构)
确定开发语言、框架、工具等

node.js(express) + vscode

编码
  • 安装依赖包

    npm i express superagent cheerio
    

    express (使用express来搭建一个简单的Http服务器。当然,你也可以使用node中自带的http模块) superagent (superagent是node里一个非常方便的、轻量的、渐进式的第三方客户端请求代理模块,用他来请求目标页面) cheerio (cheerio相当于node版的jQuery,用过jQuery的同学会非常容易上手。它主要是用来获取抓取到的页面元素和其中的数据信息)

  1. 使用express启动简易的本地Http服务器

    //index.js
    const express = require('express');
    const app = express();
    
    app.get('/',(req,res)=>{
        res.send('hello world');
    })
    
    let server = app.listen(3000, function () {
      let host = server.address().address;
      let port = server.address().port;
      console.log('Your App is running at http://%s:%s', host, port);
    });
    
    

    访问:http://localhost:3000/ 看到hello world页面

  2. 分析百度新闻首页的新闻信息

    百度新闻首页大体上分为“热点新闻”、“本地新闻”、“国内新闻”、“国际新闻”......等。这次我们先来尝试抓取左侧“热点新闻”和下方的“本地新闻”两处的新闻数据。

    F12打开Chrome的控制台,审查页面元素,经过查看左侧“热点新闻”信息所在DOM的结构,我们发现所有的“热点新闻”信息(包括新闻标题和新闻页面链接)都在id#pane-news<div>下面<ul><li>下的<a>标签中。用jQuery的选择器表示为:#pane-news ul li a

  3. 为了爬取新闻数据,首先我们要用superagent请求目标页面,获取整个新闻首页信息

// 引入所需要的第三方包
const superagent= require('superagent');

let hotNews = [];                                // 热点新闻
let localNews = [];                              // 本地新闻

/**
 * index.js
 * [description] - 使用superagent.get()方法来访问百度新闻首页
 */
superagent.get('http://news.baidu.com/').end((err, res) => {
  if (err) {
    // 如果访问失败或者出错,会这行这里
    console.log(`热点新闻抓取失败 - ${err}`)
  } else {
   // 访问成功,请求http://news.baidu.com/页面所返回的数据会包含在res
   // 抓取热点新闻数据
   hotNews = getHotNews(res)
  }
});
  1. 获取页面信息后,我们来定义一个函数getHotNews()来抓取页面内的“热点新闻”数据。
/**
 * index.js
 * [description] - 抓取热点新闻页面
 */
// 引入所需要的第三方包
const cheerio = require('cheerio');

let getHotNews = (res) => {
  let hotNews = [];
  // 访问成功,请求http://news.baidu.com/页面所返回的数据会包含在res.text中。
  
  /* 使用cheerio模块的cherrio.load()方法,将HTMLdocument作为参数传入函数
     以后就可以使用类似jQuery的$(selectior)的方式来获取页面元素
   */
  let $ = cheerio.load(res.text);

  // 找到目标数据所在的页面元素,获取数据
  $('div#pane-news ul li a').each((idx, ele) => {
    // cherrio中$('selector').each()用来遍历所有匹配到的DOM元素
    // 参数idx是当前遍历的元素的索引,ele就是当前便利的DOM元素
    let news = {
      title: $(ele).text(),        // 获取新闻标题
      href: $(ele).attr('href')    // 获取新闻网页链接
    };
    hotNews.push(news)              // 存入最终结果数组
  });
  return hotNews
};

这里要多说几点:

  1. async/await据说是异步编程的终级解决方案,它可以让我们以同步的思维方式来进行异步编程。Promise解决了异步编程的“回调地狱”,async/await同时使异步流程控制变得友好而有清晰,有兴趣的同学可以去了解学习一下,真的很好用。
  2. superagent模块提供了很多比如getpostdelte等方法,可以很方便地进行Ajax请求操作。在请求结束后执行.end()回调函数。.end()接受一个函数作为参数,该函数又有两个参数error和res。当请求失败,error会包含返回的错误信息,请求成功,error值为null,返回的数据会包含在res参数中。
  3. cheerio模块的.load()方法,将HTML document作为参数传入函数,以后就可以使用类似jQuery的$(selectior)的方式来获取页面元素。同时可以使用类似于jQuery中的.each()来遍历元素。此外,还有很多方法,大家可以自行Google/Baidu
  1. 将抓取的数据返回给前端浏览器
/**
 * [description] - 跟路由
 */
// 当一个get请求 http://localhost:3000时,就会后面的async函数
app.get('/', async (req, res, next) => {
  res.send(hotNews);
});

OK!!这样,一个简单的百度“热点新闻”的爬虫就大功告成啦!!

简单总结一下,其实步骤很简单:

  1. express启动一个简单的Http服务
  2. 分析目标页面DOM结构,找到所要抓取的信息的相关DOM元素
  3. 使用superagent请求目标页面
  4. 使用cheerio获取页面元素,获取目标数据
  5. 返回数据到前端浏览器

现在,继续我们的目标,抓取“本地新闻”数据(编码过程中,我们会遇到一些有意思的问题) 有了前面的基础,我们自然而然的会想到利用和上面相同的方法“本地新闻”数据。

  1. 分析页面中“本地新闻”部分的DOM结构

F12打开控制台,审查“本地新闻”DOM元素,我们发现,“本地新闻”分为两个主要部分,“左侧新闻”和右侧的“新闻资讯”。这所有目标数据都在id#local_newsdiv中。“左侧新闻”数据又在id#localnews-focusul标签下的li标签下的a标签中,包括新闻标题和页面链接。“本地资讯”数据又在id#localnews-zixundiv下的ul标签下的li标签下的a标签中,包括新闻标题和页面链接。

  1. OK!分析了DOM结构,确定了数据的位置,接下来和爬取“热点新闻”一样,按部就班,定义一个getLocalNews()函数,爬取这些数据。
**
 * [description] - 抓取本地新闻页面
 */
let getLocalNews = (res) => {
  let localNews = [];
  let $ = cheerio.load(res);
    
  // 本地新闻
  $('ul#localnews-focus li a').each((idx, ele) => {
    let news = {
      title: $(ele).text(),
      href: $(ele).attr('href'),
    };
    localNews.push(news)
  });
    
  // 本地资讯
  $('div#localnews-zixun ul li a').each((index, item) => {
    let news = {
      title: $(item).text(),
      href: $(item).attr('href')
    };
    localNews.push(news);
  });

  return localNews
};

对应的,在superagent.get()中请求页面后,我们需要调用getLocalNews()函数,来爬去本地新闻数据。 superagent.get()函数修改为:

superagent.get('http://news.baidu.com/').end((err, res) => {
  if (err) {
    // 如果访问失败或者出错,会这行这里
    console.log(`热点新闻抓取失败 - ${err}`)
  } else {
   // 访问成功,请求http://news.baidu.com/页面所返回的数据会包含在res
   // 抓取热点新闻数据
   hotNews = getHotNews(res)
   localNews = getLocalNews(res)
  }
});

同时,我们要在app.get()路由中也要将数据返回给前端浏览器。app.get()路由代码修改为:

/**
 * [description] - 跟路由
 */
// 当一个get请求 http://localhost:3000时,就会后面的async函数
app.get('/', async (req, res, next) => {
  res.send({
    hotNews: hotNews,
    localNews: localNews
  });
});

编码完成,激动不已!!DOS中让项目跑起来,用浏览器访问http://localhost:3000

尴尬的事情发生了!!返回的数据只有热点新闻,而本地新闻返回一个空数组[ ]。检查代码,发现也没有问题,但为什么一直返回的空数组呢?

一个有意思的问题

为了找到原因,首先,我们看看用superagent.get('http://news.baidu.com/').end((err, res) => {})请求百度新闻首页在回调函数.end()中的第二个参数res中到底拿到了什么内容?

// 新定义一个全局变量 pageRes
let pageRes = {};        // supergaent页面返回值

// superagent.get()中将res存入pageRes
superagent.get('http://news.baidu.com/').end((err, res) => {
  if (err) {
    // 如果访问失败或者出错,会这行这里
    console.log(`热点新闻抓取失败 - ${err}`)
  } else {
   // 访问成功,请求http://news.baidu.com/页面所返回的数据会包含在res
   // 抓取热点新闻数据
   // hotNews = getHotNews(res)
   // localNews = getLocalNews(res)
   pageRes = res
  }
});

// 将pageRes返回给前端浏览器,便于查看
app.get('/', async (req, res, next) => {
  res.send({
    // {}hotNews: hotNews,
    // localNews: localNews,
    pageRes: pageRes
  });
});

可以看到,返回值中的text字段应该就是整个页面的HTML代码的字符串格式。为了方便我们观察,可以直接把这个text字段值返回给前端浏览器,这样我们就能够清晰地看到经过浏览器渲染后的页面。

修改给前端浏览器的返回值

app.get('/', async (req, res, next) => {
  res.send(pageRes.text)
}

审查元素才发现,原来我们抓取的目标数据所在的DOM元素中是空的,里面没有数据! 到这里,一切水落石出!在我们使用superagent.get()访问百度新闻首页时,res中包含的获取的页面内容中,我们想要的“本地新闻”数据还没有生成,DOM节点元素是空的,所以出现前面的情况!抓取后返回的数据一直是空数组[ ]

在控制台的Network中我们发现页面请求了一次这样的接口: http://localhost:3000/widget?id=LocalNews&ajax=json&t=1526295667917,接口状态 404。 这应该就是百度新闻获取“本地新闻”的接口,到这里一切都明白了!“本地新闻”是在页面加载后动态请求上面这个接口获取的,所以我们用superagent.get()请求的页面再去请求这个接口时,接口URLhostname部分变成了本地IP地址,而本机上没有这个接口,所以404,请求不到数据。

找到原因,我们来想办法解决这个问题!!

使用第三方npm包,模拟浏览器访问百度新闻首页,在这个模拟浏览器中当“本地新闻”加载成功后,抓取数据,返回给前端浏览器。

使用Nightmare自动化测试工具

Electron可以让你使用纯JavaScript调用Chrome丰富的原生的接口来创造桌面应用。你可以把它看作一个专注于桌面应用的Node.js的变体,而不是Web服务器。其基于浏览器的应用方式可以极方便的做各种响应式的交互

Nightmare是一个基于Electron的框架,针对Web自动化测试和爬虫,因为其具有跟PlantomJS一样的自动化测试的功能可以在页面上模拟用户的行为触发一些异步数据加载,也可以跟Request库一样直接访问URL来抓取数据,并且可以设置页面的延迟时间,所以无论是手动触发脚本还是行为触发脚本都是轻而易举的。

安装依赖

npm i nightware -S

index.js中新增如下代码:

const Nightmare = require('nightmare');          // 自动化测试包,处理动态页面
const nightmare = Nightmare({ show: true });     // show:true  显示内置模拟浏览器

/**
 * [description] - 抓取本地新闻页面
 * [nremark] - 百度本地新闻在访问页面后加载js定位IP位置后获取对应新闻,
 * 所以抓取本地新闻需要使用 nightmare 一类的自动化测试工具,
 * 模拟浏览器环境访问页面,使js运行,生成动态页面再抓取
 */
// 抓取本地新闻页面
nightmare
.goto('http://news.baidu.com/')
.wait("div#local_news")
.evaluate(() => document.querySelector("div#local_news").innerHTML)
.then(htmlStr => {
  // 获取本地新闻数据
  localNews = getLocalNews(htmlStr)
})
.catch(error => {
  console.log(`本地新闻抓取失败 - ${error}`);
})

修改getLocalNews()函数为:

/**
 * [description]- 获取本地新闻数据
 */
let getLocalNews = (htmlStr) => {
  let localNews = [];
  let $ = cheerio.load(htmlStr);

  // 本地新闻
  $('ul#localnews-focus li a').each((idx, ele) => {
    let news = {
      title: $(ele).text(),
      href: $(ele).attr('href'),
    };
    localNews.push(news)
  });

  // 本地资讯
  $('div#localnews-zixun ul li a').each((index, item) => {
    let news = {
      title: $(item).text(),
      href: $(item).attr('href')
    };
    localNews.push(news);
  });

  return localNews
}

修改app.get('/')路由为:

/**
 * [description] - 跟路由
 */
// 当一个get请求 http://localhost:3000时,就会后面的async函数
app.get('/', async (req, res, next) => {
  res.send({
    hotNews: hotNews,
    localNews: localNews
  })
});

好了,大功告成~~~

总结

  1. express启动一个简单的Http服务
  2. 分析目标页面DOM结构,找到所要抓取的信息的相关DOM元
  3. 使用superagent请求目标页面
  4. 动态页面(需要加载页面后运行JS或请求接口的页面)可以使用Nightmare模拟浏览器访问
  5. 使用cheerio获取页面元素,获取目标数据