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

基于Apify+node+react-vue搭建有趣的爬虫平台

时间:2023-04-03 10:58:27 Node.js

前言熟悉我的朋友可能知道我没写过热点。为什么不?是因为我不注意热点吗?其实并不是。我对一些事情还是很关心的,确实有很多想法和看法。但我一直奉行一个原则,那就是:以活力为知足。本文介绍的内容来自笔者之前负责研发的爬虫管理平台。专门抽象出一个相对独立的功能模块来讲解如何使用nodejs开发自己的爬虫平台。文章涵盖了很多知识点,包括nodejs,作者会用尽可能通俗易懂的语言向大家介绍爬虫框架、父子进程及其通信、react和umi。您将获得Apify框架的介绍和基本用法如何创建父子进程和父子进程通信使用javascript手动实现控制最大并发爬虫数拦截整个网页图片使用nodejs实现方案使用umi3+antd4.0搭建爬虫前端界面平台预览上图是我们要实现的爬虫平台,我们可以输入指定的url来抓取网站下的数据,生成一个第三方库和模块整个网页的快照。抓拍完成后,我们就可以下载数据和图片了。页面右侧是用户抓取的记录,方便二次使用或备份。在开始本文之前,我们需要先了解一下爬虫的一些应用。我们一般所知道的爬虫大多用于爬取网页数据、抓取请求信息、网页截图等,如下图所示:当然,爬虫的应用远不止于此,我们还可以使用爬虫库进行自动化测试、服务器端渲染、自动表单提交、测试Google扩展、性能诊断等。任何语言实现的爬虫框架的原理往往是相似的。接下来笔者将介绍基于nodejs的爬虫框架Apify及其使用方法,并通过一个实用的案例方便大家快速上手爬虫开发。Apify框架介绍和基本使用apify是一个可扩展的JavaScript网络爬虫库。能够使用无头Chrome和Puppeteer开发数据提取和Web自动化作业。它提供用于管理和自动缩放无头Chrome/Puppeteer实例池的工具,支持维护目标URL请求队列,并可以将抓取的结果存储在本地文件系统或云端。我们安装使用起来非常简单,官网上也有很多例子可以参考。具体安装和使用步骤如下:安装npminstallapify--save用Apify开始第一种情况constApify=require('apify');Apify.main(async()=>{constrequestQueue=awaitApify.openRequestQueue();awaitrequestQueue.addRequest({url:'https://www.iana.org/'});constpseudoUrls=[newApify.PseudoUrl('https://www.iana.org/[.*]')];constcrawler=newApify.PuppeteerCrawler({requestQueue,handlePageFunction:async({request,page})=>{consttitle=awaitpage.title();console.log(`Titleof${request.url}:${title}`);awaitApify.utils.enqueueLinks({page,selector:'a',pseudoUrls,requestQueue,});},maxRequestsPerCrawl:100,maxConcurrency:10,});awaitcrawler.run();});使用node执行后,可能会出现如下界面:程序会自动打开浏览器,打开符合条件的url页面。我们还可以使用它提供的cli工具实现更方便的爬虫服务管理等功能,有兴趣的朋友可以试试看。apify提供了许多有用的API供开发者使用,如果想实现更复杂的能力,可以研究一下。下图是官网api的截图:我要实现的爬虫主要是使用了Apify集成的Puppeteer能力。如果对Puppeteer不熟悉,可以去官网了解一下。列出项目使用的技术框架的文档地址。如何创建父子进程以及父子进程通信如果我们要实现一个爬虫平台,需要考虑的一个关键问题是何时以及如何执行爬虫任务。因为爬取网页和截图需要在所有网页加载完成后进行处理,以保证数据的完整性,所以我们可以认为这是一个耗时的工作。当我们使用nodejs作为后台服务器时,由于nodejs本身是单线程的,当爬取请求传递给nodejs时,nodejs必须等待这个“耗时任务”完成才能处理其他请求,这会导致其他页面上的请求等待任务完成再进行,所以为了更好的用户体验和流畅的响应,我们不考虑多进程处理。好在nodejs设计支持子进程,我们可以将爬虫等耗时任务放到子进程中处理,子进程处理完成后通知主进程。整个过程如下图所示:nodejs有3种创建子进程的方式,这里我们使用fork来处理,具体实现如下://child.jsfunctioncomputedTotal(arr,cb){//耗时计算任务}//与主进程通信//监听主进程信号process.on('message',(msg)=>{computedTotal(bigDataArr,(flag)=>{//发送一个完成信号给主进程process.send(flag);})});//main.jsconst{fork}=require('child_process');app.use(async(ctx,next)=>{if(ctx.url==='/fetch'){constdata=ctx.request.body;//通知子进程开始执行任务,并传入数据constres=awaitcreatePromisefork('./child.js',data)}//创建异步线程函数creatPromisefork(childUrl,data){//加载子进程constres=fork(childUrl)//通知子进程开始工作data&&res.send(data)returnnewPromise(reslove=>{res.on('message',f=>{reslove(f)})})}awaitnext()})以上是实现父子进程通信的简单案例,我们的爬虫服务也会采用这种模式来实现。使用javascript手动控制爬虫的最大并发以上就是实现我们的爬虫应用需要考虑的技术问题。接下来正式实现业务功能。因为爬虫任务是在子流程中进行的,所以我们会在子流程代码中实现我们的爬虫功能。我们先梳理一下具体的业务需求,如下图:j'接下来我先解决控制爬虫最大并发数的问题。之所以解决这个问题,是为了考虑爬虫的性能。我们不能让爬虫抓取所有的网页,会启动很多并行进程进行处理,所以我们需要设计一个节流装置来控制每次的并发数。当前一个完成后,将获取下一批页面。具体代码实现如下://异步队列constqueue=[]//最大并发数constmax_parallel=6//起始指针letstart=0for(leti=0;i{if(document.scrollingElement){letprevScroll=document.scrollingElement.scrollTop;document.scrollingElement.scrollTop=prevScroll+scrollStep;letcurScroll=document.scrollingElement.scrollTop返回{prevScroll,curScroll}}},scrollStep);//等待3秒后继续滚动页面,让页面完全加载awaitsleep(3000);}//其他业务代码...//网页截图,并设置图片质量和保存路径constscreenshot=awaitpage.screenshot({path:`static/${uid}.jpg`,fullPage:true,质量:70});爬虫代码的其他部分不是重点,这里就不一一举例了。我已经放在github上了,大家可以交流研究。如何提取网页文本也有现成的api可以调用,您可以选择适合自己业务的api进行应用。这里我以puppeteer的page.$eval为例:consttxt=awaitpage.$eval('body',el=>{//el为DOM节点,可以修改body的Extract子节点,解析return{...}})nodejs第三方库和模块的使用为了搭建一个完整的节点服务平台,作者使用了koa这个轻量级可扩展的节点框架,glob使用强大的正则匹配模式遍历文件koa2-cors来处理有跨域访问问题的koa-static创建静态服务目录koa-body获取请求体数据如何使用这些模块实现一个完整的服务端应用,作者在代码中已经做了详细的说明,这里不再赘述一一讨论。具体代码如下:constKoa=require('koa');const{resolve}=require('path');conststaticServer=require('koa-static');constkoaBody=require('koa-body');constcors=require('koa2-cors');constlogger=require('koa-logger');constglob=require('glob');const{fork}=require('child_process');constapp=newKoa();//创建一个静态目录app.use(staticServer(resolve(__dirname,'./static')));app.use(staticServer(resolve(__dirname,'./db')));app.use(koaBody());app.use(logger());constconfig={imgPath:resolve('./','static'),txtPath:resolve('./','db')}//设置跨域app.use(cors({origin:function(ctx){if(ctx.url.indexOf('fetch')>-1){return'*';//允许来自所有域名的请求}return'';//这样只允许来自域名http://localhost的请求},exposeHeaders:['WWW-Authenticate','Server-Authorization'],maxAge:5,//该字段可选,用于指定本次预检请求的有效期,单位为秒credentials:true,allowMethods:['GET','POST','PUT','DELETE'],allowHeaders:['Content-Type','Authorization','Accept','x-requested-with'],}))//创建异步线程函数createPromisefork(childUrl,data){constres=fork(childUrl)data&&res.send(data)returnnewPromise(reslove=>{res.on('message',f=>{reslove(f)})})}app.use(async(ctx,next)=>{if(ctx.url==='/fetch'){constdata=ctx.request.body;constres=awaitcreatePromisefork('./child.js',data)//获取文件路径路径常量txtUrls=[];让reg=/.*?(\d+)\.\w*$/;glob.sync(`${config.txtPath}/*.*`).forEach(item=>{if(reg.test(item)){txtUrls.push(item.replace(reg,'$1'))}})ctx.body={state:res,data:txtUrls,msg:res?'抓取完成':'抓取失败,原因可能是非法url或请求超时或服务器内部错误'}}awaitnext()})app.listen(80)使用umi3+antd4.0搭建前端界面ofthecrawlerplatform爬虫平台的前端界面笔者使用umi3+antd4.0进行开发,因为antd4.0相比之前的版本在体积和性能上确实提升了很多,同时也做了更合理的处理拆分组件。由于前端页面实现起来比较简单,整个前端代码使用hooks编写不到200行,这里就不一一介绍了。可以在作者的github上学习研究。github项目地址:基于Apify+node+react的一个有趣的爬虫平台接口如下:可以自己clone到本地运行,也可以基于此开发自己的爬虫应用。项目使用的技术文档地址为apify一个可扩展的JavaScript网络爬虫库Puppeteerkoa--基于nodejs平台的下一代web开发框架最后,如果你想学习更多的前端知识和实战H5游戏、webpack、node、gulp、css3、javascript、nodeJS、canvas数据可视化等,欢迎在《趣谈前端》专栏学习讨论,一起探索前端边界