65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 HtmlAgility pack 和 CssSelectors

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.78/5 (9投票s)

2015 年 10 月 12 日

CPOL

6分钟阅读

viewsIcon

31997

downloadIcon

322

解析 HTML 文件过程中的观察。

引言

首先,我并不声称是 XPath 或正则表达式方面的专家,但以下是我在为客户项目解析 HTML 文档时的一些观察。

在以下示例中,我使用 HtmlAgility Pack (HAP) 将 HTML 加载到文档对象模型 (DOM) 中并解析为节点。此外,有时我需要解析文档中并非真正节点的元素,例如注释。

除了对 HAP 的一般性观察外,我还将介绍 HAP.CSSSelectors 包提供的扩展方法,这些方法可以大大简化选择操作。

背景

我一直在成功地为一名客户使用 HTMLAgility Pack,解析 HTML 文档以提取相关信息。CssSelector 扩展将添加一个更强大的抽象级别,用于收集所需数据。

Using the Code

示例所需的程序包需要通过 NuGet 导入。程序包描述将加载到项目中,但您需要将 NuGet 程序包管理器设置为恢复库。

在项目中,我包含了一个非常简单的 HTML 文件,其中包含我在项目中需要解决的问题示例。

为了在不进行任何修改的情况下进行测试,您需要将 HTML 文件复制到以下驱动器和目录 – C:\testdata

HtmlAgility 提供了许多类,包括表示 DOM 各个部分的类和枚举,这些类包括 HtmlAttributeHtmlAttributeCollectionHtmlCommentNode 等等。

我们要检查的第一个类是 HtmlDocument 类。此类包含加载和解析文档到其各自部分的方法。

在附加的源代码中,我使用 (Part X) 的命名法来标识代码的每个部分,其中 X 是一个数字。

要使用,需要实现以下行

第一部分

HtmlAgilityPack.agpack = new HtmlAgilityPack.HtmlDocument();

要调用的下一个方法是加载文档的方法。您可以从 string 加载

agpack.LoadHtml(Html string) 
//or from a resource - 
agpack.Load(@"c:\testdata\testdat.htm");

与网页浏览器一样,HAP 对提供的 Html 具有容错性。您可以查询错误,但它不会中断。

包含的文件在第二个 font 标签上缺少关闭标签,并且 end 标签位置错误。在浏览器中运行正常,在 HAP 中也不会抛出错误,但可以进行检查。

第二部分

var errors = agpack.ParseErrors;

ParseErrors 将返回一个错误集合和错误计数。有趣的是,关闭的 font 标签并没有引发错误。但错位的 </tr> 却引发了错误。

文档加载后,用于搜索的两个主要方法是

SelectNodes(string XPath)  // from the DocumentNode
GetElementbyId(string Id)  // from the HtmlDocument

由于 ID 只能有一个,getElementById 将返回单个节点,而 SelectNodes 将返回节点集合,因为使用 XPath,您可能会匹配一个或多个项目。

我的客户有一个应用程序,它会将多个文件连接在一起,并用开始和结束注释分隔每个文档。以下是我如何处理将此文档分割回其组成部分的方法。我包含的文件有一个由注释分隔的部分,注释形式为

<!-- Start Table: 1234 --> HTML Body <!-- End Table -->

其中 1234 可能代表我们需要处理的某种类型的账户号码。

第三部分

您可以使用以下方法来获取注释

var comment = agpack.DocumentNode.SelectNodes("//comment()[contains(., 'Start Table:')]");

这表示从整个文档(“//”)中选择包含从当前位置(.)开始的单词 Start Table 的注释。

由于这是一个注释,它没有子节点,其内部文本就是注释本身的文本。如果您想解析注释以确定注释中的某个值(例如本例中的账户号码),这将很有用,但如果您想要注释之间的文本,则帮助不大。为了实现这一点,我求助于正则表达式和分组。

第四部分

var html = Regex.Match(agpack.DocumentNode.InnerHtml,@"
<!-- Start Table: \d* -->(?<one>.*)<!-- End Table -->",RegexOptions.Singleline).Groups[1];

现在,在 html.Value 中,我们得到了两个标签之间的文本。

接下来是查找 DOM 中的元素,第一个示例是使用 getElementById 查找节点。有三个表格,但只有一个有 ID。一个是 ID=”abc”,另一个是 ID=”table3”

让我们开始查看 id 为 id=”abc” 的表格

第五部分

var node = agpack.GetElementbyId("abc");

这将返回一个表示该表格的单个节点。InnerHtml 将包含 <table></table> 标签之间的所有文本。它还将包含表示表格 DOM 结构的节点集合。

第 6 部分

获取行节点的其中一种方法是使用 Linq 来发现它们,例如

var rownodes = node.ChildNodes.Where(w => w.OriginalName == "tr");

这有点效果,如果您检查计数,您会看到有三行。但实际上有四行,第一个被 <thead></thead> 包裹的行找不到。

另一种方法是在节点上使用 SelectNodes 来发现 tr 元素。

rownodes = node.SelectNodes("tr");

但这仍然找不到所有行,只找到其直接子节点。

那么 node.SelectNodes("/tr"); 呢?这什么也没返回。

那么 node.SelectNodes("//tr"); 呢?好消息是它找到了丢失的行以及文档中的所有行(12 行)。

经过一番研究,我发现以下两种解决方案有效

rownodes = node.SelectNodes(node.XPath + "//tr");

//or

// https://w3schools.org.cn/xsl/xpath_axes.asp
rownodes = node.SelectNodes("descendant::tr");

这返回了所有四行,这对我来说很有趣。我认为我曾假设 HAP 会从当前节点进行 SelectNodes,并且“//tr”会起作用,但“//”表示从文档根目录开始搜索。而第二个选项确实可以作为当前选定节点的后代。

类似地,我们可以使用相同的方法查找 tr 元素的所有 td 元素。请注意,对于 table 3,我们返回了十二个 td 元素,即使它们是 <tr><font><span> 元素的子元素。

第 7 部分

node = null;
node = agpack.GetElementbyId("table3")
nodes = node.SelectNodes("descendant::td");

让我们转向 HAP.CssSelectors

它建立在 HtmlAgility Pack 之上,并且实际上会确保它作为 NuGet 包的一部分被安装。

它允许您使用 CSS 选择器而不是 XPath 来选择元素。例如

第 8 部分

rownodes = agpack.QuerySelectorAll("#abc tr");

在这种情况下,我不需要从节点开始查找,只需从整个文档选择,它就返回了预期的 4 行。

listTDNodes = agpack.QuerySelectorAll("#table3 td");

这是一个获取第二行中仅有的 <td>(三个)的示例。

listTDNodes = agpack.QuerySelectorAll("#table3 tr:nth-child(2) td");

这返回了十二个项目,四行三列。有一点需要注意。QuerySelectorAll 方法返回的是 List<node> 而不是节点集合。如果您打算混合使用,了解这一点很重要。

除了按 ID(#)选择外,您还可以按类(.)选择,这比使用 XPath 查找带有类的属性要容易得多。

listTDNodes = agpack.QuerySelectorAll(".table");

返回类为 table 的第一个和第三个表格。

关注点

总之,CssSelectors 扩展是另一个有用的工具,可以轻松选择元素,而无需深入研究 XPath 或遍历集合。我知道我将期待将这些发现中的一些应用到我自己的工作中。

历史

  • 2015 年 10 月 12 日:初始版本
© . All rights reserved.