Node爬虫+MongoDB

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

    })

})

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,737评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,103评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,710评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,909评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,794评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,557评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,939评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,572评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,852评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,871评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,692评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,490评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,939评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,114评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,409评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,971评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,176评论 2 339

推荐阅读更多精彩内容

  • 参考资料https://www.npmjs.com/package/mongodbhttps://docs.mon...
    程序员有话说阅读 747评论 0 4
  • 个人博客:https://yeaseonzhang.github.io 花了半个多月的时间,终于又把“JS红宝书”...
    Yeaseon阅读 1,713评论 2 23
  • 个人入门学习用笔记、不过多作为参考依据。如有错误欢迎斧正 目录 简书好像不支持锚点、复制搜索(反正也是写给我自己看...
    kirito_song阅读 2,439评论 1 37
  • Node.js 常用工具 util 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心JavaScr...
    FTOLsXD阅读 529评论 0 2
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,605评论 2 41