【问题标题】:How does a parser (for example, HTML) work?解析器(例如 HTML)如何工作?
【发布时间】:2011-03-10 04:21:42
【问题描述】:

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

我读过它首先标记一切,然后解析它。

分词是什么意思?

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

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

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

如果我通读 HTML Purifier 之类的源代码,是否可以很好地了解 HTML 是如何解析的?

【问题讨论】:

  • 查看en.wikipedia.org/wiki/Lexical_parser 以获得非常简短的介绍;还可以查看Parsing 那里的文章。而 HTML Purifier 在某些时候正是这样做的。
  • HTML Agility Pack 是开源的,基于tokanizer。 htmlagilitypack.codeplex.com
  • 如果你能读懂 C (ocaml, lisp),试试看一些关于 yacc/lex (ocamlyacc/ocamllex, cl-yacc/cl-lex...) 的教程。您将从代码中快速了解基础知识。如果你能读懂代码。
  • 嗯,正则表达式可能是解释标记化的最简单方法,但在 HTML 的情况下,这还不够——我想说这可能是第一步,需要额外的处理。
  • 关于这个主题的文章非常好:html5rocks.com/en/tutorials/internals/howbrowserswork

标签: html browser parsing html-parsing tokenize


【解决方案1】:

标记化可以由几个步骤组成,例如,如果你有这个 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 用于回答(意外地)我关于哪些文本片段应构成基于 HTML / XML / SGML 的语言中的标记的长期问题! (我在其他线程中问过这个问题。)谢谢,伙计!很好的例子,确实! :-)
  • 我只会更正,根据 W3C 标准,不应删除空格,而是将空格传递给实现来决定(在大多数情况下最终会删除,PRE 或类似元素除外,保留空白)。
  • @SasQ:谢谢,尽管我不建议那些想了解如何实际解析 HTML 的人使用我的答案,因为我在不了解实际 HTML 解析器或阅读 HTML 规范的情况下编写了这个答案。我的回答只是为了说明标记化过程,如果它意外匹配实际 HTML 解析器的工作方式,那将是一个非常巧合。
  • 这是一个非常有用的答案。谢谢!
【解决方案2】:

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

回答您的直接问题:标记化大致相当于学习英语,然后将其分解为单词。在英语中,大多数单词是连续的字母流,可能包括撇号、连字符等。大多数单词都被空格包围,但句号、问号、感叹号等也可以表示单词的结尾。同样,对于 HTML(或其他任何内容),您可以指定一些关于什么可以构成该语言中的标记(单词)的规则。将输入分解为标记的这段代码通常称为词法分析器。

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

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

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

【讨论】:

    【解决方案3】:

    不要错过 W3C 在parsing HTML5 上的说明。

    有关扫描/词法分析的有趣介绍,请在网上搜索 Efficient Generation of Table-Driven Scanners。它显示了扫描最终是如何由自动机理论驱动的。正则表达式的集合被转换为单个 NFA 。然后将 NFA 转换为 DFA 以使状态转换具有确定性。然后,该论文描述了一种将 DFA 转换为转换表的方法。

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

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

    几个解析资源:

    【讨论】:

    • +1 感谢 W3C 链接。它看起来像是一篇内容丰富(而且很长)的读物!
    • 而且,如果您的语法在未来不会改变,您可以将转换表直接“烘焙”到源代码中并一次性编译。这是可能的,因为您运行程序的机器实际上也是一个状态自动机!所以你可以“在硬件中实现你的自动机”。方法如下:状态可以由代码中的位置(CPU 中的指令指针)表示。状态转换只是(非)条件跳转(分支)。您还可以使用程序的堆栈来存储/恢复状态(过程调用和返回)。这将大大加快速度。
    【解决方案4】:

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

    但回到你关于词法分析器的问题:
    每种语法都由两种符号组成:非终结符号(展开为其他语法规则的符号)和终结符号(“原子”符号——它们是叶子)语法树,不要展开到其他任何东西)。终端符号通常只是标记。词法分析器中一个一个地抽取令牌,并匹配到它们对应的终端符号。

    那些终端符号(标记)通常具有常规语法,这更容易识别(这就是为什么它被分解到词法分析器中,它更专门用于常规语法并且可以比使用更通用的非- 正则语法)。

    因此,要为类似 HTML/XML/SGML 的语言编写词法分析器,您需要找到足够原子且规则的语法部分,以便词法分析器轻松处理。这里出现了问题,因为起初并不明显这些是哪些部分。这个问题我纠结了很久。

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

    • TagOpener:&lt; lexeme,用于开始标签。
    • TagCloser:&gt; lexeme,用于结束标签。
    • ClosingTagMarker:/ 用于结束标签的词位。
    • 名称:以字母开头的字母数字序列,用于标记名称和属性名称。
    • 值:可以包含各种不同字符、空格等的文本。用于属性值。
    • 等于:= lexeme,用于将属性名称与其值分开。
    • 引用:' lexeme,用于封闭属性值。
    • 双引号:" lexeme,用于封闭属性值。
    • PlainText:任何不直接包含&lt; 字符且不属于上述类型的文本。

    您还可以为实体引用设置一些标记,例如 &amp;nbsp;&amp;amp;。大概:

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

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

    事后思考: 不幸的是,其中一些标记可能只出现在其他标记中。所以需要使用词法上下文,毕竟这是另一个控制状态机识别特定标记的状态机。这就是为什么我说类似 SGML 的语言不能很好地融入词法分析的模式。

    【讨论】:

      【解决方案5】:

      这就是 HTML 5 解析器的工作原理:

      【讨论】:

        猜你喜欢
        • 2011-06-29
        • 2011-06-12
        • 2011-08-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-17
        • 1970-01-01
        相关资源
        最近更新 更多