从零学习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 相关文章推荐
html中table数据排序的js代码
Aug 09 Javascript
让人蛋疼的JavaScript语法特性
Sep 30 Javascript
dreamweaver 8实现Jquery自动提示
Dec 04 Javascript
浅谈javascript的分号的使用
May 12 Javascript
基于jQuery的select下拉框选择触发事件实例分析
Nov 18 Javascript
基于JavaScript实现本地图片预览
Feb 08 Javascript
Node.js查找当前目录下文件夹实例代码
Mar 07 Javascript
vue的diff算法知识点总结
Mar 29 Javascript
js实现敏感词过滤算法及实现逻辑
Jul 24 Javascript
vue监听对象及对象属性问题
Aug 20 Javascript
JS html事件冒泡和事件捕获操作示例
May 01 Javascript
JavaScript实现HSL拾色器
May 21 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
WIN8.1下搭建PHP5.6环境
2015/04/29 PHP
Yii框架使用魔术方法实现跨文件调用功能示例
2017/05/20 PHP
js压缩利器
2007/02/20 Javascript
append和appendTo的区别以及appendChild用法
2013/12/24 Javascript
一个非常全面的javascript URL解析函数和分段URL解析方法
2014/04/12 Javascript
javascript编程异常处理实例小结
2015/11/30 Javascript
jQuery遮罩层效果实例分析
2016/01/14 Javascript
JavaScript的ExtJS框架中数面板TreePanel的使用实例解析
2016/05/21 Javascript
javascript self对象使用详解
2016/10/18 Javascript
js实现省市级联效果分享
2017/08/10 Javascript
JS倒计时实例_天时分秒
2017/08/22 Javascript
Js利用Canvas实现图片压缩功能
2017/09/13 Javascript
JS Input里添加小图标的两种方法
2017/11/11 Javascript
微信小程序之分享页面如何返回首页的示例
2018/03/28 Javascript
nodejs nedb 封装库与使用方法示例
2020/02/06 NodeJs
JavaScript canvas动画实现时钟效果
2020/02/10 Javascript
JS监听组合按键思路及实现过程
2020/04/17 Javascript
vue路由分文件拆分管理详解
2020/08/13 Javascript
javascript实现贪吃蛇游戏(娱乐版)
2020/08/17 Javascript
[49:17]DOTA2-DPC中国联赛 正赛 Phoenix vs Dynasty BO3 第三场 1月26日
2021/03/11 DOTA
Python hashlib模块实例使用详解
2019/12/24 Python
python定时截屏实现
2020/11/02 Python
python3 os进行嵌套操作的实例讲解
2020/11/19 Python
Django集成MongoDB实现过程解析
2020/12/01 Python
悦木之源美国官网:Origins美国
2016/08/01 全球购物
迪奥美国官网:Dior美国
2019/12/07 全球购物
Fanatics官网:运动服装、球衣、运动装备
2020/10/12 全球购物
岗位职责怎么写
2014/03/14 职场文书
《蜗牛的奖杯》教后反思
2014/04/24 职场文书
公司授权委托书范文
2014/09/21 职场文书
承租经营合作者协议书
2014/10/01 职场文书
2014年质检员工作总结
2014/11/18 职场文书
英语教师求职信范文
2015/03/20 职场文书
法制工作总结2015
2015/07/23 职场文书
Python机器学习之决策树和随机森林
2021/07/15 Javascript
Python绘制散点图之可视化神器pyecharts
2022/07/07 Python