解析器(例如,HTML)如何工作?

时间:2010-06-30 14:36:54

标签: html browser parsing html-parsing tokenize

为了论证,我们假设一个HTML解析器。

我已经读过它首先标记化所有内容,然后解析它。

tokenize是什么意思?

解析器是否每个都读取每个字符,构建一个多维数组来存储结构?

例如,它是否读取<然后开始捕获元素,然后一旦它遇到关闭>(在属性之外),它就被推送到某个数组堆栈?

我为了知道而感兴趣(我很好奇)。

如果我要阅读HTML Purifier这样的内容,那会不会让我对HTML的解析方式有所了解?

5 个答案:

答案 0 :(得分:56)

标记可以由几个步骤组成,例如,如果你有这个HTML代码:

<html>
    <head>
        <title>My HTML Page</title>
    </head>
    <body>
        <p style="special">
            This paragraph has special style
        </p>
        <p>
            This paragraph is not special
        </p>
    </body>
</html>

标记生成器可以将该字符串转换为重要标记的平面列表,丢弃空格(谢谢,SasQ进行更正):

["<", "html", ">", 
     "<", "head", ">", 
         "<", "title", ">", "My HTML Page", "</", "title", ">",
     "</", "head", ">",
     "<", "body", ">",
         "<", "p", "style", "=", "\"", "special", "\"", ">",
            "This paragraph has special style",
        "</", "p", ">",
        "<", "p", ">",
            "This paragraph is not special",
        "</", "p", ">",
    "</", "body", ">",
"</", "html", ">"
]

可能有多个令牌化过程将令牌列表转换为更高级别令牌的列表,如下面的假设HTML解析器可能做的那样(仍然是一个平面列表):

[("<html>", {}), 
     ("<head>", {}), 
         ("<title>", {}), "My HTML Page", "</title>",
     "</head>",
     ("<body>", {}),
        ("<p>", {"style": "special"}),
            "This paragraph has special style",
        "</p>",
        ("<p>", {}),
            "This paragraph is not special",
        "</p>",
    "</body>",
"</html>"
]

然后解析器转换该令牌列表以形成一个树或图形,以便于程序访问/操作的方式表示源文本:

("<html>", {}, [
    ("<head>", {}, [
        ("<title>", {}, ["My HTML Page"]),
    ]), 
    ("<body>", {}, [
        ("<p>", {"style": "special"}, ["This paragraph has special style"]),
        ("<p>", {}, ["This paragraph is not special"]),
    ]),
])

此时,解析完成;然后由用户来解释树,修改它等等。

答案 1 :(得分:30)

首先,您应该意识到解析HTML特别难看 - HTML在标准化之前使用广泛(和不同)。这会导致各种丑陋,例如标准指定不允许某些构造,但随后为这些构造指定所需的行为。

直接提出问题:标记化大致相当于采用英语,并将其分解为单词。在英语中,大多数单词是连续的字母流,可能包括撇号,连字符等。大多数单词被空格包围,但句点,问号,感叹号等也可以表示单词的结尾。同样,对于HTML(或其他),您可以指定一些关于可以构成此语言中的标记(单词)的规则。将输入分解为标记的代码段通常称为词法分析器。

至少在正常情况下,在开始解析之前,将所有输入分解为令牌。相反,解析器调用词法分析器以在需要时获取下一个标记。当它被调用时,词法分析器会查看足够多的输入以找到一个令牌,将其传递给解析器,并且在下一次解析器需要更多输入之前,不会对所有输入进行标记化。

一般来说,你对解析器的工作原理是正确的,但是(至少在一个典型的解析器中)它在解析语句的过程中使用了一个堆栈,但它构建的表示语句通常是一个树(和抽象语法树,又名AST),不是多维数组。

基于解析HTML的复杂性,我会保留查找解析器,直到您先读完其他几个。如果你做一些环顾四周,你应该能够找到相当数量的解析器/词法分析器,比如数学表达式可能更适合作为介绍(更小,更简单,更容易理解等)。

答案 2 :(得分:9)

不要错过W3C关于parsing HTML5的笔记。

有关扫描/ lexing的有趣介绍,请在网页上搜索高效生成表驱动扫描仪。它显示了扫描最终是如何由自动机理论驱动的。正则表达式的集合被转换为单个NFA。然后将NFA转换为DFA以使状态转换具有确定性。然后,本文描述了将DFA转换为转换表的方法。

关键点:扫描仪使用正则表达式理论,但可能不使用现有的正则表达式库。为了获得更好的性能,状态转换被编码为巨型语句或转换表。

扫描仪保证使用正确的单词(令牌)。解析器保证单词以正确的组合和顺序使用。扫描仪使用正则表达式和自动机理论。解析器使用语法理论,尤其是context-free grammars

一对解析资源:

答案 3 :(得分:7)

HTML和XML语法(以及其他基于SGML的语法)很难解析,并且它们不适合lexing场景,因为它们不是常规。在解析理论中,常规语法是没有任何递归的那个,即自相似,嵌套模式或类似于括号的包装器,它们必须相互匹配。但是基于HTML / XML / SGML的语言确实具有嵌套模式:标签可以嵌套。嵌套模式的语法在乔姆斯基的分类中更高级别:它是无上下文的,甚至是依赖于上下文的。

但回到你关于lexer的问题:
每种语法都包含两种符号:非终端符号(那些在其他语法规则中展开的符号)和终端符号(那些是“原子”的 - 它们是叶子语法树的一部分,不要放松到其他任何东西)。终端符号通常只是令牌。令牌从词法分析器逐个泵入,并与相应的终端符号匹配。

那些终端符号(标记)通常具有常规语法,这更容易识别(这就是为什么它被排除在词法分析器之外,对于常规语法更加专业,并且可以比使用非常规方法更快地完成它 - 正规语法)。

因此,要为类似HTML / XML / SGML的语言编写词法分析器,您需要找到足够原子且规则的语法部分,以便词法分析器轻松处理。这里出现了问题,因为最初并不清楚这些部分是哪些部分。我很长一段时间都在努力解决这个问题。

但上面的 Lie Ryan 在识别这些部分方面做得非常好。为他而战!令牌类型如下:

  • TagOpener:< lexeme,用于开始标记。
  • TagCloser:> lexeme,用于结束标记。
  • ClosingTagMarker:/ lexeme用于结束标记。
  • 名称:以字母开头的字母数字序列,用于标记名称和属性名称。
  • 值:可包含各种不同字符,空格等的文本。用于属性值。
  • 等于:= lexeme,用于将属性名称与其值分开。
  • 引用:' lexeme,用于封闭属性值。
  • DoubleQuote:" lexeme,用于包含属性值。
  • PlainText:任何直接不包含<字符但未被上述类型覆盖的文字。

您还可以为实体引用添加一些令牌,例如&nbsp;&amp;。大概是:

  • EntityReference:由&后跟一些字母数字字符并以;结尾的词汇。

为什么我为'"使用了单独的令牌而没有为属性值使用一个令牌?因为常规语法无法识别哪些字符应该结束序列 - 它取决于启动它的字符(结束字符必须与起始字符匹配)。这种“括号”被认为是非常规语法。所以我把它提升到更高的层次 - 对Parser来说。将这些标记(开始和结束)匹配在一起(或者根本不匹配,对于不包含空格的简单属性值)是他的工作。

<强>有感: 不幸的是,其中一些令牌可能只出现在其他标记内。因此需要使用词汇上下文,这毕竟是控制状态机识别特定令牌的另一个状态机。这就是为什么我说类似SGML的语言不适合词法分析的模式。

答案 4 :(得分:2)

这就是HTML 5 Parser的工作原理:

This is how HTML 5 Parser works