基于Apify+node+react/vue搭建一个有点意思的爬虫平台

前言

熟悉我的朋友可能会知道,我一向是不写热点的。为什么不写呢?是因为我不关注热点吗?其实也不是。有些事件我还是很关注的,也确实有不少想法和观点。但我一直奉行一个原则,就是:要做有生命力的内容

本文介绍的内容来自于笔者之前负责研发的爬虫管理平台, 专门抽象出了一个相对独立的功能模块为大家讲解如何使用nodejs开发专属于自己的爬虫平台.文章涵盖的知识点比较多,包含nodejs,爬虫框架,父子进程及其通信,reactumi等知识, 笔者会以尽可能简单的语言向大家一一介绍.

你将收获

Apify框架介绍和基本使用

如何创建父子进程以及父子进程通信

使用javascript手动实现控制爬虫最大并发数

截取整个网页图片的实现方案

nodejs第三方库和模块的使用

使用umi3 + antd4.0搭建爬虫前台界面

平台预览

上图所示的就是我们要实现的爬虫平台, 我们可以输入指定网址来抓取该网站下的数据,并生成整个网页的快照.在抓取完之后我们可以下载数据和图片.网页右边是用户抓取的记录,方便二次利用或者备份.

正文

在开始文章之前,我们有必要了解爬虫的一些应用. 我们一般了解的爬虫, 多用来爬取网页数据, 捕获请求信息, 网页截图等,如下图:

当然爬虫的应用远远不止如此,我们还可以利用爬虫库做自动化测试,服务端渲染,自动化表单提交,测试谷歌扩展程序,性能诊断等.任何语言实现的爬虫框架原理往往也大同小异, 接下来笔者将介绍基于nodejs实现的爬虫框架Apify以及用法,并通过一个实际的案例方便大家快速上手爬虫开发.

Apify框架介绍和基本使用

apify是一款用于JavaScript的可伸缩的web爬虫库。能通过无头(headlessChromePuppeteer实现数据提取和** Web** 自动化作业的开发。  它提供了管理和自动扩展无头Chrome / Puppeteer实例池的工具,支持维护目标URL的请求队列,并可将爬取结果存储到本地文件系统或云端。

我们安装和使用它非常简单, 官网上也有非常多的实例案例可以参考, 具体安装使用步骤如下:

安装

npm install apify --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(`Title of${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)  }// 创建异步线程functioncreatePromisefork(childUrl, data){// 加载子进程constres = fork(childUrl)// 通知子进程开始workdata && res.send(data)returnnewPromise(reslove=>{        res.on('message', f => {            reslove(f)        })    })    }awaitnext()})复制代码

以上是一个实现父子进程通信的简单案例, 我们的爬虫服务也会采用该模式来实现.

使用javascript手动实现控制爬虫最大并发数

以上介绍的是要实现我们的爬虫应用需要考虑的技术问题, 接下来我们开始正式实现业务功能, 因为爬虫任务是在子进程中进行的,所以我们将在子进程代码中实现我们的爬虫功能.我们先来整理一下具体业务需求, 如下图:

>need-to-insert-img

j'接下来我会先解决控制爬虫最大并发数这个问题, 之所以要解决这个问题, 是为了考虑爬虫性能问题, 我们不能一次性让爬虫爬取所以的网页,这样会开启很多并行进程来处理, 所以我们需要设计一个节流装置,来控制每次并发的数量, 当前一次的完成之后再进行下一批的页面抓取处理. 具体代码实现如下:

// 异步队列constqueue = []// 最大并发数constmax_parallel =6// 开始指针letstart =0for(leti =0; i < urls.length; i++) {// 添加异步队列queue.push(fetchPage(browser, i, urls[i]))if(i &&      (i+1) % max_parallel ===0|| i === (urls.length -1)) {// 每隔6条执行一次, 实现异步分流执行, 控制并发数awaitPromise.all(queue.slice(start, i+1))    start = i  }}复制代码

以上代码即可实现每次同时抓取6个网页, 当第一次任务都结束之后才会执行下一批任务.代码中的urls指的是用户输入的url集合,fetchPage为抓取页面的爬虫逻辑, 笔者将其封装成了promise.

如何截取整个网页快照

我们都知道puppeteer截取网页图片只会截取加载完成的部分,对于一般的静态网站来说完全没有问题, 但是对于页面内容比较多的内容型或者电商网站, 基本上都采用了按需加载的模式, 所以一般手段截取下来的只是一部分页面, 或者截取的是图片还没加载出来的占位符,如下图所示:

所以为了实现截取整个网页,需要进行人为干预.笔者这里提供一种简单的实现思路, 可以解决该问题. 核心思路就是利用puppeteer的api手动让浏览器滚动到底部, 每次滚动一屏, 直到页面的滚动高度不变时则认为滚动到底部.具体实现如下:

// 滚动高度letscrollStep =1080;// 最大滚动高度, 防止无限加载的页面导致长效耗时任务letmax_height =30000;letm = {prevScroll:-1,curScroll:0}while(m.prevScroll !== m.curScroll && m.curScroll < max_height) {// 如果上一次滚动和本次滚动高度一样, 或者滚动高度大于设置的最高高度, 则停止截取m =awaitpage.evaluate((scrollStep) =>{if(document.scrollingElement) {letprevScroll =document.scrollingElement.scrollTop;document.scrollingElement.scrollTop = prevScroll + scrollStep;letcurScroll =document.scrollingElement.scrollTopreturn{prevScroll, curScroll}      }    }, scrollStep);// 等待3秒后继续滚动页面, 为了让页面加载充分awaitsleep(3000);}// 其他业务代码...// 截取网页快照,并设置图片质量和保存路径constscreenshot =awaitpage.screenshot({path:`static/${uid}.jpg`,fullPage:true,quality:70});复制代码

爬虫代码的其他部分因为不是核心重点,这里不一一举例, 我已经放到github上,大家可以交流研究.

有关如何提取网页文本, 也有现成的api可以调用, 大家可以选择适合自己业务的api去应用,笔者这里拿puppeteerpage.$eval来举例:

consttxt =awaitpage.$eval('body', el => {// el即为dom节点, 可以对body的子节点进行提取,分析return{...}})复制代码

nodejs第三方库和模块的使用

为了搭建完整的node服务平台,笔者采用了

koa 一款轻量级可扩展node框架

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'],}))// 创建异步线程functioncreatePromisefork(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)// 获取文件路径consttxtUrls = [];letreg =/.*?(\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搭建爬虫前台界面

该爬虫平台的前端界面笔者采用umi3+antd4.0开发, 因为antd4.0相比之前版本确实体积和性能都提高了不少, 对于组件来说也做了更合理的拆分. 因为前端页面实现比较简单,整个前端代码使用hooks写不到200行,这里就不一一介绍了.大家可以在笔者的github上学习研究.

github项目地址:基于Apify+node+react搭建的有点意思的爬虫平台

界面如下:

大家可以自己克隆本地运行, 也可以基于此开发属于自己的爬虫应用.

项目使用的技术文档地址

apify一款用于JavaScript的可伸缩的web爬虫库

Puppeteer

koa-- 基于nodejs平台的下一代web开发框架

最后

如果想学习更多H5游戏,webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。

作者:徐小夕

链接:https://juejin.im/post/5ebcbec96fb9a0437055c5ac

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342