一、Demo介绍
在每周一次的公司内部分享上,我分享了关于node的一个爬虫的Demo。通过这个Demo,分享了关于Node的web框架Express,以及MongoDB的基础知识。
git地址:https://github.com/rayderay/node-crawler
这个demo启动之后有一个爬虫的展示页面,如下
我这个爬虫爬的是博客园的博文,点击博文进去爬取文字对应博主的昵称、园龄、粉丝、和关注,将这四个字段爬取出来,存入到数据库中。
展示页面左边部分,可以自行输入需要爬取的文章数(博客园博文列表每一页有20条数据,所以输入的需要是20的整数倍),点击开始爬虫即进行爬虫,右边表格展示爬虫的过程的分析数据。
在爬取的过程中,将我们所需的四个字段进行分析和存储,点击展示MongoDB的数据,将数据库中的数据进行展示。
git down之后项目启动步骤
1)进入代码目录之后npm install
2)开启MongoDB监听
MongoDB的配置文件在mongoose.js中,将如下数据库地址代码改为本地的数据库地址连接即可。
mongoose.connect('mongodb://localhost:27017/crawler');
之后在本地开启数据库监听。
3)npm start
二、爬虫
1、分析页面并获取文章入口
查看博客园的页面我们发现,有200页,每一页对应的有20条博文。查看接口请求我们可以发现分页接口请求地址是如图这个,通过测试我们可以发现可以通过get请求请求分页信息。
分页请求接口:http://www.cnblogs.com/?CategoryId=808&CategoryType=%22SiteHome%22&ItemListActionName=%22PostList%22&PageIndex=15&ParentCategoryId=0 。通过pageIndex来控制第几页的信息。
再继续分析可以得出,我们需要的文章列表入口保留在分页接口返回的html之中。
所以第一步,我们分析前端页面传来的文章的数目,根据数目分析得出我们需要爬取的文章页数。然后再请求分页接口,将接口返回的数据进行删选,将每一页对应的20条博文地址保存在全局变量当中。
核心代码如下。
var baseUrl = 'http://www.cnblogs.com/?CategoryId=808&CategoryType=%22SiteHome%22&ItemListActionName=%22PostList%22&PageIndex=';
var pageUrls = [],//收集文章页面网站
var pageNum = req.body.pageNum;
for( var _i = 1; _i <= pageNum ; _i++){ //存储分页列表接口
pageUrls.push(baseUrl + _i + '&ParentCategoryId=0');
};
pageUrls.forEach(function(pageUrl){
superagent.get(pageUrl)
.end(function(err,pres){
console.log('fetch ' + pageUrl + ' successful');
//res.write('fetch ' + pageUrl + ' successful
');// 常规的错误处理
if (err) {
console.log(err);
}
// pres.text 里面存储着请求返回的 html 内容
var $ = cheerio.load(pres.text);
var curPageUrls = $('.titlelnk');
for(var i = 0 ; i < curPageUrls.length ; i++){
var articleUrl = curPageUrls.eq(i).attr('href');//筛选出博文入口地址
urlsArray.push(articleUrl);
// 相当于一个计数器
ep.emit('BlogArticleHtml', articleUrl);
}
})
});
在这里我们用到了三个中间件,superagent、cheerio、eventproxy。
2、 使用中间件控制异步并发数量,爬取具体的页面内容
我们拥有了20*n个博文url地址,再分析页面,我们发现我们所需要的四个字段,存在于如下这个请求中,还好我们在博文的url地址中发现了我们所需要的参数,所以接下来我们需要做的就是,分析这些url地址,获取所需要的参数然后再异步发送请求。
在这里我们使用的控制异步的中间件是async,在express中同样有很多的处理异步的中间件,诸如axios等。
我们使用async的mapLimit方法,将并发数控制在5。
var reptileMove = function(url,callback){
var delay = parseInt((Math.random() * 30000000) % 1000, 10);
curCount++;
console.log('现在的并发数是', curCount, ',正在抓取的是', url, ',耗时' + delay + '毫秒');
superagent.get(url)
.end(function(err,sres){
if (err) {
console.log(err);
return;
}
//sres.text 里面存储着请求返回的 html 内容
var $ = cheerio.load(sres.text);
//收集数据
//1、收集用户个人信息,昵称、园龄、粉丝、关注
var currentBlogApp = url.split('/p/')[0].split('/')[3],
requestId = url.split('/p/')[1].split('.')[0];
console.log('currentBlogApp is '+ currentBlogApp + '\n' + 'requestId id is ' + requestId);
//res.write('the article title is :'+$('title').text() +'
');var flag = isRepeat(currentBlogApp);
if(!flag){
var appUrl = "http://www.cnblogs.com/mvc/blog/news.aspx?blogApp="+ currentBlogApp;
personInfo(appUrl);
};
});
setTimeout(function() {
curCount--;
callback(null,url +'Call back content');
}, delay);
};
// 抓取昵称、入园年龄、粉丝数、关注数
function personInfo(url){
var infoArray = {};
superagent.get(url)
.end(function(err,ares){
if (err) {
console.log(err);
return;
}
var $ = cheerio.load(ares.text),
info = $('#profile_block a'),
len = info.length,
joinData = "",
flag = false,
curDate = new Date();
// 小概率异常抛错
try{
joinData = "20"+(info.eq(1).attr('title').split('20')[1]);
}
catch(err){
console.log(err);
joinData = "2012-11-06";
}
infoArray.name = info.eq(0).text();
infoArray.joinData = parseInt((new Date() - new Date(joinData))/1000/60/60/24);
if(len == 4){
infoArray.fans = info.eq(2).text();
infoArray.focus = info.eq(3).text();
}else if(len == 5){// 博客园推荐博客
infoArray.fans = info.eq(3).text();
infoArray.focus = info.eq(4).text();
}
//console.log('用户信息:'+JSON.stringify(infoArray));
catchDate.push(infoArray);
});
}
根据重新拼接出来的url,再创建一个获取我们所需要的四个字段的函数,将我们需要的结果存在catchDate这个字段中。
3、使用mongoose创建表,在express中进行实例化
我们要将博主的昵称、园龄、粉丝、和关注这四个字段存储在数据库当中,首先创建一个schema。schema是mongoose里会用到的一种数据模式,每个schema会映射到MongoDB中的collection。
我们在mongoose.js文件中声明了数据库的基本配置,接下来我们需要在app.js这个文件中对数据库进行引用。
var mongoose = require('./mongoose');
var db = mongoose();
在这里有一个坑就是,我们必须在别的中间件使用数据库之前对数据库进行引用,不然的话就会报找不到对应schema的错误。
然后在model目录底下创建一个crawler.server.model.js,声明schema。
var crawlerListSchema = new mongoose.Schema({
name: String ,
joinData: Number,
fans: String ,
focus: String
})
var CrawlerList = mongoose.model('crawlerList', crawlerListSchema,'CrawlerList');
4、对数据进行分析,将数据存储在MongoDB中
我们使用async的mapLimit将全部的接口请求完成之后,catchDate传递给回调函数,在回调函数中对数据进行最终的处理和存储。
async.mapLimit(articleUrls, 5 ,function (url, callback) {
reptileMove(url, callback);
}, function (err,result) {
EndDate = new Date().getTime();
console.log('final:');
console.log(catchDate);
var len = catchDate.length,
aveData = 0,
aveFans = 0,
aveFocus = 0;
for(var i=0 ; i
var eachDate = JSON.stringify(catchDate[i]),
eachDateJson = catchDate[i];
var newlist = new CrawlerList();
newlist = catchDate[i];
//存入数据库
CrawlerList.create(newlist,(err) => {
if(err) return console.log(err);
})
// 小几率取不到值则赋默认值
eachDateJsonFans = eachDateJson.fans || 110;
eachDateJsonFocus = eachDateJson.focus || 11;
aveData += parseInt(eachDateJson.joinData);
aveFans += parseInt(eachDateJsonFans);
aveFocus += parseInt(eachDateJsonFocus);
}
var startDate = moment(StartDate);
var endDate = moment(EndDate);
var costTime = endDate.diff(startDate)
var result = {
succeed: true,
errorCode: '0000000',
errorMessage: '成功',
data:{
startDate: moment(StartDate).format('YYYY-MM-DD HH:mm:ss.SS'),
endDate: moment(EndDate).format('YYYY-MM-DD HH:mm:ss.SS'),
costTime: costTime/1000+'s',
pageNum: pageNum * 20,
len: len,
joinData: Math.round(aveData/len*100)/100,
aveFans: Math.round(aveFans/len*100)/100,
aveFocus: Math.round(aveFocus/len*100)/100
}
}
res.json(result)
});
});
5. 书写路由,使用Node完成获取数据库中数据的函数
在处理请求的crawler.js中声明数据库schema。
var CrawlerList = mongoose.model('crawlerList');
因为展示的是数据库里面的全部的数据,没有任何的查询条件,所以直接使用find()方法,将全部的数据返回给前端展示。
router.get('/dbList',function(req,res,next){
CrawlerList.find({}, function(err, docs){
if(err){
res.end('Error');
return next();
}
res.json(docs);
})
})