【转】 HTML解析:基于XPath的C#类库HtmlAgiliytyPack

最近处于毕业设计开始阶段,前期工作需要去国外的一些专业数据库网站比对一些所需TF家族信息,为了快捷方便,想到用程序去帮助实现。前期实现了一系列的尝试,使用C#的的网络编程类库,获取查询结果,但是为了分析其中的结果并进行比对,我最开始尝试了两天之久的正则表达式,最后发现在解析这样的HTML文档的时候,花费时间太多,因此开始改变策略,最后得知MSHTML和HAP这两个.NET解析html的类库,出于对HAP这个轻量级类库兴趣,决定使用它来解析获取信息。

一、HAP简介

HAP是codeplex公司的一个解析html的.NET开源项目,支持XPath查询。官网(http://htmlagilitypack.codeplex.com/)给出的介绍是:

What is exactly the Html Agility Pack (HAP)?
This is an agile HTML parser that builds a read/write DOM and supports plain XPATH or XSLT (you actually don't HAVE to understand XPATH nor XSLT to use it, don't worry...). It is a .NET code library that allows you to parse "out of the web" HTML files. The parser is very tolerant with "real world" malformed HTML. The object model is very similar to what proposes System.Xml, but for HTML documents (or streams).

目前最新版本为HAP1.4.6,推出的新特性是支持Linq语言。

二、XPath

为了更好的使用HAP,必须了解XPath语言。维基百科介绍如下:

XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。

XPath基于XML的树状结构,提供在数据结构树中找寻节点的能力。起初XPath的提出的初衷是将其作为一个通用的、介于XPointer与XSL间的语法模型。但是XPath很快的被开发者采用来当作小型查询语言。

XPath中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释、根节点。节点关系有:Parent、Children、Sibling、Ancestor、Descendent。

    路径表达式用来选取节点或节点集,通过路径(path)步(steps)进行选取。

节点表达式:

表达式 描述
nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。
路径表达式 结果
bookstore 选取 bookstore 元素的所有子节点。
/bookstore

选取根元素 bookstore。

注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!

bookstore/book 选取属于 bookstore 的子元素的所有 book 元素。
//book 选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
//@lang 选取名为 lang 的所有属性


谓语:用来查找某个特定的节点或者包含某个指定的值的节点。谓语被嵌在方括号中

路径表达式 结果
/bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()<3] 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang] 选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang='eng'] 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00] 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

通配符:

通配符 描述
* 匹配任何元素节点。
@* 匹配任何属性节点。
node() 匹配任何类型的节点。

选取多个路径时,使用 “ | ”符号。


步(Step):语法为  轴名称::节点测试[谓语]

 轴(axis)-------------------------------定义所选节点与当前节点之间的树关系

节点测试(node-test)----------------识别某个轴内部的节点

零个或多个谓语(predicate)--------更深入提炼所选的节点集

XPath轴:

轴名称 结果
ancestor 选取当前节点的所有先辈(父、祖父等)。
ancestor-or-self 选取当前节点的所有先辈(父、祖父等)以及当前节点本身。
attribute 选取当前节点的所有属性。
child 选取当前节点的所有子元素。
descendant 选取当前节点的所有后代元素(子、孙等)。
descendant-or-self 选取当前节点的所有后代元素(子、孙等)以及当前节点本身。
following 选取文档中当前节点的结束标签之后的所有节点。
namespace 选取当前节点的所有命名空间节点。
parent 选取当前节点的父节点。
preceding 选取文档中当前节点的开始标签之前的所有节点。
preceding-sibling 选取当前节点之前的所有同级节点。
self 选取当前节点。


XPath可以使用运算符:

运算符 描述 实例 返回值
| 计算两个节点集 //book | //cd 返回所有拥有 book 和 cd 元素的节点集
+ 加法 6 + 4 10
- 减法 6 - 4 2
* 乘法 6 * 4 24
div 除法 8 div 4 2
= 等于 price=9.80

如果 price 是 9.80,则返回 true。

如果 price 是 9.90,则返回 false。

!= 不等于 price!=9.80

如果 price 是 9.90,则返回 true。

如果 price 是 9.80,则返回 false。

< 小于 price<9.80

如果 price 是 9.00,则返回 true。

如果 price 是 9.90,则返回 false。

<= 小于或等于 price<=9.80

如果 price 是 9.00,则返回 true。

如果 price 是 9.90,则返回 false。

> 大于 price>9.80

如果 price 是 9.90,则返回 true。

如果 price 是 9.80,则返回 false。

>= 大于或等于 price>=9.80

如果 price 是 9.90,则返回 true。

如果 price 是 9.70,则返回 false。

or price=9.80 or price=9.70

如果 price 是 9.80,则返回 true。

如果 price 是 9.50,则返回 false。

and price>9.00 and price<9.90

如果 price 是 9.80,则返回 true。

如果 price 是 8.50,则返回 false。

mod 计算除法的余数 5 mod 2 1


XPath有一百多个函数,常用在谓语中选择的函数有 

   position() 表示当前节点的序号

   last() 表示取最后一个节点

   name() 表示当前节点名字

在.NET中可以使用XPath处理XML文档,此处的html解析就是运用XPath查询选择。

三、HAP使用

   1、下载HtmlAgilityPack最新压缩包,http://htmlagilitypack.codeplex.com/releases/view/90925,官方下载地址。

   2、加压后,在所需项目中添加引用HtmlAgilityPack.dll文件,其中还附带了xml文件用于文档说明。然后添加using HtmlAgilityPack说明。如下图所示:


3、实际使用中,都是以HtmlDocument类为主线。HAP提供了两种不同方式加载HtmlDocument类的实例:从html字符串构造(可以使用LoadHtml方法和Load方法),从html文档流构造实例(使用Load方法)。从而就可以实例化一个DOM实例,对于客户端脚本语言JavaScript熟悉的开发者来说这是非常熟悉的。这些操作也与.net framework中的XmlDocument类对于XML文档的操作非常类似,而且XML操作中也支持使用XPath表达式的查询使用,不同的是HtmlDocument强化了GetElementById方法,可以直接使用定位。示例代码如下:

 

            HtmlDocument hd = new HtmlDocument();
            hd.LoadHtml(fileStr);
            HtmlNode root = hd.DocumentNode;
            HtmlNodeCollection hNodes = root.SelectNodes("//tr[@class='odd']|//tr[@class='even']/td[@class='rowNum']");

4、后续的使用需要依照自己的实际需求来设计。对于html文档的解析主要就是使用XPath表达式进行选择定位,可以参考http://www.w3school.com.cn/xpath/xpath_syntax.asp

5、去除script和style等标签。当使用document.DocumentNode.InnerText获取文档内容时会包含script等标签,因此可以使用下面的方法去除:

   foreach(var script in doc.DocumentNode.Descendants("script").ToArray())
       script.Remove();
   foreach(var style in doc.DocumentNode.Descendants("style").ToArray())
       style.Remove();
   foreach(var comment in doc.DocumentNode.SelectNodes("//comment()").toArray())
       comment.Remove();

6、在下载的HtmlAgilityPack压缩包中还找到了关于WindowsPhone开发需要的dll文件,因此对于开发WP手机应用也是非常的有效。


HtmlAgilityPack对于html页面的解析、信息采集的准确度和效率都优于使用正则表达式,而且对开发者非常友好,特别是正则表达式的学习曲线较陡,而且其中包含了很多的技巧,特别是对高效的正则表达式的书写是非常需要经验的。HtmlAgilityPack获取html文档时对原文本的结构要求是非常宽松的,这与XmlDocument的严格要求是不同的。基本上熟悉了XPath的语法之后,大部分应用都可以满足要求了。


参考资料:

1、HAP:简单好用的html解析器:http://msdn.microsoft.com/zh-tw/ee787055.aspx

2、XPath维基百科:http://zh.wikipedia.org/zh-cn/XPath

3、W3C school:http://www.w3school.com.cn/xpath/xpath_syntax.asp

4、C#:HtmlAgilityPack extract inner text:http://stackoverflow.com/questions/2785092/c-htmlagilitypack-extract-inner-text