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

基础HTML解析器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.06/5 (16投票s)

2002年5月17日

BSD

6分钟阅读

viewsIcon

224476

downloadIcon

6703

一个解析HTML的类——屏幕保护程序比赛的一部分。

Sample Image - HTMLParser.gif

引言

大约在五月的第二周,Christian 想出了一个很棒的功能,用于屏幕保护程序。它可以解析某些人名字中包含的 HTML。由于他正忙于添加其他功能,我承担了这个任务。

我的第一个想法是使用一些带有命名引用的正则表达式来提取标签,并反复使用该组,直到组中只剩下文本。

事实证明,对于 .NET Framework 已经完成的事情来说,这是很多工作。首先,对文章末尾将要解析的 HTML 类型做一些背景介绍。

HTML 片段的结构

一个片段可以包含零个或多个标签和零个或多个字符串。一个片段可以只包含一个空字符串,这是完全有效的。

单个标签有三个部分:开始、值和结束。标签的开始部分包括标签名,以及零个或多个带引号的属性。标签的值部分可以包含更多标签、文本或为空。标签的结束部分仅包含前面带有斜杠(/名称)的名称。

我们不允许一个标签嵌套在另一个标签内部开始,但在其外部结束。所以 不是 这样的有效 <b><i>f</b>oo</i> 请注意,'b' 标签在 'i' 标签之前结束。

下面是一个有效的 HTML 片段的例子。

<font color='blue'>M<b>y</b> <i>name</i></font> <b>i</b>s....

上面片段的细分如下

Tree output of the HTML

用引号括起来的树节点是输出到文本中的实际值。在此示例中,唯一带有属性的标签是 Font 标签,您会注意到该属性名为“color”,值为“blue”。

解析 HTML 片段

要生成最终格式化的文档,您需要从顶部开始,向下遍历每个节点,随着节点的出现构建样式。当遇到纯文本时,您将获取当前格式并将其与文本一起打包,以便稍后使用。当您遇到一个节点时,您会缓存当前格式,以便在进入下一个子节点时,不会带有第一个子节点的格式。

那么,如何才能做到这一点呢?我上面描述的片段结构实际上是一个 XML 片段,因此我们可以使用 .NET 的 XML 解析器来完成繁重的工作,而我们只需遍历生成的“树”。

在让 .NET 为我们完成工作之前,有一个小问题需要解决。有效的 XML 文档只有一个根标签,而我们的 HTML 片段不需要将所有内容包含在一个标签中。补救方法是将片段放在一个标签内,例如 <html> & </html>。

格式结构

在我们的简单解析器中,只支持几种不同的格式。文本颜色、粗体、斜体、下划线、上标和下标是此解析器支持的唯一格式;添加额外的格式只需查找更多标签和属性即可。

由于格式是逐步构建的,因此我创建了一个名为 TextStyle 的类,它负责跟踪当前格式。它有两个构造函数:一个创建默认的 TextStyle(无格式),另一个复制一个 TextStyle

解析 HTML (XML) 文档

在此代码中,它使用递归来解析遇到的每个节点。框架会为文本创建 XML 节点,名为 #text

我将引用源代码,而不是在此处复制代码,但我将描述代码的关键部分。

确定字体颜色

    XmlNode node; // The node we are working on
    switch(node.Name.ToLower())
    {
    case "font":
        if( node.Attributes != null )
        {
            foreach( XmlAttribute attribute 
                in node.Attributes)
            {
                switch(attribute.Name.ToLower())
                {
                    case "color":
                        // ParseColor is actually 
                        // inline in the code but
                        // I broke it out so it
                        // won't scroll the page
                        ParseColor(attribute, style);
                        break;
                }
            }
        }
        break;
    }
 
private void ParseColor(XmlAttribute a, TextStyle style)
{
    if( attribute.Value[0] != '#' )
        style.ForeColor = Color.FromName(attribute.Value);
    else
    {
        try
        {
            int r, g, b;
            r = Int32.Parse(attribute.Value.Substring(1,2), 
                System.Globalization.NumberStyles.HexNumber);
            g = Int32.Parse(attribute.Value.Substring(3,2), 
                System.Globalization.NumberStyles.HexNumber);
            b = Int32.Parse(attribute.Value.Substring(5,2), 
                System.Globalization.NumberStyles.HexNumber);
            style.ForeColor = Color.FromArgb(r, g, b);
        }
        catch
        { }
    }
}

在 ParseColor 中,您会注意到我没有使用 Convert 类将颜色值转换为十六进制 RGB 值到 Int32。这是因为我需要在 Parse() 调用中指定 HexNumber NumberStyle,以便它转换十六进制数字而不是抛出异常。我将代码包装在 try/catch 块中,以防颜色值无效。

这是我在这段解析代码中看到的唯一有趣的部分(也是唯一超过两行的代码)。

当您构建样式时,最终会遇到一个名为“#text”的节点。该节点包含当时所有标签内的文本。有了这些知识,我们就可以获取其中包含的文本和当前格式,并将其添加到我们已经创建的格式化文本列表中。

显示格式化文本

显示文本相对容易,前提是您的集合的枚举器以添加的顺序检索项。使用 foreach 循环方法,您可以遍历每个 TextStyle 并按需格式化文本。

常见问题

未加引号的属性

根据编写 HTML 的人,属性可能加引号也可能不加引号。这是一个问题,因为 XML 要求它们必须加引号。这会导致需要预先解析 HTML 以确保所有属性都加引号。

事实证明,正则表达式非常适合这项工作。我必须使用一个两步过程来为属性加引号,因为我找不到一种方法来使用字符串中的命名引用来替换选定的属性名称/值对。

查找属性名称/值对的正则表达式 (RE) 如下

\<(?<tagName>[a-zA-Z]*) (?<attributeName>[a-zA-Z]*)( )*=( )*(?<attributeValue>[#a-zA-Z0-9]*)?>

上面的 RE (以及执行实际替换的代码) 有一个 bug;它只对只有一个属性的标签有效。一旦我找到修复它的方法,我就会发布更新的代码并在此解释。由于解析器只解析一个带有属性的标签,因此这不太可能成为问题。由于这个 bug,我将等待修复它之后再发布关于为未加引号的属性加引号的第二部分。

格式错误的 XML

如果 HTML 格式错误,解析器将抛出异常。但不要害怕,我提供了一个方法来删除传递进来的字符串中的所有类似 XML 的标签。它使用一个非常简单的 RE 来查找标签,然后将其替换为空字符串。<.*?> 是查找标签的 RE。

结论

我对代码有一些改进的建议。Mike Dunn 建议在尝试将 HTML 解析为 XML 文档之前,先将其修复为有效的 XML。这似乎并不难做到;但时间不允许我实现它。也许下周我会做到。

演示程序解析 HTML,然后显示其构建的 TextStyle 集合。如果在解析过程中发生异常,它只会从文本中提取 HTML,并将其放入默认的 TextStyle 中。

© . All rights reserved.