这节我们来看看Spiderman的Parser的设计和实现。
对于爬虫而言,网页内容的多样性直接决定了解析方式的多样性和复杂性,所以在设计上必须要将不变和变进行仔细总结和分离,一方面要达到稳定的内在架构能适应多种不同的解析方式,另一方面还要具备良好的扩展性从而支持为单个网页的针对其不同特性进行解析的借口。这一节首先从总体设计上来描述spiderman的parser模块设计,然后再对具体的解析流程进行分析。
Parser的设计:
在模块设计上Parser的角色和Fetcher一样都是通过扩展点引入到系统中的,即通过实现ParsePoint借口提供一个Parser的中介模块,通过这个中介模块和具体的Parser类交互来实现页面的解析,在源码中提供的中介者Parser类是ParsePointImpl。并且提供了两个执行具体解析内容的类:DefaultModelParser,WebDriverModelParser,这两个类都实现了ModelParser接口,这点很重要,这是实现ParsePointImpl和具体parser类解耦的关键,你或许或问,那么用户如何提供自己实现的parser类,这个可以在:配置文件中找到,即:每个Model节点都有一个parser属性,这个属性其实就是提供用户解析类的class path。
Spiderman支持三种网页类型的解析:json,html和xml。内容提取上,支持三种主流的提取方式:xpath,regex和el,可以说,这三种方法结合几乎可以抽取任意形式(只要是json,html和xml的一种)的信息,这也是Spiderman强大之处的体现之一吧。(哦!对了 ,是没有支持css选择器,不过有这三种也足够了)。
下面来结合配置文件来介绍一下parser的主要思想。
<model>
<!--
| 目标网页的命名空间配置,一般用于xml页面
| prefix: 前缀
| uri: 关联的URI
<namespaces>
<namespace prefix="" uri="" />
</namespaces>
-->
<!--
| 属性的配置
| name:属性名称
| isArray:0|1 是否是多值
| isMergeArray:0|1 是否将多值合并,搭配isArray使用
| isParam:0|1 是否作为参数提供给别的field节点使用,如果是,则生命周期不会保持到最后
| isFinal:0|1 是否是不可变的参数,搭配isParam使用,如果是,第一次赋值之后不会再被改变
| isAlsoParseInNextPage:0|1 是否在分页的下一页里继续解析,用于目标网页有分页的情况
| isTrim:0|1 是否去掉前后空格
-->
<field name="title">
<parsers>
<!--
| xpath: XPath规则,如果目标页面是XML,则可以使用2.0语法,否则HTML的话暂时只能1.0
| attribute:当使用XPath解析后的内容不是文本而是一个Node节点对象的时候,可以给定一个属性名获取其属性值例如<img src="" />
| regex:当使用XPath(包括attribute)规则获取到的文本内容不满足需求时,可以继续设置regex正则表达式进行解析
| exp:当使用XPath获取的文本(如果获取的不是文本则会先执行exp而不是regex否则先执行regex)不满足需求时,可以继续这是exp表达式进行解析
| exp表达式有几个内置对象和方法:
| $output(Node): 这个是内置的output函数,作用是输出某个XML节点的结构内容。参数是一个XML节点对象,可以通过XPath获得
| $this: 当使用XPath获取到的是Node节点时,这个表示节点对象,否则表示Java的字符串对象,可以调用Java字符串API进行处理
| $Tags: 这个是内置的用于过滤标签的工具类
| $Tags.xml($output($this)).rm('p').ok()
| $Tags.xml($this).rm('p').empty().ok()
| $Attrs: 这个是内置的用于过滤属性的工具类
| $Attrs.xml($this).rm('style').ok()
| $Attrs.xml($this).tag('img').rm('src').ok()
|
| $Tags和$Attrs可以一起使用:
| $Tags.xml($this).rm('p').Attrs().rm('style').ok()
| $Attrs.xml($this).rm('style').Tags().rm('p').ok()
| skipErr:0|1 是否忽略错误消息
| skipRgxFail:0|1 是否忽略正则匹配失败,如果是,则会取失败前的值
-->
<parser xpath="//div[@class='QTitle']/h1/text()"/>
</parsers>
</field>
<field name="content">
<parsers>
<parser xpath="//div[@class='Content']//div[@class='detail']" exp="$output($this)" />
<!--attribute 黑名单-->
<parser exp="$Attrs.xml($this).rm('class').rm('style').rm('width').rm('height').rm('usemap').rm('align').rm('border').rm('title').rm('alt').ok()" />
<!--tag 黑名单,去掉内嵌内容-->
<parser exp="$Tags.xml($this).rm('map').rm('iframe').rm('object').empty().ok()" />
<!--tag 白名单,保留的标签,除此之外都要删除(不删除其他标签内嵌内容)-->
<parser exp="$Tags.xml($this).kp('br').kp('h1').kp('h2').kp('h3').kp('h4').kp('h5').kp('h6').kp('table').kp('th').kp('tr').kp('td').kp('img').kp('p').kp('a').kp('ul').kp('ol').kp('li').kp('td').kp('em').kp('i').kp('u').kp('er').kp('b').kp('strong').ok()" />
<!--其他-->
</parsers>
</f