3.1 从HTML文件中提取文本
从HTML文件中提取有效的文本,经常碰到如下两种类型:一种是针对特定的网页特征提取结构化信息,另一种是通用的网页去噪。下面首先介绍从Web网页中提取文本的基本过程,然后就网页提取所使用的工具和具体问题专门展开介绍。
在实现从Web网页中提取文本之前,需要先识别网页的编码,如果有必要,也要识别网页所使用的语言,整体流程如下。
(1)从Web服务器返回的content type 中提取编码,如果是gb2312类型的编码,那么要当成GBK处理。
(2)从网页的Meta信息中识别字符编码,如果和content type中的编码不一致,则以Meta信息中声明的编码为准。
(3)如果仍然无法确定网页所使用的字符集,那么需要根据返回流的二进制格式进行判断。同时,要确定网页所使用的语言,如UTF-8编码的语言可以是中文、英文、日文或韩文等。
如下所示的代码从Web服务器返回的头信息中提取网页编码:

有的页面在头信息中不包括编码格式内容,所以需要从Meta信息中提取,具体示例如下:

下面利用HTMLParser包提取网页中的Meta信息(HTMLParser将在3.1.1节详细介绍):

另外,还有一个和字符编码相关的问题,就是有时碰到gb2312编码的网页会有乱码问题,因为浏览器能正常显示包含GBK字符的gb2312编码网页。将org.htmlparser.lexer.InputStreamSource 中设置的编码方法进行修改,就可以把设置的字符集由 gb2312 改成GBK:


3.1.1 使用HTMLParser实现定向抓取
HTMLParser(http://htmlparser.sourceforge.net)是一个可以实现提取HTML文件的解析程序库,它的主要功能是对纯HTML文件和内嵌的HTML进行语法分析,可以使用它完成对非规范的HTML文件的解析。为了调用HTMLParser,需要在编译和运行程序之前将htmllexer.jar或htmlparser.jar添加到项目的类路径中。htmllexer.jar提供一个较低的权限,可以通过线性、平坦且连续的方式使用网页中的字符串、备注和其他标记节点。htmlparser.jar包括htmllexer.jar中的所有的类,能够提供一系列嵌套标签节点,如字符串、备注和其他标记节点。
HTMLParser将网页转换成一个个串联的Node。例如,如下所示的代码可以把一个URL中的标签都打印出来:

每次调用Lexer的nextNode()方法返回下一个Node,直到没有节点的时候返回空值。
Node有如下3类。
● RemarkNode:代表HTML中的注释。
● TagNode:标签节点,TagNode有很多继承的子类,是种类最多的节点类型,div、body等Tag的具体节点类都是TagNode的实现。
● TextNode:文本节点。
RemarkNode的具体示例如下:

如下所示的代码由3个Node组成:

● <font class="nrbt">是TagNode。
● “广告业务”是TextNode。
● </font>也是TagNode,但它的isEndTag()是真的。
各种不同的HTML标签则由Node的子类Tag表示,HTML标签是Tag,而字符正文则是StringNode。
如下示例读入并分析本地硬盘文件中的HTML页面:


HTMLParser的实现原理如下,在Lexer类中实现对HTML语法的基本解析,也就是把页面中的字符组装成一个个的Node。Lexer可以识别的Node类型定义在NodeFactory的子类中,PrototypicalNodeFactory是HTMLParser自带的工厂类,可以通过setNodeFactory()方法设置Node工厂类:

可以在 PrototypicalNodeFactory 注册新的 TagNode,从而使 HTMLParser 识别新的Node。例如,为了单独识别Strong标签,可以先新建一个Strong类:

然后在Node工厂类中注册:

使用Parser类可以提取出符合特定条件的Node的集合。Parser类的extractAllNodesThatAre(XXXTag.class)方法将HTML文件中存在的所有的标签XXXTag解析出来,并放到一个Node数组中,即NodeList。如下示例用NodeFilter提取一个表格中的内容:

利用Parser类提取Node中包含的文本的实现:

为了避免读取页面时出现乱码,可以直接由包含 HTML 页面内容的字符串构造Parser类:

利用HTMLParser提取网页正文的流程如下。
(1)从Web服务器返回的content type 中提取编码,如果是gb2312类型的编码,则当成GBK处理。
(2)从网页的Meta信息中识别字符编码,如果和content type中的编码不一致,则以Meta信息中声明的编码为准重新解析网页。
提取网页正文的完整过程如下:






3.1.2 结构化信息提取
例如,实现一个关于招聘职位方面的搜索引擎。从如图 3-1 所示的招聘网页界面中提取职位信息。

图3-1 招聘网页界面
首先定义一个职位类:

然后从下面这个职位发布的网页中提取公司名称。查看公司名称周围的特征:

可以利用如下所示的Filter进行处理:

提取公司名称的实现如下:


从职位详细页面URL地址提取职位信息的完整的过程如下:


在如上所示的代码中,如果输入一个URL地址,那么返回和该URL地址正文信息对应的职位对象。
3.1.3 网页的DOM结构
网页可以看作DOM(文档对象模型)树组织的结构。在Firefox中,使用查看器可以看到网页的DOM树。
运行Firefox,然后访问http://www.lietu.com。在菜单中,选择“Web开发者”→“查看器”命令。在查看器的左侧视图中,会看到DOM节点的树状图。
NekoHTML是一个简单的HTML扫描器和标签补偿器,使程序能解析HTML文件并形成标准的 DOM 树。NekoHTML是sourceforge 上的一个开源项目,地址为http://nekohtml.sourceforge.net。如下所示的程序可以自顶向下打印输出DOM树:



图3-2 Node的基本概念
Node的基本概念如图3-2所示。
例如,要得到Node的src中的值:

节点有 3 种类型:元素节点、注释节点和文本节点。可以通过 getNodeValue()方法打印文本节点中的文字信息:


xerces-2_9_1包括一个LSSerializer对象,可以保存DOM解析后的文档或节点,如下所示的代码将iNode节点的内容输出到控制台:

也可以使用 writeToString()方法将文档或文档中的节点所代表的一段网页写入字符串中:

还可以修改节点,如要移除节点属性,可以使用如下所示的方法:

如下所示的代码可以移除一个节点:

3.1.4 网页去噪
一个页面中经常包括导航栏或底部的公司介绍等信息,这样的信息在很多页面都会出现,可以看作噪声信息。
有多种方法可以解决这个问题,其中一种方法的思路是,利用多个网页的正文信息进行比对,把公共子串检测出来,较长的公共子串可以看作噪声信息。从两个字符串中提取最长的一个公共子串的方法如下:


最多提取5个长度大于20的公共子串的方法如下:


另外,如果公共子串出现在字符串的头部和底部就更可能是噪声信息。
提取出这样的公共子串后,把它从新提取出来的正文删除即可:

除了公共子串的方法,还可以利用网页的DOM树去掉FORM、Select、IFRAME、INPUT、STYLE中的节点。
3.1.5 正文提取
网页包括目录导航式页面(List Page)和详细页面(Detail Page)。详细页面需要抽取的正文信息包括标题和内容等。
详细页面的特征包括以下几点。
● 文字较多(非锚文本)。
● 一般都有明显的文本段落,文字较多,相应的标点符号也较多。
● URL 较长。在一般的 Web 网站链接导航树上,主题型网页主要分布在底层,多为叶节点。对于同一网站而言,主题型网页的URL相对较长。URL体现了网站内容管理的层次,对于大型网站而言,URL往往非常有规律。
● 链接较少。主题型网页的主体在于“文字”,与导航型网页相比,其链接数较少。
● 网页标题可能出现在Title标签或H1标签中,H2标签可能表示文章的段落标题。
详细页面中网页噪声的特征包括以下几点。
● 多以链接的形式出现,链接到其他的相关页面。
● 有很多锚文本,但标点符号较少,锚文本往往是对其他链接页面的说明。
● 有许多常见的噪声文本,如版权声明等,在视觉上多出现于网页的边缘。
为了提取网页正文,需要先进行网页去噪。网页去噪的方法是利用各种通用的特征来区分有效的正文和页眉、页脚、广告等其他信息,其中一个常用的特征是链接文字比。可以把HTML转换成DOM树,对每个节点计算链接文字比。如果该节点的链接文字比高于1/4左右,就把这个节点去掉。
关于正文提取,可以把所有有用的TD和DIV都打分,然后选取分值最大的 TD或DIV作为正文块。提取网页正文的流程如图3-3所示。

图3-3 提取网页正文的流程
下面先用递归调用的方法计算一个节点下的链接数:

计算一个节点下的有效正文长度,忽略锚点上的字:

计算一个文本中大概包括的正文长度的方法如下:


根据链接文字比删除无效节点的过程如下。
(1)计算节点下的链接数。
(2)计算节点下的文字数。
(3)计算节点的链接文字比,链接文字比=节点下的链接数/节点下的文字数。
(4)如果节点的链接文字比大于某个阈值,则删除这个节点。
实现代码如下:



图3-4 网页标题和锚点文本的对应关系
NekoHTML可以把HTML文件转换成XML文件。XML文件的某一部分可以用Xpath 来描述。可以用计算节点特征的方法找到覆盖主要正文的内容节点,找到这样一个节点后,可以把这个节点用Xpath表示出来,具体如下:

但是这种Xpath的绝对路径表示方式在DOM树的节点删除或修改后就会失效。
网页链接中的锚点文本是对目标网页主题内容的概括,所以往往用它作为一个网页的标题描述。图3-4所示显示的是来源于中国科技网的两个页面之间的对应关系。
取得网页标题之后,还可以利用标题信息计算网页中的内容和标题之间的距离:

