从零学习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 相关文章推荐
javascript淡入淡出效果的实现思路
Mar 31 Javascript
jQuery插件实现表格隔行换色且感应鼠标高亮行变色
Sep 22 Javascript
JS注释所产生的bug 即使注释也会执行
Nov 19 Javascript
使用Javascript实现选择下拉菜单互移并排序
Feb 23 Javascript
jQuery实现图片加载完成后改变图片大小的方法
Mar 29 Javascript
Highcharts入门之基本属性
Aug 02 Javascript
js 判断各种数据类型的简单方法(推荐)
Aug 29 Javascript
AngularJS1.X学习笔记2-数据绑定详解
Apr 01 Javascript
基于对象合并功能的实现示例
Oct 10 Javascript
Javascript 编码约定(编码规范)
Mar 11 Javascript
react实现复选框全选和反选组件效果
Aug 25 Javascript
Node.js fs模块原理及常见用途
Oct 22 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
Ha0k 0.3 PHP 网页木马修改版
2009/10/11 PHP
PHP向浏览器输出内容的4个函数总结
2014/11/17 PHP
php实现等比例不失真缩放上传图片的方法
2016/11/14 PHP
用javascript实现的支持lrc歌词的播放器
2007/05/17 Javascript
IE本地存储userdata的一个bug说明
2010/07/01 Javascript
jquery打开直接跳到网页最下面、最低端实现代码
2013/04/22 Javascript
JQuery中绑定事件(bind())和移除事件(unbind())
2015/02/27 Javascript
深入理解jQuery事件绑定
2016/06/02 Javascript
网页挂马方式整理及详细介绍
2016/11/03 Javascript
JS简单实现移动端日历功能示例
2016/12/28 Javascript
jQuery的中 is(':visible') 解析及用法(必看)
2017/02/12 Javascript
php简单数据库操作类的封装
2017/06/08 Javascript
WebStorm ES6 语法支持设置&amp;babel使用及自动编译(详解)
2017/09/08 Javascript
node.js 用socket实现聊天的示例代码
2017/10/17 Javascript
基于rollup的组件库打包体积优化小结
2018/06/18 Javascript
浅析vue-cli3配置webpack-bundle-analyzer插件【推荐】
2019/10/23 Javascript
原生js实现商品筛选功能
2019/10/28 Javascript
[47:42]Fnatic vs Liquid 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
Python去掉字符串中空格的方法
2014/03/11 Python
Python二维码生成库qrcode安装和使用示例
2014/12/16 Python
python中的闭包用法实例详解
2015/05/05 Python
Python 功能和特点(新手必学)
2015/12/30 Python
Python使用django框架实现多人在线匿名聊天的小程序
2017/11/29 Python
Python操作Sql Server 2008数据库的方法详解
2018/05/17 Python
详解python中的Turtle函数库
2018/11/19 Python
PyQt5实现简易计算器
2020/05/30 Python
浅谈keras中loss与val_loss的关系
2020/06/22 Python
详解Python模块化编程与装饰器
2021/01/16 Python
Giglio英国站:意大利奢侈品购物网
2018/03/06 全球购物
澳大利亚最受欢迎的超级商场每日优惠:Catch
2020/11/17 全球购物
药品营销策划方案
2014/06/15 职场文书
员工工作自我评价
2014/09/26 职场文书
2015年街道除四害工作总结
2015/05/15 职场文书
听证会主持词
2015/07/03 职场文书
Oracle中DBLink的详细介绍
2022/04/29 Oracle
Nginx 常用配置
2022/05/15 Servers