Node.js-baseddeclarativemonitorablecrawlernetwork归作者所有,描述了作者在重构我们的简单爬虫过程中搭建简单爬虫框架的思路和实现,代码参考这里介绍一个基于Node.js的声明式可监控爬虫网络爬虫是抓取数据的重要手段之一,以Scrapy、Crawler4j、Nutch为代表的开源框架可以帮助我们快速搭建分布式爬虫系统;简而言之,我们在开发大型爬虫系统时可能会面临以下挑战:网页爬取:最简单的爬取就是使用HTTP客户端,比如HTTPClient或fetch或request。现在随着单页应用等富客户端应用的流行,我们可以使用Selenium、PhantomJS等无头浏览器来动态执行脚本进行渲染。网页分析:网页内容的提取和分析是一个非常麻烦的问题。DOM4j、Cherrio和beautifulsoup为我们提供了基本的分析功能。作者也尝试过搭建一个全配置的爬虫,类似于Web-Scraper,但还是输给了复杂多变、多层嵌套的iFrame页面。在这里,笔者秉承代码即配置的理念。配置声明的内置复杂度比较低,但是对于那些业务复杂度高的网页,整体复杂度会呈几何级数增长。使用代码声明其内置的复杂度和门槛比较高,但是可以更好的处理业务复杂度高的网页。笔者在构思未来交互式爬虫生成接口时,也希望借鉴FaaS的思想,直接用代码来声明整个解析过程,而不是使用配置。反爬虫对抗:像淘宝这样的主流网站基本上都有反爬虫机制。他们会从请求频率、请求地址、请求行为、目标连贯性等多个维度进行分析,从而判断请求者是爬虫还是真人。用户。我们常用的方式是使用多个IP或者多个代理来避免同源的频繁请求,也可以借鉴GAN或者增强学习的思路,让爬虫根据目标的反爬虫策略自动升级改造自己网站。另一种常见的反爬虫方法是验证码。从最初的迷惑图片,到常见的拖拽验证码,都有不小的障碍。我们可以尝试通过从图片中提取文本并模拟用户行为来绕过它们。分布式调度:单机的吞吐量和性能始终存在瓶颈,分布式爬虫和其他分布式系统一样,需要考虑分布式治理、数据一致性、任务调度等问题。笔者个人的感觉是,爬虫的工作节点尽量是无状态的,将整个爬虫集群的状态存储在Redis或Consul等能够保证高可用的中心存储中。在线有价值页面预测:Google经典的PageRank可以根据网络中的连接信息判断一个URL的价值,从而优先索引或抓取有价值的页面。而像Anthelion这样的智能解析工具,可以根据之前提取的页面内容的价值,预测某个URL是否需要爬取。页面内容提取和存储:提取网页中结构化或非结构化的内容实体是自然语言处理中的常见任务之一,而从海量数据中自动提取有意义的内容也涉及到机器学习、大数据处理等知识领域。我们可以使用HadoopMapReduce、Spark、Flink等离线或流式计算引擎处理海量数据,使用词嵌入、主题模型、LSTM等机器学习技术对文本进行分析,使用HBase和ElasticSearch对文本进行存储或索引。笔者无意重新发明轮子,而是在改造我们公司一个简单的命令式爬虫的过程中,发现很多调度和监控操作应该由框架来完成。在开发大型分布式应用程序时,Node.js可能不像Java或Go那样一致(JavaScript未标准化)和性能。但是正如作者上面提到的,JavaScript的优势在于它可以通过同构代码同时运行在客户端和服务端,所以以后解析步骤完全可以在客户端调试,然后直接在客户端运行代码服务器。这对于构建灵活多变的分析可能是有意义的。总而言之,我只是想有一个可扩展、可监控、易用的爬虫框架,所以我很快推出了一个declarative-crawler,目前只是在原型阶段,还没有发布到npm;希望有兴趣的大佬多多指教,特别是发现有同类型的框架可以瞎扒一扒。让我看看我是否可以使用它们来了解更多信息。设计思路和架构概述几年前笔者写第一个爬虫的时候,整体思路是典型的命令式编程,即先抓取,再解析,最后持久化存储,如下代码:awaitfetchListAndContentThenIndex('jsgc',section.name,section.menuCode,section.category).then(()=>{}).catch(error=>{console.log(error);});但是好像作者在2016-我的前端之路:工具与工程和2015-我的前端之路:数据流驱动接口讨论过,命令式编程比声明式编程耦合性更强,可测试性和可控性更低;看来,当我们从jQuery转向React、Angular、Vue.js等框架时,我们应该尽可能地把事情交给工具和框架来管理和解决。这样也方便我们的自定义监控。总结一下,作者的设计思路主要有以下几点:关注点分离,整个架构分为爬虫调度CrawlerScheduler、Crawler、Spider、dcEmitter、Store、KoaServer、MonitorUI等部分,尽可能分离职责。在声明式编程中,每个蜘蛛的生命周期包括爬取、提取、解析、持久化存储;开发者应该独立声明这些部分,完整的调用和调度应该由框架来完成。层是可独立测试的。以爬虫的生命周期为例,提取和解析应该声明为纯函数,而抓取和持久化存储更面向业务,可以mock或带有副作用的测试。整个爬虫网络架构如下图,目前所有的代码都可以在这里找到。自定义蜘蛛和爬虫我们以爬取在线列表和详情页面为例。首先,我们需要为两个页面构建蜘蛛。注意,每个蜘蛛负责抓取和解析某个URL。用户应该先写list一个爬虫需要声明model属性,重写before_extract、parse和persist方法,每个方法都会被串行调用。另外需要注意的是,我们的爬虫可能会在外部传入一些配置信息,统一声明在extra属性中,这样在持久化的时候也可以使用。typeExtraType={module?:string,name?:string,menuCode?:string,category?:string};exportdefaultclassUAListSpiderextendsSpider{displayName="公共广告列表蜘蛛";额外的:ExtraType={};模型={$announcements:'tr[height="25"]'};构造函数(额外:ExtraType){超级();this.extra=额外的;}before_extract(pageHTML:string){returnpageHTML.replace(//gim,"");}parse(pageElements:Object){letannouncements=[];让announcementsLength=pageElements.$announcements.length;for(leti=0;i{letflag=true;//这里每个URL对应一个announcement数组}catch(err){flag=false;}}返回标志;我们可以针对这个spider单独测试,这里使用Jest。注意,为了描述方便,没有提取、解析等单元测试。在大型项目中,我们建议添加这些纯功能测试用例。varexpect=require("chai").expect;从"../../src/universal_announcements/UAListSpider.js"导入UAListSpider;让ualistSpider:UAListSpider=newUAListSpider({module:"jsgc",name:"room市政建设招标公告-服务",menuCode:"001001/001001001/00100100100",category:"1"}).setRequest("http://ggzy.njzwfw.gov.cn/njggzy/jsgc/001001/001001001/001001001001/?Paging=1",{});test("获取公共列表",async()=>{letannouncements=awaituaListSpider.run(false);expect(announcements,"返回的数据是列表,长度大于10").to.have.length.above(2);});test("捕获公共列表并执行持久化操作",async()=>{letannouncements=awaituaListSpider.run(true);expect(announcements,"返回的数据是长度大于10的列表").to.have.length.above(2);});同样,我们可以为详情页定义一个蜘蛛:model={//标题$title:"#tblInfo#tdTitleb",//时间$time:"#tblInfo#tdTitlefont",//内容$content:"#tblInfo#TDContent"};解析(页面元素:对象){...}asyncpersist(announcement:Object){...}}定义好spider之后,我们就可以定义Crawler来负责爬取整个系列的任务了。注意Spider只负责爬取单个页面,而分页等操作则由Crawler来完成:/***@Constructor*@paramconfig*@paramextra*/constructor(extra:ExtraType){super();额外的&&(this.extra=extra);}initialize(){//构建所有爬虫letrequests=[];for(leti=startPage;i{if(!Array.isArray(announcements)){thrownewError("爬虫连接失败!");}returnannouncements.map(announcement=>({url:`http://ggzy.njzwfw.gov.cn/${announcement.href}`}));}).setSpider(newUAContentSpiderr(this.extra));}}爬虫最重要的就是initialize函数,它需要完成爬虫的初始化。首先,我们需要构建所有的种子链接,即多个列表页面;然后通过setSpider方法添加对应的spider。不同的爬虫使用一个自定义的Transformer函数,从之前的结果中提取出需要的链接,传递给下一个爬虫。到目前为止,已经定义了我们的爬虫网络的关键组件。本地运行定义好Crawler后,我们就可以通过将爬虫注册到CrawlerScheduler来运行爬虫了:Announcement-Serviceclass",menuCode:"001001/001001001/00100100100",category:"1"});crawlerScheduler.register(uaCrawler);dcEmitter.on("StoreChange",()=>{console.log("----------"+newDate()+"------------");console.log(store.crawlerStatisticsMap);});crawlerScheduler.run()。然后(()=>{});这里的dcEmitter是整个状态的中转站。如果您选择使用本地操作,您可以自己监控dcEmitter中的事件:----------WedApr19201722:12:54GMT+0800(CST)------------{UACrawler:CrawlerStatistics{isRunning:true,spiderStatisticsList:{UAListSpider:[Object],UAContentSpider:[Object]},instance:UACrawler{name:'UACrawler',displayName:'UniversalAnnouncementCrawler',spiders:[对象],转换:[对象],请求:[对象],isRunning:true,额外:[对象]},lastStartTime:2017-04-19T14:12:51.373Z}}服务器运行我们也可以将爬虫作为服务运行:constcrawlerScheduler:CrawlerScheduler=newCrawlerScheduler();letuaCrawler=newUACrawler({module:"jsgc",name:"房建市政招标公告-服务类",menuCode:"001001/001001001/00100100100",category:"1"});crawlerScheduler.register(uaCrawler);newCrawlerServer(crawlerScheduler).run().then(()=>{},(error)=>{console.log(error)});此时会启动框架内置的Koa服务器,让用户通过RESTful接口控制爬虫网络,获取当前状态接口说明KeyFieldsCrawler//判断爬虫是否运行isRunning:boolean=false;//爬虫最后激活时间lastStartTime:Date;//爬虫最后一次运行的结束时间lastFinishTime:Date;//爬虫最后一次异常信息lastError:Error;Spider//最后一次运行时间lastActiveTime:Date;//平均总执行时间/msexecuteDuration:number=0;//爬虫统计count:number=0;//异常timesstatisticserrorCount:number=0;countByTime:{[number]:number}={};http://localhost:3001/获取当前爬虫还未启动的运行状态[{name:"UACrawler",displayName:"通用公告爬虫",isRunning:false,}]正常返回[{name:"UACrawler",displayName:"通用公告爬虫",isRunning:true,lastStartTime:"2017-04-19T06:41:55.407Z"}]发生错误[{name:"UACrawler",displayName:"Generalannouncementcrawler",isRunning:true,lastStartTime:"2017-04-19T06:46:05.410Z",lastError:{spiderName:"UAListSpider",message:"抓取超时",url:"http://ggzy.njzwfw.gov.cn/njggzy/jsgc/001001/001001001/001001001001?Paging=1",时间:"2017-04-19T06:47:05.414Z"}}]http://localhost:3001/start启动爬虫{message:"OK"}http://localhost:3001/status返回当前系统状态{"cpu":0,"memory":0.9945211410522461}http://localhost:3001/UACrawler根据爬虫名称查看爬虫运行状态[{"name":"UAListSpider","displayName":"通用公告列表蜘蛛","count":6,"countByTime":{"0":0,"1":0,"2":0,"3":0,..."58":0,"59":0},"lastActiveTime":"2017-04-19T06:50:06.935Z","executeDuration":1207.4375,"errorCount":0},{"name":"UAContentSpider","displayName":"通用公告内容蜘蛛","count":120,"countByTime":{"0":0,..."59":0},"lastActiveTime":"2017-04-19T06:51:11.072Z","executeDuration":1000.1596102359835,"errorCount":0}]自定义CrawlerServer提供的监控接口RESTfulAPI用于返回当前爬虫的状态信息,我们可以使用React或者其他框架快速搭建监控接口