从零学习node.js之简易的网络爬虫(四)


Posted in Javascript onFebruary 22, 2017

前言

之前已经介绍了node.js的一些基本知识,下面这篇文章我们的目标是学习完本节课程后,能进行网页简单的分析与抓取,对抓取到的信息进行输出和文本保存。

爬虫的思路很简单:

  1. 确定要抓取的URL;
  2. 对URL进行抓取,获取网页内容;
  3. 对内容进行分析并存储;
  4. 重复第1步

在这节里做爬虫,我们使用到了两个重要的模块:

  • request : 对http进行封装,提供更多、更方便的接口供我们使用,request进行的是异步请求。更多信息可以去这篇文章上进行查看
  • cheerio : 类似于jQuery,可以使用$(), find(), text(), html()等方法提取页面中的元素和数据,不过若仔细比较起来,cheerio中的方法不如jQuery的多。

一、 hello world

说是hello world,其实首先开始的是最简单的抓取。我们就以cnode网站为例(https://cnodejs.org/),这个网站的特点是:

  1. 不需要登录即可访问首页和其他页面
  2. 页面都是同步渲染的,没有异步请求的问题
  3. DOM结构清晰

代码如下:

var request = require('request'),
 cheerio = require('cheerio');

request('https://cnodejs.org/', function(err, response, body){
 if( !err && response.statusCode == 200 ){
 // body为源码
 // 使用 cheerio.load 将字符串转换为 cheerio(jQuery) 对象,
 // 按照jQuery方式操作即可
 var $ = cheerio.load(body);
 
 // 输出导航的html代码
 console.log( $('.nav').html() );
 }
});

这样的一段代码就实现了一个简单的网络爬虫,爬取到源码后,再对源码进行拆解分析,比如我们要获取首页中第1页的 问题标题,作者,跳转链接,点击数量,回复数量。通过chrome,我们可以得到这样的结构:

每个div[.cell]是一个题目完整的单元,在这里面,一个单元暂时称为$item

{
 title : $item.find('.topic_title').text(),
 url : $item.find('.topic_title').attr('href'),
 author : $item.find('.user_avatar img').attr('title'),
 reply : $item.find('.count_of_replies').text(),
 visits : $item.find('.count_of_visits').text()
}

因此,循环div[.cell] ,就可以获取到我们想要的信息了:

request('https://cnodejs.org/?_t='+Date.now(), function(err, response, body){
 if( !err && response.statusCode == 200 ){
 var $ = cheerio.load(body);

 var data = [];
 $('#topic_list .cell').each(function(){
  var $this = $(this);
 
 // 使用trim去掉数据两端的空格
  data.push({
  title : trim($this.find('.topic_title').text()),
  url : trim($this.find('.topic_title').attr('href')),
  author : trim($this.find('.user_avatar img').attr('title')),
  reply : trim($this.find('.count_of_replies').text()),
  visits : trim($this.find('.count_of_visits').text())
  })
 });
 // console.log( JSON.stringify(data, ' ', 4) );
 console.log(data);
 }
});

// 删除字符串左右两端的空格
function trim(str){ 
 return str.replace(/(^\s*)|(\s*$)/g, "");
}

二、爬取多个页面

上面我们只爬取了一个页面,怎么在一个程序里爬取多个页面呢?还是以CNode网站为例,刚才只是爬取了第1页的数据,这里我们想请求它前6页的数据(别同时抓太多的页面,会被封IP的)。每个页面的结构是一样的,我们只需要修改url地址即可。

2.1 同时抓取多个页面

首先把request请求封装为一个方法,方便进行调用,若还是使用console.log方法的话,会把6页的数据都输出到控制台,看起来很不方便。这里我们就使用到了上节文件操作内容,引入fs模块,将获取到的内容写入到文件中,然后新建的文件放到file目录下(需手动创建file目录):

// 把page作为参数传递进去,然后调用request进行抓取
function getData(page){
 var url = 'https://cnodejs.org/?tab=all&page='+page;
 console.time(url);
 request(url, function(err, response, body){
 if( !err && response.statusCode == 200 ){
  console.timeEnd(url); // 通过time和timeEnd记录抓取url的时间

  var $ = cheerio.load(body);

  var data = [];
  $('#topic_list .cell').each(function(){
  var $this = $(this);

  data.push({
   title : trim($this.find('.topic_title').text()),
   url : trim($this.find('.topic_title').attr('href')),
   author : trim($this.find('.user_avatar img').attr('title')),
   reply : trim($this.find('.count_of_replies').text()),
   visits : trim($this.find('.count_of_visits').text())
  })
  });
  // console.log( JSON.stringify(data, ' ', 4) );
  // console.log(data);
  var filename = './file/cnode_'+page+'.txt';
  fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
  console.log( filename + ' 写入成功' );
  })
 }
 });
}

CNode分页请求的链接:https://cnodejs.org/?tab=all&page=2,我们只需要修改page的值即可:

var max = 6;
for(var i=1; i<=max; i++){

 getData(i);
}

这样就能同时请求前6页的数据了,执行文件后,会输出每个链接抓取成功时消耗的时间,抓取成功后再把相关的信息写入到文件中:

$ node test.js
开始请求...
https://cnodejs.org/?tab=all&page=1: 279ms
./file/cnode_1.txt 写入成功
https://cnodejs.org/?tab=all&page=3: 372ms
./file/cnode_3.txt 写入成功
https://cnodejs.org/?tab=all&page=2: 489ms
./file/cnode_2.txt 写入成功
https://cnodejs.org/?tab=all&page=4: 601ms
./file/cnode_4.txt 写入成功
https://cnodejs.org/?tab=all&page=5: 715ms
./file/cnode_5.txt 写入成功
https://cnodejs.org/?tab=all&page=6: 819ms
./file/cnode_6.txt 写入成功

我们在file目录下就能看到输出的6个文件了。

2.2 控制同时请求的数量

我们在使用for循环后,会同时发起所有的请求,如果我们同时去请求100、200、500个页面呢,会造成短时间内对服务器发起大量的请求,最后就是被封IP。这里我写了一个调度方法,每次同时最多只能发起5个请求,上一个请求完成后,再从队列中取出一个进行请求。

/*
 @param data [] 需要请求的链接的集合
 @param max num 最多同时请求的数量
*/
function Dispatch(data, max){
 var _max = max || 5, // 最多请求的数量
 _dataObj = data || [], // 需要请求的url集合
 _cur = 0, // 当前请求的个数
 _num = _dataObj.length || 0,
 _isEnd = false,
 _callback;

 var ss = function(){
 var s = _max - _cur;
 while(s--){
  if( !_dataObj.length ){
  _isEnd = true;
  break;
  }
  var surl = _dataObj.shift();
  _cur++;

  _callback(surl);
 }
 }

 this.start = function(callback){
 _callback = callback;

 ss();
 },

 this.call = function(){
 if( !_isEnd ){
  _cur--;
  ss();
 }
 }
}

var dis = new Dispatch(urls, max);
dis.start(getData);

然后在 getData 中,写入文件的后面,进行dis的回调调用:

var filename = './file/cnode_'+page+'.txt';
fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
 console.log( filename + ' 写入成功' );
})
dis.call();

这样就实现了异步调用时控制同时请求的数量。

三、抓取需要登录的页面

比如我们在抓取CNode,百度贴吧等一些网站,是不需要登录就可以直接抓取的,那么如知乎等网站,必须登录后才能抓取,否则直接跳转到登录页面。这种情况我们该怎么抓取呢?

使用cookie。 用户登录后,都会在cookie中记录下用户的一些信息,我们在抓取一些页面,带上这些cookie,服务器就会认为我们处于登录状态,程序就能抓取到我们想要的信息。

先在浏览器上登录我们的帐号,然后在console中使用document.domain获取到所有cookie的字符串,复制到下方程序的cookie处(如果你知道哪些cookie不需要,可以剔除掉)。

request({
 url:'https://www.zhihu.com/explore',
 headers:{
 // "Referer":"www.zhihu.com"
 cookie : xxx
 }
}, function(error, response, body){
 if (!error && response.statusCode == 200) {
 // console.log( body );
 var $ = cheerio.load(body);

 
 }
})

同时在request中,还可以设定referer,比如有的接口或者其他数据,设定了referer的限制,必须在某个域名下才能访问。那么在request中,就可以设置referer来进行伪造。

四、保存抓取到的图片

页面中的文本内容可以提炼后保存到文本或者数据库中,那么图片怎么保存到本地呢。

图片可以使用request中的pipe方法输出到文件流中,然后使用fs.createWriteStream输出为图片。

这里我们把图片保存到以日期创建的目录中,mkdirp可一次性创建多级目录(./img/2017/01/22)。保存的图片名称,可以使用原名称,也可以根据自己的规则进行命名。

var request = require('request'),
 cheerio = require('cheerio'),
 fs = require('fs'),
 path = require('path'), // 用于分析图片的名称或者后缀名
 mkdirp = require('mkdirp'); // 用于创建多级目录

var date = new Date(),
 year = date.getFullYear(),
 month = date.getMonth()+1,
 month = ('00'+month).slice(-2), // 添加前置0
 day = date.getDate(),
 day = ('00'+day).slice(-2), // 添加前置0
 dir = './img/'+year+'/'+month+'/'+day+'/';

// 根据日期创建目录 ./img/2017/01/22/
var stats = fs.statSync(dir);
if( stats.isDirectory() ){
 console.log(dir+' 已存在');
}else{
 console.log('正在创建目录 '+dir);
 mkdirp(dir, function(err){
 if(err) throw err;
 })
}

request({
 url : 'http://desk.zol.com.cn/meinv/?_t='+Date.now()
}, function(err, response, body){
 if(err) throw err;

 if( response.statusCode == 200 ){
 var $ = cheerio.load(body);
 
 $('.photo-list-padding img').each(function(){
  var $this = $(this),
  imgurl = $this.attr('src');
  
  var ext = path.extname(imgurl); // 获取图片的后缀名,如 .jpg, .png .gif等
  var filename = Date.now()+'_'+ parseInt(Math.random()*10000)+ext; // 命名方式:毫秒时间戳+随机数+后缀名
  // var filename = path.basename(imgurl); // 直接获取图片的原名称
  // console.log(filename);
  download(imgurl, dir+filename); // 开始下载图片
 })
 }
});

// 保存图片
var download = function(imgurl, filename){
 request.head(imgurl, function(err, res, body) {
 request(imgurl).pipe(fs.createWriteStream(filename));
 console.log(filename+' success!');
 });
}

在对应的日期目录里(如./img/2017/01/22/),就可以看到下载的图片了。

总结

我们这里只是写了一个简单的爬虫,针对更复杂的功能,则需要更复杂的算法的来控制了。还有如何抓取ajax的数据,我们会在后面进行讲解。以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,小编还会继续分享关于node入门学习的文章,感兴趣的朋友们请继续关注三水点靠木。

Javascript 相关文章推荐
js跨浏览器实现将字符串转化为xml对象的方法
Sep 25 Javascript
jquery.autocomplete修改实现键盘上下键自动填充示例
Nov 19 Javascript
简介JavaScript中Math.cos()余弦方法的使用
Jun 15 Javascript
详解JavaScript正则表达式之分组匹配及反向引用
Mar 09 Javascript
使用jQuery操作HTML的table表格的实例解析
Mar 13 Javascript
详解JavaScript设计模式开发中的桥接模式使用
May 18 Javascript
JavaScript学习小结之被嫌弃的eval函数和with语句实例详解
Aug 01 Javascript
使用微信小程序开发前端【快速入门】
Dec 05 Javascript
js模拟微博发布消息
Feb 23 Javascript
在页面中引入js的两种方法(推荐)
Aug 29 Javascript
微信小程序开发注意指南和优化实践(小结)
Jun 21 Javascript
微信小程序实现轮播图指示器
Jun 25 Javascript
js中document.referrer实现移动端返回上一页
Feb 22 #Javascript
基于JS实现bookstore静态页面的实例代码
Feb 22 #Javascript
angular 动态组件类型详解(四种组件类型)
Feb 22 #Javascript
javascript 使用正则test( )第一次是 true,第二次是false
Feb 22 #Javascript
JavaScript实现256色转灰度图
Feb 22 #Javascript
在javascript中,null>=0 为真,null==0却为假,null的值详解
Feb 22 #Javascript
微信小程序 扎金花简单实例
Feb 21 #Javascript
You might like
Notice: Undefined index: page in E:\PHP\test.php on line 14
2010/11/02 PHP
用Simple Excel导出xls实现方法
2012/12/06 PHP
php获取文件类型和文件信息的方法
2015/07/10 PHP
laravel5使用freetds连接sql server的方法
2018/12/07 PHP
JS的replace方法详细介绍
2012/11/09 Javascript
jQuery遍历对象、数组、集合实例
2014/11/08 Javascript
jQuery取消ajax请求的方法
2015/06/09 Javascript
浅谈bootstrap源码分析之scrollspy(滚动侦听)
2016/06/06 Javascript
关于JS变量和作用域详解
2016/07/28 Javascript
JavaScript自动点击链接 防止绕过浏览器访问的方法
2017/01/19 Javascript
vue Render中slots的使用的实例代码
2017/07/19 Javascript
在Vue中用canvas实现二维码和图片合成海报的方法
2019/06/10 Javascript
layui插件表单验证提交触发提交的例子
2019/09/09 Javascript
js实现3D旋转效果
2020/08/18 Javascript
详解Python的Django框架中的templates设置
2015/05/11 Python
python 出现SyntaxError: non-keyword arg after keyword arg错误解决办法
2017/02/14 Python
浅谈Pandas:Series和DataFrame间的算术元素
2018/12/22 Python
Python partial函数原理及用法解析
2019/12/11 Python
python nohup 实现远程运行不宕机操作
2020/04/16 Python
前端面试必备之CSS3的新特性
2017/09/05 HTML / CSS
Max&Co官网:意大利年轻女性时尚品牌
2017/05/16 全球购物
JSF界面控制层技术
2013/06/17 面试题
演讲稿怎么写才完美
2014/01/02 职场文书
情人节活动策划方案
2014/02/27 职场文书
英语教师自荐信
2014/05/26 职场文书
食品工程专业求职信
2014/06/15 职场文书
园林系毕业生求职信
2014/06/23 职场文书
竞选大学学委演讲稿
2014/09/13 职场文书
我为党旗添光彩演讲稿
2014/09/13 职场文书
县人大领导班子四风对照检查材料思想汇报
2014/10/09 职场文书
2015年教师党员公开承诺书
2015/01/22 职场文书
汽车转让协议书
2015/01/29 职场文书
2015年秋季小学开学标语
2015/07/16 职场文书
2019年七夕情人节浪漫祝福语大全!
2019/08/08 职场文书
浅谈redis的过期时间设置和过期删除机制
2022/03/18 MySQL
Spring Data JPA框架自定义Repository接口
2022/04/28 Java/Android