//====================================================== // 理想論壇帖子下載爬蟲1.06 // 循環改爲了遞歸,但最多下載千餘文件就崩了 // 2018年4月27日 //====================================================== var http=require("http"); // http模塊 var zlib = require('zlib'); // 用於解析gzip網頁 var fs=require('fs'); // 文件處理模塊 var iconv = require('iconv-lite'); // 用於轉碼。 var cheerio = require("cheerio"); // 用於從HTML中以相似jquery方式查找目標 var async=require('async'); // 用於異步流程控制 var EventProxy = require('eventproxy');// 用來控制併發 //--- 下面爲全局變量 --- var folder;// 存文件的目錄 var topics=[]; // 帖子數組 var finalTopics=[];// 全部帖子加子貼的最終數組 //------------------------------- // 用於建立目錄 //------------------------------- function createFolder(){ console.log('準備建立目錄'); folder='infos('+currDateTime()+")"; fs.mkdir('./'+folder,function(err){ if(err){ console.log("目錄"+folder+"已經存在"); }else{ console.log("目錄"+folder+"已建立"); } }); } //------------------------------- // 瀏覽頁面找主貼 // start:開始頁,end:結束頁 //------------------------------- function findTopics(start,end){ console.log('準備從如下頁面尋找主貼'); for(var i=start;i<=end;i++){ pageUrl='http://www.55188.com/forum-8-'+i+'.html' findTopicsInPage(pageUrl); } } //------------------------------- // 找到每一個論壇頁的帖子 // pageUrl:論壇頁的地址 //------------------------------- function findTopicsInPage(pageUrl){ console.log("page="+pageUrl); 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); var options={ hostname:hostname, port:80, path:path, method:'GET', }; var 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); $("tbody").each(function(index,element){ var $tbody=cheerio.load($(element).html()); var topic={}; topic.pageCount=1; topic.url=null; topic.title=null; $tbody(".forumdisplay a").each(function(index,element){ var topicUrl='http://www.55188.com/'+$tbody(element).attr("href"); var topicTitle=$tbody(element).text(); topic.url=topicUrl topic.title=topicTitle; }) $tbody(".threadpages").each(function(index,element){ topic.pageCount=$tbody(element).children().last().text(); }) if(topic.url!=null && topic.title!=null){ topics.push(topic); } }) }).on("error", function(err) { console.log("findTopicsInPage函數請求後獲取響應時出現異常"+err); }) }); // 超時處理 req.setTimeout(7500,function(){ req.abort(); }); // 出錯處理 req.on('error',function(err){ console.log('findTopicsInPage函數請求時發生錯誤'+err); }); // 請求結束 req.end(); } //------------------------------- // 保存每一個帖子到文件,完成一個遞歸調本身一次 // Nodejs特殊的回調處理強迫咱們把for,while循環改爲遞歸方式 //------------------------------- function saveTopicDetails(){ if(finalTopics.length>0){ var topic=finalTopics.pop(); var topicUrl=topic.url; var topicTitle=topic.title; var index=topic.index; var currUrl=topicUrl.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); var options={ hostname:hostname, port:80, path:path, method:'GET', headers:{ 'Connection':'close', } }; var req=http.request(options,function(resp){ req.setSocketKeepAlive(false); var html = []; resp.on("data", function(data) { html.push(data); }) resp.on("end", function() { try { var buffer = Buffer.concat(html); var body = iconv.decode(buffer,'gb2312'); var $ = cheerio.load(body); var infos=[];// 得到的發帖人信息 // 獲得發帖人信息 $(".postinfo").each(function(index,element){ var content=$(element).text(); content=content.replace(/\s+/g,' ');// 空白字符替換爲一個空格 var arr=content.split(" ");// 以空格劈分 if(arr.length==7){ info={'url':topicUrl, 'title':topicTitle, '樓層':arr[1], '做者':arr[2].replace('只看:',''), '日期':arr[4], '時間':arr[5]}; infos.push(info); //console.log('info='+info); }else if(arr.length==8){ info={'url':topicUrl, 'title':topicTitle, '樓層':arr[1], '做者':arr[2].replace('只看:',''), '日期':arr[5], '時間':arr[6]}; infos.push(info); //console.log('info='+info); } }) if(infos.length>0){ var text=JSON.stringify(infos); filename='./'+folder+'/'+index+'.dat'; fs.writeFile(filename,text,function(err){ if(err){ console.log('寫入文件'+filename+'失敗,由於'+err); } }); if(index % 50==0){ console.log(coloredText(nowTime()+'第'+index+'個文件保存完畢','green'));//讓控制檯出點動靜 } //req.end(); saveTopicDetails();//一個帖子完成,遞歸一次 } } catch(e) { var text=nowTime()+"saveTopicDetail函數請求時(on_end)獲取響應時出現異常"+e+'\n'; text+=nowTime()+" url="+topicUrl+'\n'; text+=nowTime()+" title="+topicTitle+'\n'; text+=nowTime()+" index="+index+'\n'; console.log(coloredText(text,'magenta')); /// //req.end(); saveTopicDetails();//出錯後依舊遞歸 } }).on("error", function(err) { var text=nowTime()+"saveTopicDetail函數請求後(on_error)獲取響應時出現異常"+err+'\n'; text+=nowTime()+" url="+topicUrl+'\n'; text+=nowTime()+" title="+topicTitle+'\n'; text+=nowTime()+" index="+index+'\n'; console.log(coloredText(text,'red')); /// //req.end(); saveTopicDetails();//出錯後依舊遞歸 }) }); // 超時處理 req.setTimeout(1000,function(){ req.abort(); }); // 出錯處理 // 這裏最容易出錯 Error: socket hang up req.on('error',function(err){ var text=nowTime()+"saveTopicDetails函數請求時出現異常"+err+'\n'; text+=nowTime()+" url="+topicUrl+'\n'; text+=nowTime()+" title="+topicTitle+'\n'; text+=nowTime()+" index="+index+'\n'; console.log(coloredText(text,'red')); /// req.end(); //finalTopics.push(topic); saveTopicDetails();//出錯後依舊遞歸 }); // 請求結束 req.end(); } } //------------------------------- // 入口函數 // start:起始頁,從1開始 // end:終止頁,>start //------------------------------- function main(start,end){ var flow=require('nimble'); flow.series([ function(callback){ setTimeout(function(){ createFolder(); callback(); },100); }, function(callback){ setTimeout(function(){ findTopics(start,end); callback(); },100); }, function(callback){ setTimeout(function(){ var n=topics.length; console.log("共找到"+n+"個帖子"); // 得到每一個子貼所在地址,序號和標題 var index=0; var arr=[]; for(var i=0;i<n;i++){ var topic=topics[i]; for(var j=1;j<=topic.pageCount;j++){ var regexp=new RegExp(/-(\d+)-(\d+)-(\d+)/); var topicUrl=topic.url.replace(regexp,"-$1-"+j+"-$3");// 用正則表達式替換第二個數字 index++; var item={'index':index,'url':topicUrl,'title':topic.title}; arr.push(item); } } finalTopics=arr;// 全部帖子加子貼的最終數組 finalTopics.reverse(); console.log('擬生成文件'+finalTopics.length+'個'); saveTopicDetails(); callback(); },3000); }, ]); } //-------------------------------------- // 通用函數,返回當前日期時間 建立目錄用 //-------------------------------------- function currDateTime() { 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; } //-------------------------------------- // 通用函數,返回當前日期時間 控制檯輸出時間用 //-------------------------------------- function nowTime() { 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; } //------------------------------- // 獲得帶顏色(前景色)的文字,用於在控制檯輸出 // 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]; } // 開始 main(2,2);