html2struct 类库
html2struct 将 HTML 代码解析为简单的面向对象的树状结构,并提供一个小工具集来从中提取数据
引言
html2struct
旨在作为从外部 HTML 源进行数据挖掘的辅助工具。
它可以通过标签结构和属性轻松地从 HTML 文件中提取数据,而不依赖于可能发生变化并导致提取失败的其他内容。
它将 HTML 代码解析成一个简单的对象树状结构,并提供一套小工具来从中提取数据。它是一个轻量级的解析器,不依赖于浏览器或 DOM 对象等资源密集型外部组件。它仅创建一个由 htmlTag
对象组成的简单树。
它 **不** 生成 HTML,不运行脚本,也不获取任何外部引用。
它不尝试强制执行 HTML 文档标准,也不关心是否符合这些标准,例如是否必须拥有 <HTML>
或 <BODY>
标签。这使得可以轻松地将任何 HTML 代码片段解析成一种结构,据我所知,这使得此解决方案与其他我所见过的 HTML/XML 解析器有所不同。
理论上,它也应该能够解析其他标记语言,例如 XHTML、XML、SGML 以及其他变体。目前,这在很大程度上是未经测试的领域,但我已经尝试过解析一些 RSS 源,并且它们能够很好地解析 XML。随着时间的推移,我希望使这个解析器能够处理所有类似 HTML 的标记语言。
背景
我一直在开发一个专门针对小型广告/分类信息的搜索引擎,它从不同来源收集信息,并允许人们进行搜索。我喜欢称之为一种本地化的小型 Google,今天,我每天索引多达 2,000 条来自 20 个不同来源的广告。这个项目需要从不同的 HTML 页面中提取大量数据,这些页面以不同的方式呈现,以提取人们可以搜索的统一数据材料。
我是正则表达式的忠实拥护者,并且一直使用它们来从这些 HTML 源中提取数据,但现在,在花费数月时间微调极其复杂的表达式后,我得出结论,对于不断变化的数据源,定义一个“正确”的表达式太难了。
我反复发现,在有人对其 HTML 代码进行微小更改后,我的搜索引擎会错误地挖掘数据,而这些更改可能极难调试。这些更改包括添加或删除 HTML 标签、添加/删除/交换元素中属性的顺序,甚至添加一个空格都可能导致问题。尽管我尽力预测这些变化,但我发现这是不可能的,并且表达式反复失效。
这个问题需要一种不同的方法。我希望能够解析 HTML 代码,而不考虑其大小写、元素/属性的顺序、空格或是否符合特定的 HTML 标准。
经过一番搜索,我决定自己做一个解析器,因为我发现所有现有的解决方案似乎都包含一个功能齐全的浏览器或 DOM 对象生成器来进行解析,并且往往会在整个 HTML 代码不符合特定标准或其中有错误时拒绝它。
最后,我决定在开源社区分享这个项目。这是我第一次以正式的方式这样做,我一直想这样做很久了。我希望你会发现这个类很有用,并且当然希望我没有在重复造轮子。
定义
这个库包含 2 个类,主类名为 htmlStruct
,另一个是表示结构中标签的 htmlTag
。正如类视图所示,结构非常简单。
关于属性的一点说明
htmlStruct
类只有 2 个属性
AllTags
- 包含 HTML 文档中的所有已解析元素InnerTags
- 代表树状结构,通常包含顶级元素,如<HTML>
和<HEADER>
。该列表用于向下遍历 HTML 树。
htmlTag
是用于提取数据的类,它具有一些用于导航和数据提取的属性。
Tag
- 当然,存储当前标签的名称。Attributes
- 提供对当前标签定义的属性(如 'src
' 和 'href
')的Dictionary
类型访问。Html
- 存储用于创建该标签的 HTML 源。对于<TEXT>
,它存储文本。LineNr
- 存储标签在 HTML 源中的行号,用于调试。InnerTags
- 存储在当前标签的打开/关闭标签内找到的标签,这些标签本身也可以有内部标签等。Next
、Previous
和Parent
- 用于从通过搜索函数隔离的当前标签进行导航。
关于函数的一点说明
通常来说,htmlStruct
类中的函数操作 AllTags
并搜索整个文档,而 htmlTag
类中的函数则递归地操作 InnerTags
,并且不搜索当前标签范围之外的内容。
Parse()
- 将 HTML 文档作为string
输入,填充属性并生成树结构。Search()
- 返回与基于标签名、属性或值的给定搜索表达式都匹配的标签列表。SearchHtml()
- 返回其Html
属性与正则表达式匹配的标签列表。FirstTag()
- 返回与基于标签名、属性或值的搜索条件都匹配的第一个标签。FirstHtml()
- 返回其Html
属性与正则表达式匹配的第一个标签。NextTag()
- 返回与给定搜索表达式都匹配的下一个后续标签,无论它是否是内部标签。NextHtml()
- 返回其Html
属性与正则表达式匹配的下一个后续标签,无论它是否是内部标签。PreviousTag()
- 返回与给定搜索表达式都匹配的上一个先行标签,无论它是否是内部标签。PreviousHtml()
- 返回其 HTML 表达式与正则表达式匹配的上一个先行标签,无论它是否是内部标签。ToText()
- 从当前标签及其内部标签中提取文本。如果遇到<BR>
或<P>
标签,它们将被视为新行。
关于搜索条件的一点说明
所有 Search()
、FirstTag()
和 PreviousTag()
函数都接受相同的搜索参数:标签名称、属性和值。它们还接受一个不区分大小写的 正则表达式 作为搜索 string
。然后,它们将进行搜索,返回所有给定表达式为 true
的标签。如果提供了标签名称,则返回名称匹配的标签。如果提供了属性,则返回属性名称匹配的标签。如果提供了值,则返回任何属性值匹配的标签。如果同时提供了属性和值,则返回属性名称匹配且值也匹配的标签(嗯,有点意思...)。
关于 <TEXT>/<COMMENT>/<SCRIPT> 的一点说明
为了简单起见,我决定将文本和注释也表示为标签,但它们实际上出现在 HTML 的标签之间。这允许您使用 search
函数轻松搜索 <TEXT>
或 <COMMENTS>
标签。此外,当解析器遇到脚本时,它只会创建 <SCRIPT>
标签并将代码放在 HTML 属性中。
请注意,我不会费心创建关闭标签的对象,因为它们对于表示结构本身不是必需的。
使用该类
操作主类
使用此解决方案时,您会发现从 HTML 中提取数据变得异常简单。
using System.Collections.Generic;
using html2struct;
//
// Operating the main class.
//
htmlStruct tree = new htmlStruct(strHTML);
// And if you intend to re-use the wrapper just do:
tree.Parse(strHTML)
如何使用这些类的快速示例
我喜欢定义一个临时标签 (t
),然后用它来提取数据。这可以防止“对象引用未设置为对象的实例。
”错误,允许进行顺序搜索,也有助于调试。
// Attempt to find a <H3> Tag and get the text contained in it (Will produce error if no H3 found)
string sTitle = tree.FirstTag("<H3>", "", "").ToText();
// How to get all text and comment elements in a document
List<htmlTag> list = tree.Search("<TEXT>|<COMMENT>", "", "");
// How to get all references in a document
List<htmlTag> list = tree.Search("", "href|src", "");
// Isolating src of an image is of course a breeze using t as temporary tag
string sImage = (t = tree.FirstTag("<IMG>", "src",
"")) != null ? t.Attributes["src"] : "";
// How to isolate an email address
string sEmail = (t = tree.FirstTag("<A>", "href",
"mailto:")) != null ? t.Attributes["href"] : "";
// How to get text following a single text element
string sPrice = (t = tree.FirstHtml("^Price$")) != null ? t.Next.ToText() : "";
// Locate a <DIV> Tag within BODY element using HTML source
htmlTag tag = (t = tree.FirstTag("<BODY>", "", "")) != null &&
(t = t.FirstHtml("<div class=\"details\">")) != null ? t : null;
// How to extract a list of divs with varying classes, e.g., search results with light/dark entries
List<htmlTag> list = (t = tree.FirstTag("<DIV>", "class", "some-listing")) != null ?
t.Search("<DIV>",
"class", "entry( grey)?") : null;
此外,大多数页面都有一个 <DIV>
块,其中包含我感兴趣的所有数据。在这种情况下,我首先隔离该标签,然后在其中进行搜索。
htmlTag ad = tree.FirstTag("div", "class", "Details");
if (ad != null)
{
htmlTag t;
string sTitle = (t = ad.FirstTag("<H3>", "", "")) != null ? t.ToText() : "";
}
结论
正则表达式虽然强大,但并非数据挖掘的理想选择。它们往往会变得庞大且极其复杂,变化很快。代码的微小变动,例如添加一个空格,都可能轻易导致其停止匹配,并且极难调试。
我放弃了依赖正则表达式来构建我的搜索引擎,并决定创建这个库,以将正则表达式的强大模式匹配能力与功能相结合,然后可以将其定向到文本中。
在使用 html2struct
进行一些尝试后,我发现它对变化的 HTML 代码相当容忍。我发现可以轻松地将现有代码应用于新的 HTML 源,只需对搜索参数进行微小更改,而不是重写大量的正则表达式。
html2struct
不关心标签或属性顺序的变化,也不关心 HTML 元素的添加或删除,只要您不直接依赖它们即可。它甚至不关心结构上的变化,即使它们移动了页面的整个部分,只要它们不更改您明确搜索的标签/属性即可。
html2struct
在数据挖掘方面的处理比单独使用正则表达式要好得多。事实上,它们根本无法与这种方法相比,我有点后悔没有早点做。
已知问题
在处理 HTML 代码时,最好记住,我们实际上是在处理未经检查的纯用户输入。无法确定人们可能有意或无意地将什么样的数据插入到代码中。我已经尽我所能调试了这个解决方案,使其能够正常使用,但现在我决定分享它,无疑会出现无数问题。
- 嵌套注释和脚本无法正确处理。例如,“
<!-- rem <!-- more rem --> -->
”将导致解析器跳过注释中的最后一个“-->
”,并将其之后插入为<TEXT>
标签。在这里,我遇到了正则表达式处理嵌套/递归模式的问题。 - 未命名的标签,如“
<<em>desperately</em> important>
”会导致解析器忽略开始标签,正常继续,但最后以一个<TEXT>
标签结束,其 HTML 为“important>
”。 - 当前
Next
和Previous
指向在解析过程中发现的标签顺序。因此,父元素的Next
指向其第一个InnerTag
,而不是指向同一级别上紧随其后的下一个标签。Previous
也指向最后一个标签,无论它是位于同一级别上当前标签之前的父标签的子元素。例如,如果我正在查找某个标签t
之后的文本,但t
有 2 个子标签,我将不得不将该文本引用为t.Next.Next.Next
而不是t.Next
。我想我们可以称之为深度优先导航而不是广度优先导航。我还没有完全决定是否要更改这一点,所以我会等待一些舆论压力。
历史
- 2018 年 4 月 2 日至 4 月 7 日:已完成文章并修复了小问题。对编辑们所有的小修复表示歉意。
- 2018 年 4 月 4 日:在测试各种来源时,遇到了一个未被识别的
<![DATA[...]]>
元素。已修复此问题并以版本 5 重新发布了库。 - 2018 年 10 月 30 日:修复了一些错误,审查了文章,并以版本 6 发布。
- 修复了属性没有引号时未被正确处理的错误。
- 更改了处理开始/结束标签的方式,不再假定开始标签是后续标签的父级(这可能导致单个标签被视为子标签),而是现在使用结束标签来假定之前的标签是子标签。
- 在删除 HTML 文本中无法识别的内容时发现了一个小错误。如果源中没有
<
,它不会删除文本并陷入无限循环。 - 在搜索函数中添加了
SearchHtml()
、NextTag()
、NextHtml()
、PreviousTag()
和PreviousHtml()
。
注意:为了避免歧义,不得不将属性Next
从NextTag
重命名,将Previous
从PreviousTag
重命名。 - 在围绕嵌套元素生成
datastructure
的方式中发现了错误,导致树结构畸形。向htmlTag
添加了Status
属性,以便在解析时跟踪哪些之前的标签已被关闭。
- 2018 年 10 月 29 日:发现了一些拼写错误,并添加了一整句话。