以前写了个如今看来很不完美的小爬虫,不少地方没有处理好,好比说在知乎点开一个问题的时候,它的全部回答并非所有加载好了的,当你拉到回答的尾部时,点击加载更多,回答才会再加载一部分,因此说若是直接发送一个问题的请求连接,取得的页面是不完整的。还有就是咱们经过访问连接下载图片的时候,是一张一张来下的,若是图片数量太多的话,真的是会下到你睡完觉它还在下。html
此次的的爬虫是上次那个的升级版,爬虫代码在个人github上能够找到=>NodeSpider。node
整个爬虫的思路是这样的:在一开始咱们经过请求问题的连接抓取到部分页面数据,接下来咱们在代码中模拟ajax请求截取剩余页面的数据,固然在这里也是能够经过异步来实现并发的,对于小规模的异步流程控制,能够用这个模块=>eventproxy,但这里我就没有用啦!咱们经过分析获取到的页面从中截取出全部图片的连接,再经过异步并发来实现对这些图片的批量下载。git
抓取页面初始的数据很简单啊,这里就不作多解释了github
1 /*获取首屏全部图片连接*/ 2 var getInitUrlList=function(){ 3 request.get("https://www.zhihu.com/question/34937418") 4 .end(function(err,res){ 5 if(err){ 6 console.log(err); 7 }else{ 8 var $=cheerio.load(res.text); 9 var answerList=$(".zm-item-answer"); 10 answerList.map(function(i,answer){ 11 var images=$(answer).find('.zm-item-rich-text img'); 12 images.map(function(i,image){ 13 photos.push($(image).attr("src")); 14 }); 15 }); 16 console.log("已成功抓取"+photos.length+"张图片的连接"); 17 getIAjaxUrlList(20); 18 } 19 }); 20 }
模拟ajax请求获取完整页面ajax
接下来就是怎么去模拟点击加载更多时发出的ajax请求了,去知乎看一下吧!json
有了这些信息,就能够来模拟发送相同的请求来得到这些数据啦。数组
1 /*每隔100毫秒模拟发送ajax请求,并获取请求结果中全部的图片连接*/ 2 var getIAjaxUrlList=function(offset){ 3 request.post("https://www.zhihu.com/node/QuestionAnswerListV2") 4 .set(config) 5 .send("method=next¶ms=%7B%22url_token%22%3A34937418%2C%22pagesize%22%3A20%2C%22offset%22%3A" +offset+ "%7D&_xsrf=98360a2df02783902146dee374772e51") 6 .end(function(err,res){ 7 if(err){ 8 console.log(err); 9 }else{ 10 var response=JSON.parse(res.text);/*想用json的话对json序列化便可,提交json的话须要对json进行反序列化*/ 11 if(response.msg&&response.msg.length){ 12 var $=cheerio.load(response.msg.join(""));/*把全部的数组元素拼接在一块儿,以空白符分隔,不要这样join(),它会默认数组元素以逗号分隔*/ 13 var answerList=$(".zm-item-answer"); 14 answerList.map(function(i,answer){ 15 var images=$(answer).find('.zm-item-rich-text img'); 16 images.map(function(i,image){ 17 photos.push($(image).attr("src")); 18 }); 19 }); 20 setTimeout(function(){ 21 offset+=20; 22 console.log("已成功抓取"+photos.length+"张图片的连接"); 23 getIAjaxUrlList(offset); 24 },100); 25 }else{ 26 console.log("图片连接所有获取完毕,一共有"+photos.length+"条图片连接"); 27 // console.log(photos); 28 return downloadImg(50); 29 } 30 } 31 }); 32 }
在代码中post这条请求https://www.zhihu.com/node/QuestionAnswerListV2,把原请求头和请求参数复制下来,做为咱们的请求头和请求参数,superagent的set方法可用来设置请求头,send方法能够用来发送请求参数。咱们把请求参数中的offset初始为20,每隔必定时间offset再加20,再从新发送请求,这样就至关于咱们每隔必定时间发送了一条ajax请求,获取到最新的20条数据,每获取到了数据,咱们再对这些数据进行必定的处理,让它们变成一整段的html,便于后面的提取连接处理。
异步并发控制下载图片
再获取完了全部的图片连接以后,即断定response.msg为空时,咱们就要对这些图片进行下载了,不可能一条一条下对不对,由于如你所看到的,咱们的图片足足有

没错,2万多张,不过幸亏nodejs拥有神奇的单线程异步特性,咱们能够同时对这些图片进行下载。但这个时候问题来了,据说同时发送请求太多的话会被网站封ip的啊!全部咱们确定不能同时并发下载这两万多张图片,这个时候就须要对异步并发数量进行一些控制了。并发
在这里用到了一个神奇的模块=>async,它不只能帮咱们拜托难以维护的回调金字塔恶魔,还能轻松的帮咱们进行异步流程的管理。具体看文档啦,这里就只用到了一个强大的async.mapLimit方法。异步
1 var requestAndwrite=function(url,callback){ 2 request.get(url).end(function(err,res){ 3 if(err){ 4 console.log(err); 5 console.log("有一张图片请求失败啦..."); 6 }else{ 7 var fileName=path.basename(url); 8 fs.writeFile("./img1/"+fileName,res.body,function(err){ 9 if(err){ 10 console.log(err); 11 console.log("有一张图片写入失败啦..."); 12 }else{ 13 console.log("图片下载成功啦"); 14 callback(null,"successful !"); 15 /*callback貌似必须调用,第二个参数将传给下一个回调函数的result,result是一个数组*/ 16 } 17 }); 18 } 19 }); 20 } 21 22 var downloadImg=function(asyncNum){ 23 /*有一些图片连接地址不完整没有“http:”头部,帮它们拼接完整*/ 24 for(var i=0;i<photos.length;i++){ 25 if(photos[i].indexOf("http")===-1){ 26 photos[i]="http:"+photos[i]; 27 } 28 } 29 console.log("即将异步并发下载图片,当前并发数为:"+asyncNum); 30 async.mapLimit(photos,asyncNum,function(photo,callback){ 31 console.log("已有"+asyncNum+"张图片进入下载队列"); 32 requestAndwrite(photo,callback); 33 },function(err,result){ 34 if(err){ 35 console.log(err); 36 }else{ 37 // console.log(result);<=会输出一个有2万多个“successful”字符串的数组 38 console.log("所有已下载完毕!"); 39 } 40 }); 41 42 };
先看这里=>async
mapLimit方法的第一个参数photos是全部图片连接的数组,也是咱们并发请求的对象,asyncNum是限制并发请求的数量。当咱们有这个参数时,好比它的值是10,则它一次就只会帮咱们从数组中取10条连接,执行并发的请求,这10条请求都获得响应后,再发送下10条请求。
结尾
哦哦~,明天就是除夕了~