当前位置: 首页 > 后端技术 > Node.js

Usingdeclarative-crawlertoCrawlZhihuMeitu

时间:2023-04-03 14:50:40 Node.js

Usingdeclarative-crawlertoCrawlZhihuMeitu是作者对declarative-crawler的具体实例讲解,属于作者的数据科学与机器学习程序员手册。这部分的源码可以参考这里。declarative-crawler的分层架构和设计理念可以参考笔者之前的文章《基于Node.js的声明式可监控爬虫网络初探》。这里还是想以知乎简单的list-details页面为例,来说明一下declarative-crawler的基本用法。首先我们来看一下爬取的目标。比如我们搜索美女或者其他话题,可以得到如下答案的列表页:点击某个答案后,可以进入以下答案详情页,我们的目标是将图片全部保存到本地。目前已知的处理动态页面的爬虫设计是基于React的动态网络。也就是说,我们不能直接使用fetch这样的静态抓取器。事实上,declarative-crawler提供了多种类型的爬虫,比如爬取静态网页的HTMLSpider,爬取动态网页的HeadlessChromeSpider,爬取接口的JSONSpider.js,爬取数据库的MySQLSpider等。而这里我们需要协同渲染用无界面的浏览器抓取静态页面、脚本、CSS等,然后得到真实的网页。这里我们使用HeadlessChrome作为渲染载体。笔者将在以后的文章中介绍如何使用HeadlessChrome。这里我们只需要使用预设的Docker镜像就可以将Chrome作为服务来运行。HeadlessChromeSpider其实就是对Chrome远程调试协议的封装。在下面的代码中,我们简单地导航到URL,然后在抓取HTML值之前等待页面加载:CDP({host:this.host,port:this.port},client=>{//设置网络和页面处理程序const{Network,Page,Runtime}=client;Promise.all([Network.enable(),Page.enable(),Runtime.enable()]).then(()=>{returnPage.navigate({url});}).catch(err=>{console.error(err);client.close();});Network.requestWillBeSent(params=>{//console.log(params.request.url);});Page.loadEventFired(()=>{setTimeout(()=>{Runtime.evaluate({expression:"document.body.outerHTML"}).then(result=>{resolve(result.result.value);client.close();});},这个.延迟);});}).on("error",err=>{console.error(err);});但是这种方式无法获取到我们想要的图片信息,我们可以使用Network模块监听所有的网络请求,可以发现由于知乎是按照滚动懒加载的方式加载图片的,当页面加载事件被触发时,onlythefollowingsmallavatarswillactuallybeloadedintheimgtag:https://pic4.zhimg.com/a99b7a9933526403f0b012bd9c11dbbf_60w.jpghttps://pic1.zhimg.com/151ee0138f8432d61977504615d0614c_60w.jpghttps://pic2.zhimg.com/c2847b95e204cd6e23fca03d18610a65_60w.jpghttps://pic2.zhimg.com/5f026494c8bcc7283770e84c37c1aa49_60w.jpghttps://pic1.zhimg.com/4bd564be18599d169a6fab3b83f3c418_60w.jpghttps://pic1.zhimg.com/16eb0d6650f962d8ff1b0b339a4563cc_60w.jpghttps://pic1.zhimg.com/b6f5310d9fac7c173ce8e310f6196f38_60w.jpghttps://pic3.zhimg.com/0aac046c829d37edcf0b9ba780dc2f92_60w.jpghttps://pic3.zhimg.com/c4cdff37d72774768c202478c1adc1b6_60w.jpghttps://pic1.zhimg.com/aa1dc6506f009530c701ae9ae283c424_60w.jpghttps://pic4.zhimg.com/200c20e15a427b5a740bc7577c931133_60w.jpghttps://pic4.zhimg.com/7be083ae4531db70b9bd9149dc30dd1b_60w.jpghttps://pic2.zhimg.com/5261bc283c6c2ed2900a504e2677d365_60w.jpghttps://pic1.zhimg.com/9a6762c751175966686bf93bf009ab30_60w.jpghttps://pic4.zhimg.com/b1b92239d6718aa146b0669dc423e693_60w.jpg针对这种情况,我们的第一个思路为了模拟用户滚动,Chrome为我们提供了一个Input模块来远程执行一些模拟操作,例如点击和触摸:awaitInput.synthesizeScrollGesture({x:0,y:0,yDistance:-10000,repeatCount:10});但是这种方法性能差,等待时间长。另一个想法是借鉴Web测试中的MonkeyTest,在界面中插入额外的脚本。但是由于知乎的ContentSecurityPolicy禁止插入来路不明的脚本,这个方法不行。最后我们还是把透视放到界面里面,发现知乎把懒加载的图片都放在了noscript标签里面,所以我们直接从noscript标签中提取懒加载图片地址保存即可。声明处理单个页面的蜘蛛主题列表页面我们首先需要声明抓取某个主题下所有答案列表的蜘蛛。基本用法如下:/***@function知道某个主题答案的爬虫*/exportdefaultclassTopicSpiderextendsHeadlessChromeSpider{//定义模型model={".feed-item":{$summary:".summary",$question:".question_link"}};/***@function默认解析函数*@parampageObject*@param$*@returns{Array}*/parse(pageObject:any,$:Element){//存储所有捕获的对象letfeedItems=[];for(let{$question,$summary}ofpageObject[".feed-item"]){feedItems.push({questionTitle:$question.text(),questionHref:$question.attr("href"),answerHref:$($summary.find("a")).attr("href"),摘要:$summary.text()});}返回feedItems;}}声明蜘蛛我们需要核心的是声明模型,即页面的DOM抽取规则。这里我们在底部使用cherrio;然后声明解析方法,即从DOM元素对象中提取具体数据。然后我们可以使用Jest编写简单的单元测试://@flowimportTopicSpiderfrom"../../spider/TopicSpider";constexpect=require("chai").expect;lettopicSpider:TopicSpider=newTopicSpider()。setRequest("https://www.zhihu.com/topic/19552207/top-answers").setChromeOption("120.55.83.19");test("抓取某个话题下的答案列表",asyncdone=>{letanswers=awaittopicSpider.run(false);expect(answers,"返回的数据是一个列表,长度大于10").to.have.length.above(2);done();});answerpageImageextraction对于答题页提取来说稍微复杂一些,因为我们还需要声明图片下载器。这里的parse函数中,我们提取出noscript下包含的所有img标签和图片链接,最后调用内置的downloadPersistor保存图片:/***@function专门用于爬取答案和缓存的爬虫*/exportdefaultclassAnswerAndPersistImageSpiderextendsHeadlessChromeSpider{//定义模型model={//抓取所有默认$imgs:"img",//抓取所有延迟加载的大图像$noscript:"noscript"};/***@function解析提取的页面对象*@parampageElement存储页面对象*@param$整个页面的DOM表示*@returns{Promise.}*/asyncparse(pageElement:any,$:Element):any{//存储所有图像letimgs=[];//获取所有默认图像for(leti=0;i}*/asyncpersist(imgs){awaitdownloadPersistor.saveImage(imgs);}}同样我们可以编写相关的单元测试://@flowimportAnswerAndPersistImageSpiderfrom"../../spider/AnswerAndPersistImageSpider";constexpect=require("chai").expect;global.jasmine.DEFAULT_TIMEOUT_INTERVAL=1000000;//初始化letanswerAndPersistImageSpider:AnswerAndPersistImageSpider=newAnswerAndPersistImageSpider("AnsetswerAndPersist)https://www.zhihu.com/question/29134042").setChromeOption("120.55.83.19",null,10*1000);test("抓取所有图片在某题",asyncdone=>{letimages=awaitanswerAndPersistImageSpider.run(false);expect(images,"返回的数据是一个列表,长度大于10").to.have.length.above(2);done();});test("抓取某个问题中的所有图片并保存",asyncdone=>{letimages=awaitanswerAndPersistImageSpider.run(true);done();});声明串联多个蜘蛛的爬虫负责采集和处理订单页面蜘蛛写完后,我们需要写一个串联多个蜘蛛的爬虫:exportdefaultclassBeautyTopicCrawlerextendsCrawler{//初始化爬虫initalize(){//构建所有爬虫letrequests=[{url:"https://www.zhihu.com/topic/19552207/top-answers"},{url:"https://www.zhihu.com/topic/19606792/top-answers"}];this.setRequests(requests).setSpider(newTopicSpider().setChromeOption("120.55.83.19",null,10*1000)).transform(feedItems=>{if(!Array.isArray(feedItems)){抛出新错误("爬虫连接失败!");}returnfeedItems.map(feedItem=>{//判断url中是否存在zhihu.com,存在则直接返回consthref=feedItem.answerHref;if(!!href){//存在有效二级链接returnhref.indexOf("zhihu.com")>-1?href:`https://www.zhihu.com${href}`;}});}).setSpider(newAnswerAndPersistImageSpider().setChromeOption("120.55.83.19",null,10*1000));}}爬虫的核心就是它的初始化函数,这里我们需要输入种子地址和爬虫的串口配置,然后交给爬虫自动执行服务器运行和监控爬虫声明后,我们就可以将整个爬虫作为服务器来运行了://@flowimportCrawlerSchedulerfrom"../../crawler/CrawlerScheduler";importCrawlerServerfrom"../../server/CrawlerServer”;从“./crawler/BeautyTopicCrawler”导入BeautyTopicCrawler;constcrawlerScheduler:CrawlerScheduler=newCrawlerScheduler();让beautyTopicCrawler=newBeautyTopicCrawler();crawlerScheduler.register(beautyTopicCrawler);newCrawlerServer(SchedulerServer)then(()=>{},错误=>{console.log(error);});服务启动后,我们可以访问3001端口获取当前系统状态:http://localhost:3001/[{name:"BeautyTopicCrawler",displayName:"Crawler",isRunning:false,lastStartTime:"2017-05-03T05:03:58.217Z"}]然后访问启动爬虫的起始地址:http://localhost:3001/startcrawlerstart之后我们可以查看具体爬虫的运行状态:http://localhost:3001/BeautyTopicCrawler{"leftRequest":37,"spiders":[{"name":"TopicSpider","displayName":"Spider","count":2,“countByTime”:{“0”:0,“59”:0},“lastActiveTime”:“2017-05-03T04:56:31.650Z”,“executeDuration”:13147.5,“errorCount”:0},{“name":"AnswerAndPersistImageSpider","displayName":"Spider","count":1,"countByTime":{"0":0,"59":0},"lastActiveTime":"2017-05-03T04:56:44.513Z","executeDuration":159120,"errorCount":0}]}我们也可以通过预定义的监控接口实时查看爬虫的运行状态(正在复现,尚未接入真实数据),可以在根目录的ui文件夹中运行:yarninstallnpmstart看到如下界面:最后我们还可以在本地文件夹(默认为/tmp/images)中查看所有抓取图片的列表):