天天看点

项目航海项目航海–A2D产品的前世今生

项目航海–A2D产品的前世今生

撰写时间2018年8月10日 星期五 上午10:59至2018年8月16日 星期四 下午16:40

  本文不同于作者之前写过的任何博文,是作者纯粹的研发经历和思路手书,以前的技术博文或多或少会摘取网络上的产品思路和技术想法,而此文更像是一个从无到有的产品经历,而非产品介绍或者技术讲解。作者本身是一个从业两年出头三年不到,脱离技术小白不久却离大牛之路遥遥无期的技术人员,有过六七个大型项目的研发经历,带过三四个完整的中小型项目,自认头脑不算聪慧却还灵活,受限于从业经历相对简单,时限较短,眼光还尚显稚嫩,故而纯手书的此文或多或少会存在一些技术问题和产品设计漏洞,希望阅读此文的读者可以谅解,如果有好的想法和提议,欢迎和作者进行交流,促进共同的进步,在此表达感谢!

                                   —-前言

  浏览本文,首要必须搞清楚几个核心问题,A2D是什么,爬虫技术应用于何处,工具本身的行为出发点在哪,产品的切入点为何?

  要解答这些问题,作者思前想后很久,还是得先从产品的来源和最初的需求说起。

  A2D这个命名来源于鄙公司2017年一个几近失败的项目,该项目采用仿制市面上流行的某python爬虫工具,进行了java重写和功能重塑,技术核心部分中对于验证登录是做了如下处理:

1.对于不需要登录的站点,无需填写cookie;

2.对于需要登录的系统,则是采用填写登录信息后从浏览器去直接复制cookie,然后填进工具避开登录。核心爬取功能则是通过用户填写正则表达式,从页面中截取出本次采集所需要的站点数据,然后进行数据持久化。

该产品存在三个巨大的设计问题:

1.如果采集的系统模块过于庞大繁杂,会出现内存溢出,系统卡死,用户体验性差;

2.非技术人员如果并未经过系统培训,不清楚如何从浏览器获取cookie并复制进工具,不知道如何判断正则以及编写正则,就无法获取到想要的数据,用户体验过于复杂难懂,学习成本高。

3.由于工具本身只获得数据,导致UI及其简单,从产品设计的角度看,拓展延伸性差,市场推广难度大。

  基于高层对于该产品的满意度低,研发中心对于A2D提出了新的产品研发要求,将需求进行了重新整理,并给出了以下目标:

1.项目针对主要目标为政务系统,需要绕行登录验证(所采集系统提供登录用户名密码);

2.采集的数据为系统各个模块的表格头部信息(所采集系统提供各个模块的url);

3.对于采集下来的表头信息进行预处理(转化为政务系统中的信息资源信息项);

4.尝试进行自动化采集的设计(定时进行系统模块表头信息的获取,由于cookie存在过期的情况,采用cookie登录的爬虫系统无法尝试在无人使用的情况下采集数据)。

  由此,才有了这个产品接下去的一系列故事。

  在接到这个产品之初,作者以及作者的团队成员从来没有接触过爬虫这类概念,使得作者闹了盲人摸象的设计错误,一度按照传统政务系统的方式进行项目管理和产品设计,在业务架构和工具构成上犯了众多常识性错误,再加上当时需求的紧迫度很高以及一些现实状况(疲于应付上级的检查)的干扰,导致没有合理运用投石问路的方式进行前期项目风险评估,也没有全方位针对现在主流的工具进行分析,使得产品设计初期的业务架构非常的脆弱不稳定,简单无亮点。

  起初经过不到两天的紧张调研和讨论后,采用了最初的结构划分和业务流程:

当时的系统设计分为三个模块:

1.登录绕行模块;

2.页面爬取模块;

3.内容处理模块。

项目航海项目航海–A2D产品的前世今生

  当作者以及团队成员饱含信心开始进行功能细化,准备进行工具开发时,登时就被泼了一盆冷水,在登录绕行模块,就受到了阻碍,自以为合理的登录绕行并没有一个可行的方案,以下列举出当时作者和副手商量的几个尝试性方案:

1.在工具的站点配置功能,将目标系统的登录页内置进来,输入登录信息后请求登录,一旦成功则通过某种方式自动从浏览器去读取当前可用的cookie,保存后完成登录的绕行;

2.避免获取cookie的方式,爬取登录页的用户名密码验证码的字段名,在配置页面让用户输入用户名密码验证码,试图从请求后拼接username和password来进行后端模拟登录。

  第二个方案从一开始就被确认为几乎不可行,因为我们没法真正绕过附骨之蛆的验证码,就是这个讨厌的验证码让作者耗尽心思,对于爬虫来说,验证码就像是杀虫剂。而且,并非所有系统的登录采用的请求方式都是一样的,我们是无法去操作我们的目标系统,这就导致这个方案无疾而终了。

  那就尝试第一个方案吧,在当时的情况下做出了无奈之举,作者和副手认真研究了市面上的八抓鱼,发现它可以做到类似于这个方案的效果,这让当时的我们欣喜若狂,但是这个欢喜持续了不到两天就消失了,这个方式完美得避开了验证码,却套入了浏览器的大坑中,为了做安全控制,防止xss攻击,浏览器有内置的httponly属性,而我们无法从前端更改它,甚至读取不了此属性,借用其他第三方的浏览器插件强制更改后的成功更是让当时的我们接受不了,甚至于当时的作者打算产品中加上一个插件下载链接和使用手册(这当然是开玩笑的,并不可行),对此,作者得出的结论是八抓鱼与其说是一个爬虫工具,更像是一个套着工具外壳的自制简化版浏览器,站在上帝的视角处理打开的网页,执行想要的操作然后获取想要的数据,几乎很少存在限制(此处评论是来自于作者短浅的见解),而我们却无法像它一样去做一个自己的浏览器,让人徒呼奈何。之后我们又发现了一个问题,即使我们有了cookie中的jsessionid,也可能无法处理单用户登录的系统,当然这个并没有做深入尝试,只是提出了一个猜想,因为当时我们的测试遇到了一个和url相关的问题,并且这个问题让人印象深刻。

  问题是由于我们所测试系统是鄙公司开发的政务服务系统,我们开始模拟用户使用环境下系统的可行性,但是,当我们尝试第一个系统时,问题就出来了,当我们登录系统,人工选择其中一个模块想要获取它的url时,发现浏览器地址栏并未发生变化,我们明白这是系统本身的过滤器起到了作用,那么从f12直接去找前端的模块路径总可以吧,结果让人啼笑皆非,我们尝试在浏览器输入得到的模块完整url,结果却得不到iframe中的页面,这个结果让作者一头雾水,开发过程中我们往往会在浏览器输入get请求的方法测试自己的对外接口是否正确,为何会无结果,经过一系列艰苦卓绝(毫无头绪进行中,作者以及副手都是java后台程序员出身,前端技能相对薄弱)地排查,发现原来我们所测试系统的前端在页面渲染前,对字典数据进行了一次初始化加载,将其放在了iframe外,如果是整个系统是可以显示的,但是如果是只取iframe中的一部分,会导致下拉选择的控制器读取字典值失败,让前端程序死在了胚胎中。这个排查结果大大打击了当时的研发情绪,怀疑论一度弥漫开来。

  如果前端有各种各样奇奇怪怪的数据处理方式,而且各种框架进阶速度那么快,怎么保证我们的产品有足够的容错率。直至今日,这个问题仍然是我思考的一个重大方向,且没有啥突破性进展,只能自己安慰自己,任何技术都是存在时效性的。

  好在作者以及副手当时对于登录模拟方式,在交流中迸发了灵光一现,才使得项目进程出现了一丝转机。经过一下午的研讨以及尝试,我们提出了第三个方案,之后陆陆续续修正过,但是当时的大纲并未发生变化,方案如下:

3.同样需要绕开cookie,先爬取登录页,获取到用户名密码验证码的精准定位,得到输入的用户名密码验证码,然后采用PhantomJS模拟浏览器进行机器模拟人为登录操作。

  这里简单介绍下PhantomJS,PhantomJS是一个无界面的,可脚本编程的WebKit浏览器引擎。它原生支持多种web 标准:DOM 操作,CSS选择器,JSON,Canvas 以及SVGPhantomJS是一个无界面的,可脚本编程的WebKit浏览器引擎。它原生支持多种web 标准:DOM 操作,CSS选择器,JSON,Canvas 以及SVG。简单来说,PhantomJS就是一个可编程的无头浏览器.而无头浏览器就是一个完整的浏览器内核,包括了js解析引擎,渲染引擎,请求处理等,但是不包括显示和用户交互页面的浏览器。

  介绍就到此,现在来说一下为何作者和副手会想到使用PhantomJS。为了对付惹人讨厌的验证码,那么我们就需要先获取到这个验证码的图片,无论是想要破解它或者做其他操作,这一步都是必不可少的(当然这句话可以再探讨下正确性),当时有一个同事偶然间提示验证码我们并不一定需要绕过去,我们是否可以让机器去进行识别填写,比如使用其他的打码平台。

  这个思路让我们眼前一亮,在提出这个想法之前,我们就曾经尝试过验证码的破解,当然,破解验证码是老生常谈的一个话题,验证码识别常规的处理方式有四种,1.人工智能2.模式识别3.机器视觉4.图像处理。这里简单介绍一下模式识别的原理和过程:

1.图像采集:验证码呢,就直接通过HTTP抓HTML,然后分析出图片的url,然后下载保存就可以了。

2.预处理:检测是正确的图像格式,转换到合适的格式,压缩,剪切出ROI,去除噪音,灰度化,转换色彩空间这些。

3.检测:主要是找出文字所在的主要区域。

4.前处理:“一般”要做文字的切割。

5.训练:通过各种模式识别,机器学习算法,来挑选和训练合适数量的训练集。不是训练的样本越多越好。过学习,泛化能力差的问题可能在这里出现。这一步不是必须的,有些识别算法是不需要训练的。

6.识别:输入待识别的处理后的图片,转换成分类器需要的输入格式,然后通过输出的类和置信度,来判断大概可能是哪个字母。识别本质上就是分类。

模式识别一般具有如图所示结构,主要包括预处理、特征提取、训练和识别几个部分,验证码识别系统也不例外。根据验证码的特点,研究表明验证码识别的难点和重点在于预处理,即去除噪声、干扰和字符分割。

项目航海项目航海–A2D产品的前世今生

            模式识别系统原理结构框图

  以下为作者尝试过的验证码识别技术的具体细节。

1.1预处理

预处理是在验证码学习和识别之前对验证码图片进行前期处理,主要包括Jpeg解码、二值化、去除噪声和干扰、字符分割、归一化等。预处理的好坏极大地影响到识别性能,其中去除干扰和字符分割尤为重要。

Jpeg解码:并非所有验证码的识别都需要进行Jpeg解码,但是本验证码图片为Jpeg格式,只有先经过Jpeg解码才能进行处理。采用GDI++进行Jpeg解码,通过调用相关函数,使Jpeg格式的验证码图片首先转换为bmp格式,暂存于临时文件中,然后再打开文件进行处理。

二值化:二值化是将验证码图片的灰度值,以某一阈值为限,转换为0或255,即黑和白,以便于处理。因为本验证码的字符灰度偏白,所以在进行二值化之前先进行了灰度反置,即让背景变白,让字符变黑。二值化阈值根据具体验证码分析所得,选择合理的阈值可消除很多背景、噪声,同时不损伤字符笔画。二值化阈值设为120,可以很好的去除背景。

去除噪声和干扰:二值化后大部分噪声都已经去除,但是还有很多干扰线,不去除这些干扰线就不可能进行后续的处理。通过对验证码干扰线的特点进行分析,发现干扰线很细,比字符笔画细很多,干扰线的高度为1个象素,干扰线相交处的高度一般为2个象素。首先去除了所有高度为1的点线,然后去除了高度为2且与高度为1的象素相通的点线。

字符分割:字符分割即把验证码图片分割成单个的字符,这是有效识别的基础。先利用种子填充算法得到几个连通线,这样未粘连的字符即可分割。对于粘连字符,还需要进一步分割。粘连字符的判别主要依据字符的点数和宽高比特征,大于某一阈值则初步判断为字符粘连。阈值根据验证码特征统计分析所得。对于初步判断为粘连的字符,为了防止判断错误,还用预识别的方法进行了进一步判断,如果能很好地识别也不认为是粘连字符。对于粘连字符的分割,本程序采用在垂直投影图中找谷点的方法进行分割。

归一化:验证码字符存在位置偏移、大小不一、旋转不定的特点,如果不进行归一化就不能很好识别。采用质心对齐和线性插值放大的方法进行归一化,使字符变为统一的规格,以便于进行匹配识别。

1.2特征提取

特征提取是从经过预处理的字符图片中,提取出一定维数的特征向量,这样能提高字符匹配和识别的存储量和运算速度。字符有很多特征,选用合适的特征才能达到正确识别的目的。采用字符的区域密度的特征,即将字符分为5*5的25个方格区域,计算每个方格中的点数与字符总点数之比,以得到25维特征向量。该特征反映了字符笔画的空间分布情况,并且对字符笔画的粗细不敏感。

1.3 训练

训练是从训练集验证码中提取出标准模板,即标准特征库的过程。采用大量验证码进行训练,使每个字符都有200个左右的标准模板。通过预处理和特征提取后,将训练集验证码的特征向量存入文件中。训练时需要指明各验证码的正确值。为了不出现错误的标准模板,对于分割时发现有字符粘连的训练集验证码不加入模板库。

1.4 识别

采用最近邻判别法进行识别,即将每个字符的特征向量与标准特征库中的特征向量进行匹配,与哪个字符的匹配最好就判别为哪个字符。具体匹配方法是计算特征向量之间的欧几里德距离,如以下公式所示:

Length=∑|CurrPara[i]−StdPara[i]|2 L e n g t h = ∑ | C u r r P a r a [ i ] − S t d P a r a [ i ] | 2

其中CurrPara为待识别字符的特征向量,StdPara为标准模板的特征向量,i=0,1,2,…,24。待识别字符与哪个标准模板的欧几里德距离最小,就表示和哪个模板最匹配,即判别为哪个字符。依次对验证码的各个字符进行识别,即可识别出验证码字符串。还返回了识别结果的可信度reliability。可信度依据验证码的最大最近邻距离计算所得,最大最近邻距离指验证码中各字符最近邻距离的最大值。根据多个阈值对最大最近邻距离进行分段,每个分段返回一个可信度。最大最近邻距离的阈值根据验证码识别情况统计分析所得。Reliability=80表示识别结果80%可信。测试表明Reliability≥80时有很高的可信度。

  介绍到此为止,当然作者并非此中高手,具体的算法也是从他人已有的demo中获取的(在这找不到具体的出处,实在不好意思),但是这种方式经过我们的测试,正确率会随着字母或者数字验证码的位数发生巨大差异,4位的数字准确率能达到67%,而6位的只有可怜的46%了,加上字母后更加悲惨,o和0的区别还有复杂横线等干扰都会大幅度干扰命中率,在这种情况下,我们一直避开考虑验证码,但是事实证明我们过于偏执。在得到新的想法后,经过一番搜寻,我们找到了几个打码平台,挣码以及超级鹰。

  这里首先介绍一下什么叫做打码,有个远程客户端把你所要打的码通过某个平台客户端来进行的人工代打,平台则负责分发和审核。这种被称之为人工智能的方式却很好得解决了我们的问题,对于简单验证码可以达到秒级回应和高到95%以上的正确率,经过再三的尝试和研讨后,作者决定采用某一个打码平台来解决我们某些破解不了验证码的问题。

  最开始作者想使用挣码,但是找了大半天,也没有找到这个平台的javaAPI,而超级鹰这个平台有完整的javaAPI,那么还犹豫什么呢,超级鹰(chaojiying)的集成接入马上提上了日程。

  说了这么多,回到最初的情形,为何会使用PhantomJS呢,原因很离奇,我们接入chaojiying后发现了一个很有意思的问题,我们直接取到的验证码图片发送给平台后,得到的结果都是错误的,那么原因是什么呢?因为图片太大了,为此作者和chaojiying的客服对于这个问题缠斗了好久,现在回想起来还真是滑稽的问题啊。为了解决图片过大的问题,我们开始搜寻java的自动截屏程序(造轮子对于开发应用型产品来说是最后考虑的方式),结果不出意外的发现了PhantomJS,很多问题迎刃而解。

  在使用PhantomJS的过程中,首先接触到的是Selenium,这是个什么东西呢?

  Selenium是一系列基于Web的自动化工具,提供一套测试函数,用于支持Web自动化测试。函数非常灵活,能够完成界面元素定位、窗口跳转、结果比较。Selenium框架由多个工具组成,包括:Selenium IDE,Selenium RC,Selenium WebDriver和SeleniumRC。本文不详细介绍这些具体工具的使用。毕竟这个框架绝大部分使用场景都在于自动化测试,本文只简述在爬虫中的应用以及我们为何需要它。

  本文阅读到这,对于爬虫,不知道大家有没有发现有一个巨大的问题,就是动态网页。什么是动态网页?在此简单普及一下,就是网页最后的内容不是一开始就是完整的,而是等代码都加载完毕之后再执行一段js代码来补充网页的内容。比如说网页最后的内容是A,最初的代码是B,B里面包含一段js代码,这段代码执行之后可以产生C,这样B+C才等于A。而我们通过网络访问网页的url得到的只是B。这样做在很大程度上防止了一些简单爬虫的访问。

  js代码是依赖于浏览器的,所以我们使用了PhantomJS,通过它我们可以执行访问网页,执行js代码,得到的网页内容都可以通过自动化的程序得到。不过这些必须通过另一个框架来操作,也就是Selenium。在Selenium库里面有一个WebDriver的API。WebDriver有点像可以加载网站的浏览器,但是它也可以像其他的Selector对象一样用来查找页面元素,与页面上的元素进行交互(如:点击,发送文本等),以及执行其他动作来运行爬虫。

  到此,作者和副手终于有了出鞘的刀剑,可以大杀四方了,A2D项目也慢慢进入了一个稳定的开发流程,但是,接下去就真的一帆风顺了吗?故事还远远没有结束,困难就像跋山,翻过一座又入一座。看似解决了的模拟登录问题遇到了性能上的瓶颈,每一轮的测试过程都达到了20秒以上,这是灾难级的,用户有效的等待时间大约为3秒,超过7秒就会产生厌烦感,大大提高不满度,但是A2D单单一个模拟登录功能耗时就超过了合理时长的8,9倍,那么问题到底在何处呢?是超级鹰返回验证码太慢,还是模拟浏览器的代码有问题,亦或者是爬取框架的问题,这无疑给当时的作者产生了巨大的压力,作者和副手针对这个问题检查了全部的流程,终于找到了问题所在。超级鹰确实存在一定程度的性能损耗,第一在于对于复杂验证码的处理缓慢,第二在于判断错误的回馈接口存在重复的性能损耗,但是这些并不是导致模拟登录和蜗牛比速度的原因。真正的原因是phantomjs本身的问题,我们记录了phantomjs驱动的耗时,实例化一个对象需要近2秒的时间,而里面的显氏等待我们设为2.4秒,而我们模拟点击操作和加载其余代码的时间也大约在2秒左右,超过7秒执行一次,这还不算爬取页面,管道自动处理数据,持久化存储以及错误重载这些时间,绝望感油然而生。根据作者和副手的再三讨论,得出以下结论,诚如PhantomJS提供了一系列的API来让我们编程控制。这些API看似可以帮我们实现一些不易实现的功能,但是真的是屠龙刀倚天剑了吗?看一下phantomJS问题具体在哪:

1.单线程处理,Phantomjs在处理页面的时候,实际上同浏览器不同。它是单线程下载的。所以通常很慢。也就是说PhantomJS的加载速度并不完全等于用户访问页面的加载速度

2.JS嵌套,在Phantom中API本身提供的功能相当的有限。这时我们可能需要借助JS来实现一些功能。但是在JS中又不能引用Phantomjs的函数。调用起来极不方便

3.用于爬虫处理并没有想象中那么完美,PhantomJS的一个重要的功能就是获取JS执行后的内容。但是真的所有的都可以获取到么?其实不然,比如a标签是触发s函数来实现跳转的。js的实现是使用window.location.href=”xxx”.我们会发现 对于这种写法,我们是没有办法获取到请求地址的。a(按钮)标签点击后修改了当前页面,我们会发现也没有办法直接一次性得到所有的链接。

  第二第三个问题尚可接受,第一个问题是其内核原有的策略,作者和副手还没有能力去改变它,那么怎么提升性能和效率呢?我们项目的副手(也是核心技术开发工程师)提出了对象池的应用策略来优化性能,这个想法的起源在于每实例化一个驱动对象就要走完其完整的生命周期,那么我们能不能使用池技术进行生命周期的管理呢,事实上是可以的。这里简单介绍下对象池技术。

  Java对象的生命周期大致包括三个阶段:对象的创建,对象的使用,对象的清除。

  因此,对象的生命周期长度可用如下的表达式表示:T = T1 + T2 +T3.其中T1表示对象的创建时间,T2表示对象的使用时间,而T3则表示其清除时间。由此,我们可以看出,只有T2是真正有效的时间,而T1、T3则是对象本身的开销。对象池技术基本原理的核心有两点:缓存和共享,即对于那些被频繁使用的对象,在使用完后,不立即将它们释放,而是将它们缓存起来,以供后续的应用程序重复使用,从而减少创建对象和释放对象的次数,进而改善应用程序的性能。事实上,由于对象池技术将对象限制在一定的数量,也有效地减少了应用程序内存上的开销。

  虽然采用了对象池技术有效得消灭了驱动反复实例化和回收的性能损耗,但是功能综合的耗时还是超过了15秒,于是作者给出了第二个策略,业务核心模块功能的分离。

  分离操作是将模拟登录,爬取页面,数据处理三个部分通过流程控制分离出来,提高用户体验,让系统级操作通过用户级操作进行控制,分离后具体的流程变为:

项目航海项目航海–A2D产品的前世今生

  流程中将两次爬取的过程分离开来,让用户自己参与进系统,将原本超过15秒的超长等待变为两个不到7秒的等待动画享受时间,并且可以选择性操作这两个步骤,至此,模拟登录性能上的重大挑战告一段落了。在这个过程中,有一个技术被反复得提到,这就是xpath提取技术。那么这又是什么呢?

  XPath是一门在XML文档中查找信息的语言。XPath用于在XML文档中通过元素和属性进行导航,并且可用来在XML文档中对元素和属性进行遍历,结构关系包括父、子、兄弟、先辈、后代等。爬虫程序的核心是对网页进行解析,从中提取出自己想要的信息数据。这些数据可能是网址(url、href)、图片(image)、文字(text)、语音(MP3)、视频(mp4、avi……),它们隐藏在网页的html数据中,在各级等级分明的element里面,通常是有迹可循的。我们传统去提取页面中的有效信息采用的是正则表达式,而正则表达式却无法让我们心满意足地精准定位到具体的元素,但是只要取到了页面中我们所需元素的xpath,我们就能通过它提取出我们真正想要的信息。于是,我们引入了xpath以及它的信息提取技术。

  文章进行到此,有一个小的插曲要简单讲述下,因为项目前期工期紧张,作者项目组的前端获取xpath并未按照作者预想的监听键盘的起落事件和鼠标的点击事件,在模拟登录页面采用了form下的各个元素拼出xpath,这样就存在一个严重问题,如果采集的登录页面没有form元素,而是采用div或者a标签,那么就找不到元素,当然也无法拼出想要的xpath了。为此系统模拟登录的命中率就大幅度降低。

  这当然是一个小插曲,但是却是A2D最致命的问题,模拟登录的命中成功率和采集到数据准确率,截止到本文撰写时,这个问题仍然在不断得进行优化处理。

  项目终于在一个问题接一个问题解决的过程中,慢慢步入了正轨,一开始的担忧和惊慌已经消减,团队成员的效率也出现了很大程度得提高,行文至此,作者一直避而不谈一个最核心的技术框架,那就是我们的基础爬虫框架webmagic,为何到现在才提起这个技术,主要原因是如果将这个框架像前文提到的那些技术一样结合项目本身分析刨解,文章就脱离了原先的初衷,所以这里我们就简要介绍下这个框架。

  一个好的框架必然凝聚了领域知识。WebMagic的设计参考了业界最优秀的爬虫Scrapy,而实现则应用了HttpClient、Jsoup等Java世界最成熟的工具,它几乎使用Java原生的开发方式,只不过提供了一些模块化的约束,封装一些繁琐的操作,并且提供了一些便捷的功能。WebMagic最大的特点就是微内核和高可扩展性,WebMagic的核心并不复杂,主要是将这些组件结合并完成多线程的任务。

  WebMagic的核心在webmagic-core包中,其他的包可以理解为对WebMagic的一个扩展。WebMagic以扩展的方式,实现了很多可以帮助开发的便捷功能。例如基于注解模式的爬虫开发,以及扩展了XPath语法的Xsoup等,而这些功能在WebMagic中是可选的。WebMagic由Downloader、PageProcessor、Scheduler、Pipeline四大组件,通过Spider将它们彼此组织起来构成的。这四大组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。Spider可以认为是一个大的容器,同时也是WebMagic逻辑的核心。这也是为何作者的项目名命名为spider的缘故。下图是webmagic的技术架构图:

项目航海项目航海–A2D产品的前世今生

四大组件介绍如下:

1.Downloader

Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了apache httpclient作为下载工具。

2.PageProcessor

PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。在这四个组件中,PageProcessor对于每个站点每个页面都不一样,所以团队的核心技术工程师(副手)对这一部分进行了定制开发。

3.Scheduler

Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。

4.Pipeline

Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。这里有A2D开发过程中的另一个小插曲,副手实现完pipeline的数据处理后发现并不能直接将数据返回给调用的service,那么就无法让数据得到展示,而设计之初,第一次爬取到的目标页数据是需要进行一些选择和信息处理的,那么得不到展示的数据就使得这个功能泡了汤。其实问题的原因是WebMagic默认只提供了“输出到控制台”和“保存到文件”两种结果处理方案。当时副手提议改造pipeline使得数据能得到回调,这样做确实能实现功能,并且不会破坏本身的程序结构,但是作者细思后拒绝了这种涉及面极大的改造,于是有了以下的数据流转思路:

项目航海项目航海–A2D产品的前世今生

思路的核心是将数据库作为一个中间媒介,类似于文件的效果,因为对于临时数据我们也需要有一个进行统计版本的预留选择,多调用一次数据库虽然会损失一些性能,但是带来的好处也是不言而喻的。

  讲过四大组件,需要介绍下WebMagic的核心部分,webmagic-core只包含爬虫基本模块和基本抽取器。webmagic-extension是WebMagic的主要扩展模块,提供一些更方便的编写爬虫的工具。包括注解格式定义爬虫、JSON、分布式等支持。同时WebmMgic提供与Selenium的结合,WebMagic依赖Selenium进行动态页面的抓取。提供了WebMagic与Saxon的结合。Saxon是一个XPath、XSLT的解析工具,webmagic依赖Saxon来进行XPath2.0语法解析支持。正是因为有了强大并且灵活的webmagic,作者对于A2D产品的健壮性和拓展性一直抱以极大的期望。

  行文至此,A2D开发需要用到的核心爬虫技术已经基本介绍完毕,因为在最初的需求中呈现的性能要求不高,所以并未考虑高并发,爬取数据量过大等情况,所以多线程,消息队列,内存处理以及缓存机制都并未在产品中得到应用。作者以及团队终于攻破了模拟登录和数据爬取两个核心模块,虽然至今或多或少仍然存在一些问题和不足,但是这不妨碍项目进入到了下一个阶段,数据处理,这个阶段虽然技术含量大幅度降低,但是对于产品设计和业务理解的要求却提高了甚多。由于鄙公司有目录系统,拥有一套完整的信息资源信息项的处理体系,作者在参考了系统的共通性后,重新调整了A2D的输出方式用以丰富产品的可用性。

  具体的调整思路主要为两步,第一步是通过转换采集到的表头数据,让其拥有信息项的元素特性,第二步是多次采集的结果需要一个标准的对比过程,那么设置一个模版制造版本模块和比对功能就成为可行。这就不得不介绍A2D第二个最为重要的亮点,自动化采集。首先作者抛出几个问题,何为自动化采集?自动化采集如何实现?自动化采集的难点和问题在哪?要解答这几个问题,作者先介绍一下定时任务在程序中的使用。

  定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券;比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。由于作者采用的系统基础框架是springboot2.0框架,对于实现简单的定时任务十分容易,只需要在我需要定时执行的功能方法上加上一个Scheduled注解就可以按照我设定的时间执行任务,但是仅仅拥有这些并不能实现我们自动化采集的功能,用户需要自主配置时间来执行对于某个站点的定时采集,为此我们引入了与spring无缝对接的定时任务框架Quartz。

项目航海项目航海–A2D产品的前世今生

  对于这个定时框架的具体实现作者就不在本文中赘述了,Quartz拥有强大的调度功能、灵活的使用方式、还具有分布式集群能力,每一个熟悉spring的读者在需要的时候恐怕都会想到它吧。在使用它的过程中,作者前前后后也碰到了不少问题,例如更改了数据库的定时任务信息并未让调度器进行重新扫描的问题,再例如配置的方式中如何将调度器交给spring的IOC去进行管理,还有锁问题等等,但是随着深入研究使用,这些问题都只是沿路的绊脚石而已,毕竟守得云开见月明嘛。

  写到这里,作者的A2D项目进展到了一个关键时期,那就是需要进行测试和试用了,我们尝试了鄙公司所开发的各套系统,得到的结果让作者还是得到了一些心理上的满足,绝大部分的系统和模块在我们一次次的尝试和修改中完成了它们数据的抽取,但是对于做了安全反爬以及那些久远的远古系统,A2D却无可奈何了起来,当一个登录页面中出现了7,8张干扰验证码获取的无用图片,当验证码并不是图而是一个跳转,当登录按钮不是一个按钮,而是一张图片时,模拟登录暴露了它的局限性,时至本文撰写,仍然没有特别的解决方式,命中率仍然是一个大考验。即使模拟登录成功,一些远古系统诡异的页面展示方式,并不准确的url路径,也让A2D的爬取难度重重,于是作者对于A2D的未来提出了更多的要求,同样这些要求也会在A2D的后续版本中一一得到实现:

1.爬取数据过于单一,仅仅限于表头的爬取对于产品本身,显得过于单薄,在后续版本开发中,将数据拓展到编辑框中的字段以及实体数据,而前端对于所爬数据的方式也将得到一些优化和提升,增加一定用户的人机交流,不完全过滤掉所爬页面的js事件。

2.爬取目标url获取过于专业,用户很难提供准确的模块路径,A2D将链接爬取融入系统中来,可以尝试站点模块的整体或者选择性爬取,这里将引入性能优化的几种方式,多线程,内存管理,队列等。

3.对于pipeline处理所爬页面的方式进行优化,将引入mongoDB来应对突然增大的爬取量,采用文件存储读取的方式而不是通过数据库存取临时表,这样稳定性将得到很大提高。

4.将正式打通与目录系统的数据通道,提供真实可靠的数据入口。

5.优化部署方式,由于是前后端分开部署,现在采用的部署方式存在一些隐性问题,另外breakpipe问题出现在了linux部署的系统日志,而windows安然无恙,问题的根源还需要深入研究。

6.将UI进一步优化,提升用户体验,增加一些等待时间的动画效果等。

  如果A2D进入到下一阶段的迭代周期,作者也难以估量高复杂度的产品是不是会暴露出无法解决和无法规避的问题,但是风险和机遇总是并存的,这也是我们作为技术人员,一直孜孜不倦得理由。最后作者再此感谢A2D产品所有的参与人员,强烈感谢我的项目组成员,感谢技术敏感,头脑聪慧,灵性十足的团队核心技术工程师,作者的副手佳儿,身在远端,交流不便但是效率超群的老前端老周,工作韧性十足,潜力巨大的后台程序员耀暖。再次感谢所有读本文的读者,谢谢你们的耐心和肯定,希望得到你们的回馈和建议,期待作者还能在之后的生涯中继续撰写我的项目航海经历!

作者 毛鑫敏

联系方式 15168082750

qq 331228638

邮箱 [email protected]