为了账号安全,请及时绑定邮箱和手机立即绑定

Node爬虫+MongoDB

标签:
Node.js

一、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);

    })

})



作者:RayLightUp
链接:https://www.jianshu.com/p/f3ae68e3eb09


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消