Node-Spider

基于Node.js的爬虫项目

实现的最终结果:通过爬取https://www.cnblogs.com/里面的文章获取对应作者的相关信息。
目的:学习node搭建服务器、分析网页请求并模拟请求、熟悉http协议相关、node写入文件等功能

项目文件结构说明

index.js:项目的启动(入口)文件
server.js:主文件
package.json:模块依赖文件及相关配置

搭建node服务器

默认开始本步骤前已经安装好node环境

server.js:

引入node内置的http模块:


const http = require('http');

创建一个简单的node服务器:


// 指定监听的port及hostname

const listenPort = 3000;
const listenHostName = '127.0.0.1';

http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/plain;charset=utf-8'});
    response.end("hello world\n");
}).listen(listenPort, listenHostName, () => {
    console.log(`Server running at http:// ${listenHostName}:${listenPort}/`);
});

测试服务器是否搭建成功:在命令行进入相应的目录文件,使用node server.js命令启动服务器,然后浏览器打开127.0.0.1:3000访问,网页出现hello world即表示服务器创建成功。

使用到的模块

node内置模块

  • http:用来搭建服务器
  • fs: 文件系统,用来写入和读取文件
  • url:针对url处理

使用方法:代码中使用require()直接引入。

第三方模块

  • eventproxy:控制并发
  • async:控制并发请求的数量(防止被封号)
  • superagent:实现客户端请求代理(类似封装的ajax请求)
  • cheerio:针对html片段实现jquery功能操作

使用方法:先安装到项目中,然后在代码中使用require()引入。

安装方法:

先通过npm init来生成package.json文件。

然后通过npm 安装依赖模块的方法来安装对应的模块:


npm install eventproxy --save-dev

下面列出相关模块使用学习参考链接:

  1. 使用eventproxy控制并发。参考理解

  2. 使用async控制并发请求数量防止被封IP。参考理解

  3. 使用superagent实现客户端请求代理模块。参考理解

  4. 使用cheerio实现将请求回来的html片段实现可以类似jquery使用的功能。参考理解

思考: eventproxyasyncpromise的异同?

具体代码展示

index.js:


/**
 * @author lupinggan
 * @description 项目启动文件
 */

const server = require('./server.js');
server.start();

server.js:


/**
 * @author lupinggan
 * @description node爬虫服务器文件-一次性抓取很多文章获取相关信息
 * 
 */

// 引入node内置的http模块

const http = require('http');

// 引入node的文件系统模块

const fs = require('fs');

// 引入node内置的url模块

const url = require('url');

// 引入eventproxy来控制并发

const eventproxy = require('eventproxy');

// 引入async模块来控制并发的请求数量以防止被封号

const async = require('async');

// 引入superagent来实现客户端请求代理

const superagent = require('superagent');

// 引入cheerio实现jquery功能操作

const cheerio = require('cheerio');

// 指定监听的port及hostname

const listenPort = 3000;
const listenHostName = '127.0.0.1';

// 定义爬取的入口地址

const catchFirstUrl = 'http://www.cnblogs.com/';

// 定义相关变量来存储数据

const urlsArry = [], // 需要爬取的网址(每篇文章访问的url)
      deleteRepeat = {} , // 用来存储作者姓名的字典
      catchData = [], // 存放爬取数据
      pageUrls = [], // 存放收集文章页面网站
      pageNum = 5, // 要爬取的文章的页数
      singlePagePostNum = 20, // 单页面的文章数量
      startDate = new Date(), // 开始时间
      endDate = false; // 结束时间

// 实例化eventproxy()

const ep = new eventproxy();

for(let i=0; i < pageNum; i++){
    // 通过抓包工具分析,每页的文章列表数据获取是通过ajax post方式获取到的
    pageUrls.push("https://www.cnblogs.com/mvc/AggSite/PostList.aspx");
    // pageUrls.push("https://www.cnblogs.com?CategoryId=808&CategoryType='SiteHome'&ItemListActionName='PostList'&PageIndex="+ (i + 1) +"&ParentCategoryId=0");
    // pageUrls.push("https://www.cnblogs.com/mvc/AggSite/PostList.aspx?CategoryId=808&CategoryType='SiteHome'&ItemListActionName='PostList'&PageIndex="+ (i + 1) +"&ParentCategoryId=0");
}

// 创建服务器

// http.createServer(function (request, response) {
//     response.writeHead(200, {'Content-Type': 'text/plain;charset=utf-8'});
//     response.end("这里是使用node开发的爬虫 hello world\n");
// }).listen(listenPort, listenHostName, () => {
//     console.log(`Server running at http:// ${listenHostName}:${listenPort}/`);
// });

//  判断是否有重复的作者

const isAuthorRepeat = (authorName) => {
    if(deleteRepeat[authorName] == undefined ) {
        // 说明字典表中还没有该作者的信息
        deleteRepeat[authorName] = 1;
        return 0;
    }else if(deleteRepeat[authorName] == 1) {
        // 说明字典表中已经存在该作者的信息
        return 1;
    }
}

// 作者详细信息获取

const personInfo = (url) => {

    // 存放作者相关信息
    const infoObj = {};
    superagent.get(url)
    .end(function(err,res) {
        if(err){
            console.error(err);
            return;
        }
        const $ = cheerio.load(res.text),
              info = $('#profile_block a');

        infoObj.name = info.eq(0).text();
        infoObj.age = info.eq(1).text();
        if (info.length == 4) {
            infoObj.fans = info.eq(2).text();
            infoObj.focus = info.eq(3).text();
            infoObj.honour = 0
        } else if (info.length == 5) {
            infoObj.fans = info.eq(3).text();
            infoObj.focus = info.eq(4).text();
            infoObj.honour = 1;
        }
        catchData.push(infoObj);
    })
}

// 主程序

const start = () => {
    // 创建服务器
    http.createServer(function (request, response) {
        
        // 浏览器一次刷新会导致这里请求两次,原因是浏览器会默认一次请求favicon.ico(网页标签上的那个小图标)
        // console.log(url.parse(request.url));
        if(url.parse(request.url).path == '/favicon.ico'){
            return;
        }
        // 设置字符编码防止出现中文乱码
        response.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});
        // 控制并发,当所有的请求完成后,触发下面的函数
        ep.after('BlogArticleHtml', pageUrls.length*singlePagePostNum, function(articleUrls) {
            // articleUrls是一个数组,是通过ep.emit传过来的的articleUrl数组集合

            // 打印输出结果
            // response.write('输出结果:');
            // response.write('<br/>');
            // response.write('共' + articleUrls.length +'篇文章<br/>');
            // for(let i = 0; i < articleUrls.length; i++){
            //     response.write('第' + i + '篇:' + articleUrls[i] + '<br/>');
            // }

            // 对数组进行检查--去重处理
            // 通过针对爬取到的数组去重处理,发现通过"https://www.cnblogs.com/#p" + i不能实际爬取到所有真正的blog地址,因为我们需要对请求使用第三方抓包工具处理分析
            const _articleUrls = articleUrls.filter((currentvalue, index, arr) => {
                return arr.indexOf(currentvalue) === index;
            });
            response.write('输出结果:');
            response.write('<br/>');
            response.write('共' + _articleUrls.length +'篇文章<br/>');
            // for(let i = 0; i < _articleUrls.length; i++) {
            //     response.write('第' + i + '篇:' + _articleUrls[i] + '<br/>');
            // }

            // 对爬取回来的所以的文章url地址进行请求,进而获取需要的作者信息 
            // 由于爬取回来的articleUrls数组长度可能会非常大。因此当我们针对其中的每个具体地址去发送请求的过程中,需要
            // 控制并发的请求数量,以防止被封号或者封ID

            // 我们这里使用async模块来控制并发的数量,详细使用请参考:
            // github地址:https://github.com/caolan/async
            // 使用demo: https://github.com/alsotang/async_demo/blob/master/map.js
            // mapLimit(arr, limit, iterator, callback)
            
            // 控制并发数
            let curCount = 0;

            async.mapLimit(_articleUrls, 5, function(item, callback) {
        
                // 定义延迟时间
                // const delay = parseInt(2000);
                const delay = parseInt((Math.random() * 3000000) % 1000, 10);
                curCount++;
                console.log('现在的并发数是:' + curCount + '---正在抓取的是:'+ item + '延迟' + delay);
                superagent.get(item)
                .end(function(err,res){
                    // 请求错误处理
                    if(err){
                        console.error(err);
                        return;
                    }
                    
                    const $ = cheerio.load(res.text);
                    // 收集每篇文章的信息
                    const currentBlogApp = item.split('/p/')[0].split('/')[3],
                          requestId = item.split('/p/')[1].split('.')[0];
                    
                    // 这里还是使用response而不是superagent返回的res来输出
                    response.write('当前博客:' + currentBlogApp + ',' + '请求的id: '+ requestId +'<br/>');
                    response.write('当前的文章题目:'+ $('title').text() +'<br/>');

                    // 检测是否有重名-针对同一个人,他的信息获取一次就够了
                    const flag = isAuthorRepeat(currentBlogApp);
                    if(!flag){
                        // 通过抓包分析,拼接用于获取作者个人信息的url
                        const appUrl = "http://www.cnblogs.com/mvc/blog/news.aspx?blogApp="+ currentBlogApp;
                        // 博客作者详细信息获取
                        personInfo(appUrl);
                    }
                });

                setTimeout(function() {
                    curCount--;
                    callback(null, item + '请求内容');
                },delay);

            },function(err, result) {
                console.log(result);
                console.log('----------------');
                console.log(catchData);
                // 实时写入文件

                fs.writeFile('data.json', JSON.stringify(catchData), 'utf-8', (err) => {
                    if(err) {
                        console.error('写入文件有误');
                    }
                });
                // appendFile是往文件中添加,不会覆盖
                // fs.appendFile('data.json', JSON.stringify(catchData), 'utf-8', (err) => {
                //     if(err) {
                //         console.error('写入文件有误');
                //     }
                // })
            })

            
            // 结束客户端等待状态
            // response.end();
        });
        
        pageUrls.map(function(currentvalue, index, arr) {
            // 根据对文章列表的抓包分析,每页获取的blog列表数据是通过aja post请求获得的
            // superagent.get(currentvalue)
            superagent.post(currentvalue)
            .send({
                CategoryId: 808,
                CategoryType: 'SiteHome',
                ItemListActionName: 'PostList',
                PageIndex: index+1,
                ParentCategoryId: 0
            })
            .end(function(err, res) {
                if(err){
                    console.error("爬取总页数时错误:" + err);
                    return;
                }
                // res.text存放着请求返回的未解析的html
                // res还包含其他的返回属性相关请查看http://cnodejs.org/topic/5378720ed6e2d16149fa16bd
                // 为什么是使用res.text?是因为superagent是这么设计的。。
                // 将返回的html片段通过使用cheerio.load()加载后,可以类似使用jquery的方式来获取相关元素
                const $=cheerio.load(res.text);
                // 获取每一页上文章的url
                const curPageUrls = $('.titlelnk');
                for(let i = 0; i < curPageUrls.length; i++) {
                    const articleUrl = curPageUrls.eq(i).attr('href');
                    urlsArry.push(articleUrl);
                    // 使用eventproxy模块来控制并发
                    // 每执行完一次就执行一次类似计数器加一的效果
                    // 将每次的articleUrl作为参数传递给ep
                    ep.emit('BlogArticleHtml', articleUrl);
                }
            });
        });
        console.log(pageUrls.length*singlePagePostNum);

    }).listen(listenPort, listenHostName, () => {
        console.log(`Server running at http:// ${listenHostName}:${listenPort}/`);
    });
}

// 导出该模块的接口 CommonJS规范

exports.start = start;

后续还将实现的功能:

现在已经将爬取到的数据存到了文件中,后续希望能实现将数据读取出来进行处理分析在页面以图表的形式展现出来。同时希望能做到在页面进行相关操作,从而动态获取数据动态展示数据。

本爬虫的实现是参考原作者的示例来学习的。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,657评论 18 139
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,615评论 2 41
  • 总体架构: node-agent TARS框架中Node.js程序启动器,提供生产环境所需的服务属性。 deplo...
    宫若石阅读 4,720评论 0 1
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 4,072评论 2 58
  • 因为要准备清明节请假回家一趟~今天的画有点简单了哈~室友说,为了表达诚意,应该画两个,但是宝宝今天又加班啊~今天先...
    玖七七阅读 905评论 2 0