【Nodejs】理想论坛帖子爬虫1.02

在1.01版本中,我发现各回调函数找到数据后再插入数据库有个竞争问题不好解决,如果等所有回调都完成也没有好的处理方法,因为启动不止一处启动了新的TopicSpider实例。

于是我决定把读数据和写DB分开,爬虫负责前一部分,insertDB.js负责后一部分。

这样做避免了爬虫写DB竞争和判断所有回调都结束的难点问题。

而insertDB.js中,等待所有读文件的回调都结束是有办法的,那就是并行化控制,毕竟读各个文件比较简单,易于用task控制。

下面是爬虫代码:

//======================================================
// 理想论坛帖子下载爬虫1.02
// 目标网址:http://www.55188.com/forum-8-1.html
// 已经将获得数据和插入数据分开
// 此文件代码负责找到数据并存入文件
// 插入数据由insertDB.js负责
// 2018年4月16日
//======================================================

// 内置http模块
var http=require("http");

// 用于解析gzip网页(ungzip,https得到的网页是用gzip进行压缩的)
var zlib = require('zlib'); 

// 内置文件处理模块
var fs=require('fs');

// 用于转码。非Utf8的网页如gb2132会有乱码问题,需要iconv将其转码
var iconv = require('iconv-lite');

// cheerio模块,提供了类似jQuery的功能,用于从HTML code中查找图片地址和下一页
var cheerio = require("cheerio");

// 请求参数,JSON格式,http和https都有使用
var options;

// request请求
var req;

// 爬虫总数
var spiderCount=0;

// 完成任务的爬虫数
var spiderFinished=0;

// 爬虫获得的全部信息
var allInfos=[];

// 存放文件的目录
var folder="";

//-------------------------------
// 爬虫类
// index:爬虫序号
// title:帖子标题
// url:帖子地址
// pageNo:帖子页码
//-------------------------------
function TopicSpider(title,url){
    var obj=new Object;
    spiderCount++;
    obj.index=spiderCount;    // 序号
    obj.title=title;        // 标题
    obj.url=url;            // 地址
    obj.pageNo=1;            // 页码

    obj.crawl=function(){
        // 得到hostname和path
        var currUrl=this.url.replace("http://","");
        var pos=currUrl.indexOf("/");
        var hostname=currUrl.slice(0,pos);    
        var path=currUrl.slice(pos);    
        pos=currUrl.lastIndexOf("/");
        var dir="http://"+currUrl.slice(0,pos);    
        
        // 初始化options  
        options={
            hostname:hostname,
                port:80,
                path:path,// 子路径
              method:'GET',        
        };    

        // 请求并处理response
        req=http.request(options,function(resp){
            var html = [];                

            resp.on("data", function(data) {
                html.push(data);
            })
            resp.on("end", function() {
                
                var buffer = Buffer.concat(html);
                var body = iconv.decode(buffer,'gb2312');
                var $ = cheerio.load(body);    
                var infos=[];// 获得的发帖人信息

                // 得到下一页信息
                var nexturl=null;
                $(".pages_btns .pages strong").each(function(index,element){
                    
                    if(nexturl==null){
                        obj.pageNo=$(element).text();
                        
                        var nextNode=$(element).next();
                        if(nextNode && nextNode.attr("href")){
                            // 如有下一页再起新爬虫
                            nexturl='http://www.55188.com/'+nextNode.attr("href");

                            var nextspider=new TopicSpider(obj.title,nexturl);
                            nextspider.crawl();
                        }
                    }
                })

                // 得到发帖人信息
                $(".postinfo").each(function(index,element){                    
                    var content=$(element).text();
                    content=content.replace(/s+/g,' ');// 空白字符替换为一个空格
                    var arr=content.split(" ");// 以空格劈分
                    
                    if(arr.length==7){
                        info={'url':obj.url,
                              'title':obj.title,
                              '楼层':arr[1],
                              '作者':arr[2].replace('只看:',''),
                              '日期':arr[4],
                              '时间':arr[5]};
                        infos.push(info);
                        //console.log('info='+info);
                    }else if(arr.length==8){
                        info={'url':obj.url,
                              'title':obj.title,
                              '楼层':arr[1],
                              '作者':arr[2].replace('只看:',''),
                              '日期':arr[5],
                              '时间':arr[6]};
                        infos.push(info);
                        //console.log('info='+info);
                    }
                })

                // 信息获取结束,打印出爬虫信息
                var msg='';
                msg+='小爬序号:'+obj.index+'
';
                msg+='小爬爬取的帖子标题:'+obj.title+'
';
                msg+='小爬爬取的页码:'+obj.pageNo+'
';
                msg+='小爬爬取的帖子地址:'+obj.url+'
';
                msg+='小爬获取的信息数量:'+infos.length+'
';                
                obj.savefile(obj.index,infos); // 存文件
                msg+='小爬#:'+obj.index+'任务完成
';
                console.log(msg);                

                // 计算比例
                spiderFinished++;
                var percent=(spiderFinished*100/spiderCount).toFixed(2);
                console.log('
启动的爬虫数:'+spiderCount+',完成任务的爬虫数:'+spiderFinished+',完成比例:'+percent+"%");
                
            }).on("error", function(err) {
                console.log(coloredText('请求失败,异常为'+err,'red'));  
            })
        });

        // 超时处理
        req.setTimeout(7500,function(){
            req.abort();
        });

        // 出错处理
        req.on('error',function(err){
            console.log(coloredText('请求发生错误'+err,'red'));  
        });

        // 请求结束
        req.end();    
        
    };

    // 保存文件
    obj.savefile=function(index,infos){
        var text=JSON.stringify(infos);

        filename='./'+folder+'/'+index+'.dat';

        fs.writeFile(filename,text,function(err){
            if(err){
                console.log('写入文件'+filename+'失败,因为'+err);
            }
        });
    }

    return obj;
}

//-------------------------------
// 找到每个论坛页的帖子
// pageUrl:论坛页的地址
//-------------------------------
function findTopicsInPage(pageUrl){
    console.log("Current page="+pageUrl);

    // 得到hostname和path
    var currUrl=pageUrl.replace("http://","");
    var pos=currUrl.indexOf("/");
    var hostname=currUrl.slice(0,pos);    
    var path=currUrl.slice(pos);    
    pos=currUrl.lastIndexOf("/");
    var dir="http://"+currUrl.slice(0,pos);            
    
    // 初始化options  
    options={
        hostname:hostname,
            port:80,
            path:path,// 子路径
          method:'GET',        
    };    

    // 请求并处理response
    req=http.request(options,function(resp){
        var html = [];

        resp.on("data", function(data) {
            html.push(data);
        })
        resp.on("end", function() {
            var buffer = Buffer.concat(html);

            var body = iconv.decode(buffer,'gb2312');
            var $ = cheerio.load(body);

            $(".forumdisplay a").each(function(index,element){                
                var topicUrl='http://www.55188.com/'+$(element).attr("href");
                var topicTitle=$(element).text();

                // 启动子贴爬虫
                
                var spider=new TopicSpider(topicTitle,topicUrl);
                spider.crawl();
            })  
        }).on("error", function(err) {
            console.log("findTopicsInPage函数失败"+err);
        })
    });

    // 超时处理
    req.setTimeout(7500,function(){
        req.abort();
    });

    // 出错处理
    req.on('error',function(err){
        console.log('请求发生错误'+err);  
    });

    // 请求结束
    req.end();    
}

//-------------------------------
// 得到带颜色(前景色)的文字,用于在控制台输出
// text:文字
// color:前景色
//-------------------------------
function coloredText(text,color){
    var dic = new Array();
    dic["white"] = ['x1B[37m', 'x1B[39m'];
    dic["grey"] = ['x1B[90m', 'x1B[39m'];
    dic["black"] = ['x1B[30m', 'x1B[39m'];
    dic["blue"] = ['x1B[34m', 'x1B[39m'];
    dic["cyan"] = ['x1B[36m', 'x1B[39m'];
    dic["green"] = ['x1B[32m', 'x1B[39m'];
    dic["magenta"] = ['x1B[35m', 'x1B[39m'];
    dic["red"] = ['x1B[31m', 'x1B[39m'];
    dic["yellow"] = ['x1B[33m', 'x1B[39m'];

    return dic[color][0]+text+dic[color][1]
}

//-------------------------------
// 入口函数
// start:起始页,从1开始
// end:终止页,>start
//-------------------------------
function main(start,end){
    console.log(coloredText('爬取任务开始','yellow'));

    folder='infos('+getNowFormatDate()+")";

    // 创建目录
    fs.mkdir('./'+folder,function(err){
        if(err){
            console.log("目录"+folder+"已经存在");
        }
    });

    for(var i=start;i<=end;i++){        
        pageUrl='http://www.55188.com/forum-8-'+i+'.html'
        findTopicsInPage(pageUrl);
    }
}

//-------------------------------
// 测试函数,用于测试TopicSpider类
//-------------------------------
function testTopicSpider(){
    var spider=new TopicSpider('测试','http://www.55188.com/thread-8325667-1-1.html');
    spider.crawl();
}

//--------------------------------------
// 取得当前时间
//--------------------------------------
function getNowFormatDate() {
    var date = new Date();
    var seperator1 = "-";
    var seperator2 = "_";
    var month = date.getMonth() + 1;
    var strDate = date.getDate();
    if (month >= 1 && month <= 9) {
        month = "0" + month;
    }
    if (strDate >= 0 && strDate <= 9) {
        strDate = "0" + strDate;
    }
    var currentdate =date.getFullYear() + seperator1 + month + seperator1 + strDate
            + " " + date.getHours() + seperator2 + date.getMinutes()
            + seperator2 + date.getSeconds();
    return currentdate;
}

// 开始
main(1,5);

//testTopicSpider();

写出的文件示例截图如下:

这些文件是json格式的,示例:

[{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"楼主","作者":"先见天机","日期":"2018-4-15","时间":"15:13"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"2楼","作者":"短线冲","日期":"2018-4-15","时间":"15:27"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"3楼","作者":"勇儿马甲","日期":"2018-4-15","时间":"15:52"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"4楼","作者":"崇心开始","日期":"2018-4-15","时间":"15:52"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"5楼","作者":"老法儿","日期":"2018-4-15","时间":"16:28"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"6楼","作者":"金山水财","日期":"2018-4-15","时间":"16:33"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"7楼","作者":"生活如愿","日期":"2018-4-15","时间":"17:19"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"8楼","作者":"李汶安","日期":"2018-4-15","时间":"17:41"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"9楼","作者":"极品之星","日期":"2018-4-16","时间":"07:57"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"10楼","作者":"叼叼狼","日期":"2018-4-16","时间":"08:54"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"11楼","作者":"先见天机","日期":"2018-4-16","时间":"09:47"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"12楼","作者":"先见天机","日期":"2018-4-16","时间":"09:56"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"13楼","作者":"先见天机","日期":"2018-4-16","时间":"10:10"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"14楼","作者":"先见天机","日期":"2018-4-16","时间":"10:23"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"15楼","作者":"先见天机","日期":"2018-4-16","时间":"10:35"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"16楼","作者":"先见天机","日期":"2018-4-16","时间":"10:51"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"17楼","作者":"先见天机","日期":"2018-4-16","时间":"10:58"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"18楼","作者":"先见天机","日期":"2018-4-16","时间":"11:00"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"19楼","作者":"先见天机","日期":"2018-4-16","时间":"11:10"},{"url":"http://www.55188.com/thread-8337207-1-5.html","title":" 大势分析 ","楼层":"20楼","作者":"先见天机","日期":"2018-4-16","时间":"11:17"}]

而insertDB.js的代码如下:

//======================================================
// 在理想论坛帖子下载爬虫1.02生成数据文件后读取数据插入数据库
// 2018年4月17日
//======================================================
var fs=require('fs');    // 内置文件处理模块
var allInfos=[];        // 数据文件里获得的全部信息
var folder;                // 数据文件所在的目录
var tasks=[];            // 任务数组
var finished=0;            // 已完成任务的数量

//--------------------------------------
// 将数据插入数据库
//--------------------------------------
function insertDB(){
    console.log("开始插入DB");
    console.log('总计将有'+allInfos.length+"条数据将被插入数据库");
    
    var mysql=require('mysql'); // 连接mysql数据库的模块

    var conn=mysql.createConnection({
        host:'127.0.0.1',
        port:'3306',
        database:'test',
        user:'root',
        password:'12345678',
    });

    conn.connect(function(err){
        if(err){
            console.log('与MySQL数据库建立连接失败');
        }else{
            console.log('与MySQL数据库建立连接成功');

            for(var i=0;i<allInfos.length;i++){
                var info=allInfos[i];
                sql="insert into test.topic7(floor,author,tdate,ttime,addtime,url,title) values ('"
                    +info['楼层']+"','"
                    +info['作者']+"','"
                    +info['日期']+"','"
                    +info['时间']+"',"
                    +"now(),'"
                    +info['url']+"','"
                    +info['title']+"' "
                    +" )";

                conn.query(sql,function(err,result){
                    if(err){
                        console.log(err);
                    }else{
                        //console.log("Insert succeed");    
                    }
                            
                })
            }

            conn.end();
            //console.log('全部数据插入完成');
        }
    }); 
}

//--------------------------------------
// 检查是否完成
//--------------------------------------
function checkIfFinished(){
    finished++;

    // 读取任务全完成再插DB
    if(finished==tasks.length){
        console.log("数据总量:"+allInfos.length);
        insertDB();
    }
}

//--------------------------------------
// 入口函数
//--------------------------------------
function main() {
    folder='./'+'infos(2018-04-17 5_38_18)';// 数据文件所在目录

    fs.readdir(folder,function(err,files){
        if(err) throw err;

        for(var index in files){
            var task=(function(file){
                return function(){
                    fs.readFile(file,'utf8',function(err,data){
                        if(err){
                            console.log('读取文件失败,因为'+err);
                        }else{
                            var infos=JSON.parse(data);
                            allInfos=allInfos.concat(infos);   
                            
                            checkIfFinished();
                        }
                    });
                }
            })(folder+'/'+files[index]);

            tasks.push(task);
        }

        for(var task in tasks){
            tasks[task]();
        }
    });    
}

// 开始
main();

下面是数据库截图:

这样分离后,原有的问题就算解决了,Python爬虫也可以照此解决。

2018年4月17日08点45分

原文地址:https://www.cnblogs.com/heyang78/p/8861515.html