NO IMAGE

Nodejs爬蟲(定時爬取)

前言

Node.js是一個Javascript執行環境(runtime)。實際上它是對Google
V8引擎進行了封裝。V8引 擎執行Javascript的速度非常快,效能非常好。Node.js對一些特殊用例進行了優化,提供了替代的API,使得V8在非瀏覽器環境下執行得更好。

Node.js是一個基於Chrome JavaScript執行時建立的平臺, 用於方便地搭建響應速度快、易於擴充套件的網路應用。Node.js使用事件驅動
非阻塞I/O 模型而得以輕量和高效,非常適合在分散式裝置上執行資料密集型的實時應用。

使用NodeJs寫網頁爬蟲的優勢

大家都知道,我們要寫一個網頁爬蟲,爬取網頁上的資訊,實際上就是將目標網站的頁面html下載下來,然後通過各種方式(如正規表示式)獲取我們想要的資訊並儲存起來。從這點看來,使用Nodejs來寫網頁爬蟲便有著相當大的優勢。

n Nodejs採用了Javascript的語法規則,是前端開發人員能夠很容易上手

n Nodejs寫爬蟲可以避免寫一大堆正規表示式去匹配元素,我們可以用jquery的語法直接獲取dom物件,方便快捷,可讀性強。

n Nodejs解決了Javascript無法直接作業系統檔案的短板,讓我們可以輕鬆作業系統中檔案。

NodeJs寫網頁爬蟲需要準備的環境

1.首先,如果你的電腦沒有安裝nodejs,那麼,你需要到nodejs的官網中下載一個nodejs安裝包並安裝(安裝過程跟普通程式無異,這裡就不再贅述)。

Nodejs的官方網址為:

https://nodejs.org/en/

 

2.安裝好NodeJs之後,我們就可以在我們的專案空間中建立我們的專案目錄,並通過npm命令對專案進行初始化,並安裝以下外掛(具體安裝過程不再贅述,大家可百度一下npm安裝外掛的方法)。

 

"bufferhelper":"^0.2.1",
"cheerio":"^0.20.0",
"http":"^0.0.0",
"https":"^1.0.0",
"iconv-lite":"^0.4.13",
"node-schedule":"^1.1.1",
"path":"^0.12.7",
"request":"^2.74.0",
"url":"^0.11.0"

 

 

 

3.然後,我們可以全域性安裝一下express模組,命令如下:

 

npm install -g express-generator

cnpm install -g express-generator

 

 

4.安裝好express模組之後呢,我們就可以通過express建立一個新的爬蟲專案啦,具體命令如下:

 

express spider

 

命令執行完後我們就可以看到這樣的一個專案啦:

 

專案構建好之後,我們還要為專案安裝依賴,命令如下:

npm install

 

 

做完上面的步驟,我們的環境就算是搭建好了,接下來,我們就來看一下我們的爬蟲系統涉及到了那些模組。

 

1) 檔案系統(./module/File.js)

 

/**
 * 常用檔案操作模組
 * Created by 湯文輝 on 2016-08-02.
 */

var fs = require(‘fs’),//檔案操作
    mkdirp = require("mkdirp");//目錄操作

var File=
function(options) {

    this.path= options.path||
"";
    this.filename= options.filename||
"";
    this.encoding= options.encoding||
"UTF-8";

};

/**
 * 修改檔案內容並儲存
 *
@paramcontent   檔案內容
 *
@parambAppend   是否追加模式
 *
@paramencoding  檔案編碼,預設為UTF-8
 */
File.prototype.save=
function(content,bAppend,encoding)
{

    varself =
this;

    varbuffer =
newBuffer(content,encoding || self.encoding);

    vardoFs
=function
() {

        fs.open(self.path+self.filename,bAppend
? ‘a’:
‘w’,"0666",function
(err,fd) {
            if(err) {
                throwerr;
            }
            varcb2
=function
(err) {
                if(err){
                    throwerr;
                }

                fs.close(fd,function(err){
                    if(err){
                        throwerr;
                    }
                    console.log(‘檔案成功關閉…’);
                })
            };
            fs.write(fd,buffer,0,buffer.length,0,cb2);
        });

    };

    fs.exists(self.path,function
(exists) {
        if(!exists) {
            self.mkdir(self.path,"0666",function
() {
                doFs();
            });
        }else
{
            doFs();
        }
    });

};

/**
 * 遞迴建立目錄
 *
@parampath      目錄路徑
 *
@parammode      模式  預設使用 0666
 *
@paramfn        回撥
 *
@paramprefix    父級選單
 */
File.prototype.mkdir=
function(path,mode,fn,prefix)
{

    sPath = path.replace(/\\+/g,’/’);
    varaPath = sPath.split(‘/’);
    prefix = prefix ||”;
    varsPath = prefix + aPath.shift();
    varself =
this;
    varcb
=function
() {
        fs.mkdir(sPath,mode,function
(err) {
            if((!err) || ( ([47,-4075]).indexOf(err["errno"])
> -1 )) {//建立成功或者目錄已存在
                if(aPath.length>
0) {
                    self.mkdir(aPath.join(‘/’),mode,fn,sPath.replace(/\/$/,”)
+’/’);
                }else
{
                    fn();
                }
            } else{
                console.log(err);
                console.log(‘建立目錄:’+
sPath + ‘失敗’);
            }
        });
    };
    fs.exists(sPath,function
(exists) {
        if(!exists) {
            cb();
        }else if
(aPath.length>
0) {
            self.mkdir(aPath.join(‘/’),mode,fn,sPath.replace(/\/$/,”)
+’/’);
        }else
{
            fn();
        }
    });

};

module.exports=
File;

 

 

2) URL系統(./module/URL.js)

 

/**
 * URL處理類
 * Created by 湯文輝 on 2016-08-02.
 */

var urlUtil = require("url");
var pathUtil = require("path");

var URL=
function(){

};

/**
 *
@desc獲取URL地址 路徑部分 不包含域名以及QUERYSTRING
 *
 *
@paramstring url
 *
 *
@returnstring
 */
URL.getUrlPath=
function(url){

    if(!url){
        return”;
    }
    varoUrl = urlUtil.parse(url);
    if(oUrl["pathname"]
&& (/\/$/).test(oUrl["pathname"])){
        oUrl["pathname"] +="index.html";
    }
    if(oUrl["pathname"]){
        returnoUrl["pathname"].replace(/^\/+/,”);
    }
    return”;

};

/**
 *
@desc判斷是否是合法的URL地址一部分
 *
 *
@paramstring urlPart
 *
 *
@returnboolean
 */
URL.isValidPart=
function(urlPart){
    if(!urlPart){
        return false;
    }
    if(urlPart.indexOf("javascript")
> -1){
        return false;
    }
    if(urlPart.indexOf("mailto")
> -1){
        return false;
    }
    if(urlPart.charAt(0)
=== ‘#’){
        return false;
    }
    if(urlPart ===’/’){
        return false;
    }
    if(urlPart.substring(0,4)
=== "data"){//base64編碼圖片
        return false;
    }
    return true;
};

/**
 *
@desc修正被訪問地址分析出來的URL 返回合法完整的URL地址
 *
 *
@paramstring url 訪問地址
 *
@paramstring url2 被訪問地址分析出來的URL
 *
 *
@returnstring || boolean
 */
URL.prototype.fix=
function(url,url2){
    if(!url || !url2){
        return false;
    }
    varoUrl = urlUtil.parse(url);
    if(!oUrl["protocol"]
|| !oUrl["host"] || !oUrl["pathname"]){//無效的訪問地址
        return false;
    }
    if(url2.substring(0,2)
=== "//"){
        url2 = oUrl["protocol"]+url2;
    }
    varoUrl2 = urlUtil.parse(url2);
    if(oUrl2["host"]){
        if(oUrl2["hash"]){
            deleteoUrl2["hash"];
        }
        returnurlUtil.format(oUrl2);
    }
    varpathname = oUrl["pathname"];
    if(pathname.indexOf(‘/’)
> -1){
        pathname = pathname.substring(0,pathname.lastIndexOf(‘/’));
    }
    if(url2.charAt(0)
=== ‘/’){
        pathname = ”;
    }
    url2 = pathUtil.normalize(url2);//修正
./ 和 ../
    url2 = url2.replace(/\\/g,’/’);
    while(url2.indexOf("../")
> -1){//修正以../開頭的路徑
        pathname = pathUtil.dirname(pathname);
        url2 = url2.substring(3);
    }
    if(url2.indexOf(‘#’)
> -1){
        url2 = url2.substring(0,url2.lastIndexOf(‘#’));
    }else if(url2.indexOf(‘?’)
> -1){
        url2 = url2.substring(0,url2.lastIndexOf(‘?’));
    }
    varoTmp = {
        "protocol": oUrl["protocol"],
        "host": oUrl["host"],
        "pathname": pathname +’/’
+ url2
    };
    returnurlUtil.format(oTmp);
};

module.exports=
URL;

 

 

3) Robot系統(即爬蟲系統主體)

 

 

/**
 * 網頁爬蟲
 * Created by 湯文輝 on 2016-08-02.
 */

var File = require("./File.js");
var URL = require("./URL.js");
var http = require("http");
var https = require("https");
var cheerio = require(‘cheerio’);
var iconv = require(‘iconv-lite’);
var BufferHelper = require("bufferhelper");
var request = require(‘request’);

var oResult = {
    aNewURLQueue: [],//尚未執行爬取任務的佇列
    aOldURLQueue: [],//已完成爬取任務的佇列
    aTargetURLList: [],//目標物件URL集合
    oTargetInfoList: {},//目標物件集合
    oRetryCount:{},//失敗重試記錄
    iCount:0,//爬取url總數
    iSuccessNum:0//爬取成功數
};

/**
 * 爬蟲程式主體
 *
@paramoptions
 *
@constructor
 
*/
var Robot=
function(options) {

    varself =
this;
    this.domain= options.domain||
"";//需要爬取網站的域名
    this.firstUrl= options.firstUrl||
"";//需要爬取網站的url
    this.id=
this.constructor.create();//唯一識別符號
    this.encoding= options.encoding||
"UTF-8";//頁面編碼
    this.outputPath=
options.outputPath||
"";//爬取內容存放路徑
    this.outputFileName=
options.outputFileName||
"result.txt";//結果儲存檔名
    this.timeout= options.timeout||
5000;//超時時間
    this.retryNum= options.retryNum||
5;//失敗重試次數
    this.robots= options.robots||
true;//是否讀取robots.txt檔案

    this.debug= options.debug||
false;//是否開啟除錯模式

    this.file=
newFile({
        path:this.outputPath,
        filename:this.outputFileName
    });

    oResult.aNewURLQueue.push(this.firstUrl);//將第一個url新增進佇列之中

    this.handlerComplete=
options.handlerComplete||
function(){//佇列中所有的url均抓取完畢時執行回撥
            console.log("抓取結束…");

            varstr =
"",i=0,len=oResult.aTargetURLList.length;

            for(i=0;i<len;i++){

                url = oResult.aTargetURLList[i];
                str+="("+oResult.oTargetInfoList[url].name+")
: "+url+"\n"

            }
            this.file.save(str,true);

            this.file.save("\n抓取完成…\n",true);
        };

    this.disAllowArr=
[];//不允許爬取路徑

    varrobotsURL =
this.firstUrl+"robots.txt";

    request(robotsURL,function(error,response,body){
        if(!error && response.statusCode==
200) {
            this.disAllowArr=
self.parseRobots(body);
        }

    });

};

//預設唯一標識
Robot.id=
1;

/**
 * 累加唯一標識
 *
@returns{number}
 */
Robot.create=
function() {
    return this.id++;
};

/**
 * 解析robots.txt
 *
@paramstr
 *
@returns{Array}
 */
Robot.prototype.parseRobots=
function(str){

    varline = str.split("\r\n");

    vari=
0,len=line.length,arr
= [];

    for(i=0;i<len;i++){

        if(line[i].indexOf("Disallow:")!=-1){

            arr.push(line[i].split(":")[1].trim())

        }

    }

    returnarr;

};

/**
 * 判斷當前路徑是否允許爬取
 *
@paramurl
 *
@returns{boolean}
 */
Robot.prototype.isAllow=
function(url){

    vari=
0,len=this.disAllowArr.length;
    for(i=0;i<len;i++){

        if(url.toLowerCase().indexOf(this.disAllowArr[i].toLowerCase())!=-1){
            return false;
        }

    }

    return true;

};

/**
 * 開啟爬蟲任務
 */
Robot.prototype.go=
function(callback) {

    varurl =
"";

    if(oResult.aNewURLQueue.length>0){

        url = oResult.aNewURLQueue.pop();

        if(this.robots&&this.isAllow(url)){

            this.send(url,callback);

            oResult.iCount++;

            oResult.aOldURLQueue.push(url);

        }else{

            console.log("禁止爬取頁面:"+url);

        }

    }else{

        this.handlerComplete.call(this,oResult,this.file);

    }

};

/**
 * 傳送請求
 *
@paramurl   請求連結
 *
@paramcallback  請求網頁成功回撥
 */
Robot.prototype.send=
function(url,callback){

    varself =
this;

    vartimeoutEvent;//由於nodejs不支援timeout,所以,需要自己手動實現

    varreq =
”;
    if(url.indexOf("https")
> -1){
        req = https.request(url);
    }else
{
        req = http.request(url);
    }

    timeoutEvent = setTimeout(function()
{
        req.emit("timeout");
    },this.timeout);

    req.on(‘response’,function(res){
        varaType = self.getResourceType(res.headers["content-type"]);
        varbufferHelper =
newBufferHelper();
        if(aType[2]
!== "binary"){
        } else{
            res.setEncoding("binary");
        }
        res.on(‘data’,function(chunk){
            bufferHelper.concat(chunk);
        });
        res.on(‘end’,function(){//獲取資料結束
            clearTimeout(timeoutEvent);

            self.debug&&
console.log("\n抓取URL:"+url+"成功\n");

            //將拉取的資料進行轉碼,具體編碼跟需爬去資料的目標網站一致
            data= iconv.decode(bufferHelper.toBuffer(),self.encoding);

            //觸發成功回撥
            self.handlerSuccess(data,aType,url,callback);

            //回收變數
            data=
null;
        });
        res.on(‘error’,function(){
            clearTimeout(timeoutEvent);
            self.handlerFailure(url);
            self.debug&&
console.log("伺服器端響應失敗URL:"+url+"\n");
        });
    }).on(‘error’,function(err){
        clearTimeout(timeoutEvent);
        self.handlerFailure(url);
        self.debug&&
console.log("\n抓取URL:"+url+"失敗\n");
    }).on(‘finish’,function(){//呼叫END方法之後觸發
        self.debug&&
console.log("\n開始抓取URL:"+url+"\n");
    });
    req.on("timeout",function()
{
        //對訪問超時的資源,進行指定次數的重新抓取,當抓取次數達到預定次數後將不在抓取改url下的資料
        if(oResult.oRetryCount[url]==undefined){
            oResult.oRetryCount[url] =0;
        }else if(oResult.oRetryCount[url]!=undefined&&oResult.oRetryCount[url]<self.retryNum){
            oResult.oRetryCount[url]++;
            console.log("請求超時,排程到佇列最後…");
            oResult.aNewURLQueue.unshift(url);
        }
        if(req.res)
{
            req.res.emit("abort");
        }

        req.abort();
    });

    req.end();//發起請求

};

/**
 * 修改初始化資料,須在呼叫go方法前使用方能生效
 *
@paramoptions
 */
Robot.prototype.setOpt=
function(options){

    this.domain= options.domain||
this.domain||"";//需要爬取網站的域名
    this.firstUrl= options.firstUrl||
this.firstUrl||
"";//需要爬取網站的url
    this.id=
this.constructor.create();//唯一識別符號
    this.encoding= options.encoding||
this.encoding||
"UTF-8";//頁面編碼
    this.outputPath=
options.outputPath||
this.outputPath||
"";//爬取內容存放路徑
    this.outputFileName=
options.outputFileName||
this.outputFileName||
"result.txt";//結果儲存檔名
    this.timeout= options.timeout||
this.timeout||
5000;//超時時間
    this.retryNum= options.retryNum||
this.retryNum||
5;//失敗重試次數
    this.robots= options.robots||
this.robots||
true;//是否讀取robots.txt檔案

    this.debug= options.debug||
this.debug||
false;//是否開啟除錯模式

    this.file=
newFile({
        path:this.outputPath,
        filename:this.outputFileName
    });

    oResult.aNewURLQueue.push(this.firstUrl);//將第一個url新增進佇列之中

    this.handlerComplete=
options.handlerComplete||
this.handlerComplete||
function(){
            console.log("抓取結束…");

            varstr =
"",i=0,len=oResult.aTargetURLList.length;

            for(i=0;i<len;i++){

                url = oResult.aTargetURLList[i];
                str+="("+oResult.oTargetInfoList[url].name+")
: "+url+"\n"

            }
            this.file.save(str,true);

            this.file.save("\n抓取完成…\n",true);
        };

};

/**
 * 資料拉取成功回撥
 *
@paramdata  拉取回來的資料
 *
@paramaType 資料型別
 *
@paramurl   訪問連結
 *
@paramcallback  使用者給定訪問成功回撥,丟擲給使用者做一些處理
 */
Robot.prototype.handlerSuccess=
function(data,aType,url,callback){

    if(callback){

        var$ = cheerio.load(data);
        callback.call(this,$,aType,url,oResult.aNewURLQueue,oResult.aTargetURLList,oResult.oTargetInfoList);

        oResult.iSuccessNum++;
        this.go(callback);
    }else{
        this.go();
    }

};

/**
 * 失敗後繼續執行其他爬取任務
 *
@paramurl
 */
Robot.prototype.handlerFailure=
function(url){

    //oResult.aNewURLQueue.indexOf(url)==-1&&oResult.aNewURLQueue.unshift(url);
    this.go();

};

/**
 *
@desc判斷請求資源型別
 *
 *
@paramstring  Content-Type頭內容
 *
 *
@return[大分類,小分類,編碼型別] ["image","png","utf8"]
 */
Robot.prototype.getResourceType=
function(type){
    if(!type){
        return”;
    }
    varaType = type.split(‘/’);
    aType.forEach(function(s,i,a){
        a[i] = s.toLowerCase();
    });
    if(aType[1] && (aType[1].indexOf(‘;’)
> -1)){
        varaTmp = aType[1].split(‘;’);
        aType[1] = aTmp[0];
        for(vari
= 1;i < aTmp.length;i++){
            if(aTmp[i] && (aTmp[i].indexOf("charset")
> -1)){
                aTmp2= aTmp[i].split(‘=’);
                aType[2] =aTmp2[1]
?aTmp2[1].replace(/^\s+|\s+$/,”).replace(‘-‘,”).toLowerCase()
: ”;
            }
        }
    }
    if((["image"]).indexOf(aType[0])
> -1){
        aType[2] ="binary";
    }
    returnaType;
};

module.exports=
Robot;

 

 

上面的功能都實現後,我們就可以開始來使用我們的爬蟲系統了,首先,在app.js中呼叫我們的Robot模組

 

/**
 * Created by 湯文輝 on 2016-08-03.
 */
var express = require("express");
var Robot = require("./module/robot.js");
var schedule = require("node-schedule");

function getTime(){
    vardate =
newDate();
    vary = date.getFullYear();
    varm = date.getMonth()+1;
    vard = date.getDate();
    varh = date.getHours();
    varmi = date.getMinutes();
    vars = date.getSeconds();

    m = m<10?"0"+m:m;
    d = d<10?"0"+d:d;
    h = h<10?"0"+h:h;
    mi = mi<10?"0"+mi:mi;
    s = s<10?"0"+s:s;

    returny+"_"+m+"_"+d+"_"+h+"_"+mi+"_"+s;

}

var options = {
    domain:"dytt8.net",
    firstUrl:"http://www.dytt8.net/",
    outputPath:"./output/testRobot/",
    outputFileName:"test.txt",
    encoding:"GBK",
    timeout:6000,
    robots:true,
    debug:true,
    handlerComplete:function(oResult,file){

        console.log("抓取結束…");

        file.save("\n抓取完成…\n總共訪問網頁數為"+oResult.iCount+"條,其中成功訪問網頁數"+oResult.iSuccessNum+"條",true);

    }
};
var robot =new
Robot(options);
var reg1 =/\/html\/[a-z0-9]+\/[a-z0-9]+\/[\d]+\/[\d]+\.html/gmi;
var reg2 =/\/html\/[a-z0-9]+\/index\.html/gmi;
//var reg3 = /(ftp|http):\/\/.+\.(rmvb|mp4|avi|flv|mkv|3gp|wmv|wav|mpg|mov)/gmi;

function start(){

    robot.go(function($,aType,url,aNewURLQueue,aTargetURLList,oTargetInfoList){

        varself =
this;
        varpUrl = url;
        if(url===options.firstUrl){

            varaA = $("a");

            aA.each(function(){

                varhref = $(this).attr(‘href’);

                if(href.indexOf("http://")==-1){

                    href = options.firstUrl+href.substring(1);

                }

                varres = reg1.exec(href);

                if(res){

                    aNewURLQueue.indexOf(href)==-1&&aNewURLQueue.push(href);

                }

            });

        }else{

            $(‘a’).each(function(){

                varhref = $(this).attr(‘href’);
                varres2 = reg2.exec(href);

                console.log("頁面["+pUrl+"]二級頁面:【"+
href + "】");

                if(href.indexOf("thunder://")!=-1){

                    varurl = $(this).text().trim();
                    console.log("\n目標連結【"+$("h1").text().trim()+"】:"+url+"\n");
                    varname = $("h1").text().trim();
                    if(aTargetURLList.indexOf(url)){
                        aTargetURLList.push(url);
                        oTargetInfoList[url] = {
                            name:name
                        };
                    }

                    self.file.save(url+"\n",true);

                }else if(href.indexOf("ftp://")!=-1){
                    varurl = $(this).attr("href");
                    console.log("\n目標連結【"+$("h1").text().trim()+"】:"+url+"\n");
                    varname = $("h1").text().trim();
                    if(aTargetURLList.indexOf(url)){
                        aTargetURLList.push(url);
                        oTargetInfoList[url] = {
                            name:name
                        };
                    }
                    self.file.save(url+"\n",true);

                }else if(res2){
                    if(href.indexOf("http://")==-1){

                        href = options.firstUrl+href.substring(1);

                    }

                    varres = reg1.exec(href);

                    if(res){

                        aNewURLQueue.indexOf(href)==-1&&aNewURLQueue.push(href);

                    }
                }

            });

        }

    });
}

var rule =new
schedule.RecurrenceRule();

rule.dayOfWeek= [0,new
schedule.Range(1,6)];

rule.hour=
19;

rule.minute=
45;

console.log("定時爬取任務,下次爬取時間為"+rule.hour+"時"+rule.minute+"分");

var j = schedule.scheduleJob(rule,function(){

    robot.setOpt({
        outputFileName:getTime()+"-"+"電影天堂.txt"
    });
    console.log("開始定時爬取任務…");
    start();

});

 

然後,我們在命令列中輸入

 

 

 

node app.js

 

執行即可,爬蟲將會在星期一~星期天的晚上19:45分定時爬取電影天堂電影下載連結,並輸出到output目錄中