1. 苏葳的备忘录首页
  2. 因特网

浏览器的HTML解析器(一)

浏览器 HTML 解析浏览器组件中的HTML解析器的作用是把HTML标签解析成一棵分析树。HTML是一种语言,但这种语言跟常见的编程语言又有所不同。这种语言专注于因特网内容的展现。HTML语言的词汇和语法在W3C组织创建的规范(http://taligarsiel.com/Projects/howbrowserswork1.htm#w3c)中定义。当前版本是HTML4,正在制订HTML5的标准,届时HTML将拥有更强大的功能。

非上下文无关语法

我们在分析介绍中已经看到,语言的语法可以使用BNF之类进行规范的定义。

不幸的是所有常规的解析器类别都不适用于HTML(我并没有为了好玩把它们列出来—他们将在解析CSS和JavaScript时用到)。

HTML没法很容易的用解析器需要的上下文无关语法来定义。

有一种规范的格式来定义HTML—DTD(文档类型定义)—但它并不是上下文无关语法。

这在早期的网站上显的有些奇怪—HTML跟XML非常相似。有大批可用的XML解析器。还有种HTML的XML变种—XHTML—所以它们之间有何大的不同的呢?

区别在于HTML非常“宽容”,它允许你忽略某种隐含加入的标签,有时候忽略开始和结束标签等等。整体来说,相对于XML的严苛语法而言,这是种“软”语法。

显然看起来象是一个小小的区别创造了个不同的世界。一方面这是HTML为何如此流行的主要原因—它宽容对待你的错误,使网站编程者日子过的更轻松。另一方面,这也使得写出规范语法定义变的困难。现在总结一下—HTML不能方便的解析,不能被常规语法分析器处理因为它的语法不是上下文无关语法,也不能被XML解析器解析。

HTML的DTD

HTML的定义是DTD格式。这种格式通常用来定义SGML语言家族。这种格式包含所有允许使用元素的定义,它们的属性和层次关系。象我们之前看到的,HTML的DTD不构成上下文无关语法。

DTD也有一些变化。严格模式只支持纯粹的规范但其它模式包含对早期浏览器使用的一些标记的支持。目的是为了向后兼容于早期内容。当前的严格模式DTD在这里:http://www.w3.org/TR/html4/strict.dtd

DOM

输出树—分析树是一棵DOM元素和属性结点构成的树。DOM是文档对象模型的缩写。它是HTML文档的对象表示,也是HTML元素对外部世界比如JavaScript的接口。

树的根是“Document”对象。

DOM和标签几乎有一对一的关系。例如,这个标签:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

将会转换成以下DOM树:

浏览器的HTML解析器

图8:示例标记的DOM树

象HTML一样,DOM有W3C组织的规范。查看:http://www.w3.org/DOM/DOMTR。它是个操作文档的通用规范,一个描述HTML具体元素的特殊模块。HTML的定义能在:http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html找到。

当我提及包含DOM结点的树时,我是指由实现了DOM接口之一的元素们构成的树。浏览器采用的是包含其它一些浏览器内部使用的属性的实际实现。

解析算法

象我们在先前章节看到的,HTML不能被常规的自顶而下或自底而上解析器解析。

原因是:

1 这种语言天生的宽容度。

2 现实里浏览器对无效HTML广为流传的一些错误用法传统上具有的容错度。

3 解析过程是可重入的。通常源码在解析过程中不会变化,但是在HTML里,包含“document.write”的脚本标签能够增加另外的语法元素,因此这解析过程实际上修改了输入。

既然不能使用常规解析技术,浏览器创造了解析HTML的自已的解析器。

解析算法在HTML5规范里有详细描述。算法由两步组成—标记化和构造树。

标记化是词法分析,把输入分拆成词汇。在HTML中的词汇是开始标签,结束标签,属性名和属性值。

分词器识别词汇,把它传送给树构造器,再取得下一个字符用于识别出下一个词汇,如此继续直到输入结束。

浏览器的HTML解析器

图6:HTML解析流程(来自HTML5规范)

标记化算法

算法的输出是HTML标记。这种算法表达成一个状态机。每个状态消耗输入流的一个或多个字符,根据这些字符更新下一个状态。判定受当前标记化状态和树构造状态的影响。这意味着同一个被消耗掉的字符依据当前状态不同会产生不同的结果,以得到正确的下一步状态。算法过于复杂难以完整叙述,因此我们用一个简单的例子来帮助理解这个概念。

基本例子—对下面的HTML分词:

<html>
  <body>
    Hello world
  </body>
</html>

初始状态是“数据状态”。当遇到“<”字符时,状态切换成“标签开放状态”。消耗一个“a-z”字符导致“开始标签记号”,状态被改为“标签命名状态”。我们保持在这个状态,直到“>”字符被读到。每个字符被添加到新的标记名上。在我们的场景中创建的标记是一个“html”标记。

当到达“>”标签时,当前标记被发送出去,状态变回了“数据状态”。“<body>”标签将会被同样流程处理。截止目前我们发送出了“html”和“body”标签。我们现在回到了“数据状态”。消耗“H”字符会导致创建和发送一个字符标记,这会继续到到达“</body>”的“<”为止。我们会为“Hello world”的每个字符发送一个字符标记。

现在我们回到了“标签开放状态”。消耗下个输入的“/”将导致创建个“结束标签记号”并迁移到“标签命名状态”。我们再次保持在这个状态直到到达了“>”。然后新的标签记号会被发送, 我们返回“数据状态”。“</html>”输入会象先前情形一样被处理。

浏览器的HTML解析器

图9:标记化例子输入。

树的构造算法

当分析器创建时,文档对象也被创建了。在树的构造阶段,DOM树及它的根文档会被修改,一些元素会被加到里面。分词器发送的每个节点会被树构造器处理。对于每个标记,规范定义了哪个DOM元素与之相关,并且据此创建DOM元素。除了增加元素到DOM之外,它也增加了一个开放元素的栈。这个栈用于纠正嵌套不匹配和未关闭的标签。这个算法也用状态机描述。状态被称为“插入模式”。

我们来看一下例子输入的构造树的过程:

<html>
<body>
   Hello world
</body>
</html>

给树构造阶段的输入是来自分词阶段的标记序列。第一个模式是“初始化模式”。收到HTML标记后会转换到“前html”模式,重新处理此种模式下的标记。这会导致创建HTMLHtmlElement元素,并把它添加到根文档对象。

状态会切换到“前head”模式。我们收到“body”标记。尽管我们没有一个“head”标记,一个HTMLHeadElement将会被隐含创建并添加到树上。

我们现在转换到“内head”模式,然后是“后head”模式。body标记被重新处理,一个HTMLBodyElement元素被创建并插入,然后模式转换到“内body”模式。

“Hello world”字符串的字符标记现在收到了。第一个会引起创建并插入一个“Text”节点,其它字符会添加到这个节点。

收到body结束标记会引发到“后body”模式的转换。我们会接收html结束标签,这会转换模式成“后后body”模式。接收文件结束标记会结束这次解析。

浏览器的HTML解析器

图10:示例html的树构造

(待续)

原创文章,作者:苏葳,如需转载,请注明出处:https://www.swmemo.com/2027.html

发表评论

邮箱地址不会被公开。 必填项已用*标注