11
2013
04

浏览器工作原理 - 浏览器HTML解析器

HTML 解析器

HTML解析器的工作就是解析HTML标记到解析树。

HTML文法定义

现在的HTML文法由W3C组织定义,当前版本HTML4,HTML5已经在各个浏览器中被使用,但正式出台需要到2012年。

非上下文无关文法

正如在解析简介中提到的,上下文无关文法的语法可以用类似BNF的格式来定义。

  不幸的是,所有的传统解析方式都不适用于HTML(当然我提出它们并不只是因为好玩,它们将用来解析CSS和JavaScript),HTML不能简单的用解析所需的上下文无关文法来定义。

HTML有一个正式的格式定义——DTD(DocumentType Definition文档类型定义)——但它并不是上下文无关文法。

HTML更接近于XML,现在有很多可用的XML解析器,HTML有个XML的变体——XHTML,它们间的不同在于,HTML更宽容,它允许忽略一些特定标签,有时可以省略开始或结束标签。总的来说,它是一种soft语法,不像XML呆板、固执。

  显然,这个看起来很小的差异却带来了很大的不同。一方面,这是HTML流行的原因——它的宽容使web开发人员的工作更加轻松,但另一方面,这也使很难去写一个格式化的文法。所以,HTML的解析并不简单,它既不能用传统的解析器解析,也不能用XML解析器解析。

HTMLDTD

HTML用DTD格式进行定义,这一格式是用于定义SGML家族的语言。包括了对所有允许元素及它们的属性和层次关系的定义。正如前面提到的,HTML DTD并没有生成一种上下文无关文法。

DTD有一些变种,标准模式只遵守规范,而其他模式则包含了对浏览器过去所使用标签的支持,这么做是为了兼容以前内容。

最新的标准DTD在http://www.w3.org/TR/html4/strict.dtd

DOM

输出树——也就是解析树,是有DOM元素和属性节点组成。DOM是Document Object Model的缩写。它描述的是HTML的document对象,并且提供HTML元素的接口供外部的JavaScript调用。

树的根就是“Document”对象。

DOM标签基本是一一对应的关系,如下面的例子

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

将被转换成下面的DOM树


image015.png

例子标记的DOM树

跟HTML一样,DOM的规范也是由W3C组织制定的。http://www.w3.org/DOM/DOMTR可以看到。这是使用文档的一般规范。一个模型描述一种特定的HTML元素,可以在http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.htm查看HTML定义。

  这里所谓的树包含了DOM节点是说树是由实现了DOM接口的元素构建而成的,浏览器使用已被浏览器内部使用的其他属性的具体实现。

解析算法

正如前面章节中讨论的,HTML不能被一般的自上而下或自下而上的解析器所解析。

  原因是:

  1. 这门语言本身的宽容特性

  2. 浏览器对一些常见的非法HTML有容错机制

  3. 解析过程是往复的,通常源码不会在解析过程中发生改变,但在HTML中,脚本标签包含的“document.write”可能添加额外标签,因此在解析过程中实际上修改了输入。

  不能使用正则解析技术,浏览器为HTML定制了专属的解析器。

HTML5规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。

  符号化是词法分析的过程,将输入解析为符号,HTML的符号包括开始标签、结束标签、属性名及属性值。

符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。

image017.png

HTML解析流程


符号识别算法

算法输出HTML符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。

  这个算法很复杂,这里用一个简单的例子来解释这个原理。

基本示例——符号化下面的HTML:

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

初始状态为“Data State”,当遇到“<”字符,状态变为“Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为“Tag name state”,一直保持这个状态直到读取到“>”,每个字符都附加到这个符号名上,例子中创建的是一个HTML符号。

  当读取到“>”,当前的符号就完成了,此时,状态回到“Datastate”,“<body>”重复这一处理过程。到这里,html和body标签都识别出来了。现在,回到“Data state”,读取“Hello world”中的字符“H”将创建并识别出一个字符符号,这里会为“Hello world”中的每个字符生成一个字符符号。

  这样直到遇到“</body>”中的“<”。现在,又回到了“Tagopen state”,读取下一个字符“/”将创建一个闭合标签符号,并且状态转移到“Tag name state”,还是保持这一状态,直到遇到“>”。然后,产生一个新的标签符号并回到“Datastate”。后面的“</html>”将和“</body>”一样处理。

image019.png

符号化示例输入

树构建算法

在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。

来看一下示例中树的创建过程:

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

  构建树这一阶段的输入是符号识别阶段生成的符号序列。

  首先是“initial mode”,接收到html符号后将转换为“beforehtml”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。

  状态此时变为“before head”,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。

  现在,转到“in head”模式,然后是“afterhead”。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“in body”模式。

  然后,接收到字符串“Hello world”的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。

接收到body结束符号时,转移到“afterbody”模式,接着接收到html结束符号,这个符号意味着转移到了“after after body”模式,当接收到文件结束符时,整个解析过程结束。

image022.gif

示例HTML树的构建过程

解析结束时的活动

在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。

  文档状态将被设置为完成,同时触发一个load事件。

Html5规范中有符号化及构建树的完整算法

(http://www.w3.org/TR/html5/syntax.html#html-parser)。

浏览器容错

你从来不会在一个html页面上看到“无效语法”这样的错误,浏览器修复了无效内容并继续工作。

  以下面这段html为例:

<html> 
    <mytag> 
    </mytag> 
    <div> 
    <p> 
    </div> 
    Really lousy HTML 
    </p> 
</html>

这段html违反了很多规则(mytag不是合法的标签,p及div错误的嵌套等等),但是浏览器仍然可以没有任何怨言的继续显示,它在解析的过程中修复了html作者的错误。

  浏览器都具有错误处理的能力,但是,另人惊讶的是,这并不是html最新规范的内容,就像书签及前进后退按钮一样,它只是浏览器长期发展的结果。一些比较知名的非法html结构,在许多站点中出现过,浏览器都试着以一种和其他浏览器一致的方式去修复。

Html5规范定义了这方面的需求,webkit在html解析类开始部分的注释中做了很好的总结。

  解析器将符号化的输入解析为文档并创建文档,但不幸的是,我们必须处理很多没有很好格式化的html文档,至少要小心下面几种错误情况。

1.在未闭合的标签中添加明确禁止的元素。这种情况下,应该先将前一标签闭合

2.不能直接添加元素。有些人在写文档的时候会忘了中间一些标签(或者中间标签是可选的),比如HTMLHEAD BODY TR TD LI等

3.想在一个行内元素中添加块状元素。关闭所有的行内元素,直到下一个更高的块状元素

4.如果这些都不行,就闭合当前标签直到可以添加该元素。

  下面来看一些webkit容错的例子:

</br> instead of<br>

  一些网站使用</br>替代<br>,为了兼容IE和Firefox,webkit将其看作<br>。

代码:

if (t->isCloseTag(brTag) && m_document->inCompatMode()){ 
    reportError(MalformedBRError); 
    t->beginTag = true; 
}

Note-这里的错误处理在内部进行,用户看不到。

  迷路的表格

  这指一个表格嵌套在另一个表格中,但不在它的某个单元格内。

比如下面这个例子:

<table> 
    <table> 
        <tr><td>innertable</td></tr> 
    </table> 
    <tr><td>outertable</td></tr> 
</table> 
webkit将会将嵌套的表格变为两个兄弟表格:
<table> 
    <tr><td>outertable</td></tr> 
</table> 
<table>
    <tr><td>innertable</td></tr> 
</table>

代码:

if (m_inStrayTableContent &&localName == tableTag) 
    popBlock(tableTag);

Webkit使用堆栈存放当前的元素内容,它将从外部表格的堆栈中弹出内部的表格,则它们变为了兄弟表格。

嵌套的表单元素

用户将一个表单嵌套到另一个表单中,则第二个表单将被忽略。

代码:

if (!m_currentFormElement) { 
    m_currentFormElement= new HTMLFormElement(formTag, m_document); 
}

太深的标签继承

www.liceo.edu.mx是一个由嵌套层次的站点的例子,最多只允许20个相同类型的标签嵌套,多出来的将被忽略。

代码:

bool HTMLParser::allowNestedRedundantTag(constAtomicString& tagName) 
{ 
    unsigned i = 0; 
    for (HTMLStackElem* curr = m_blockStack; 
        i < cMaxRedundantTagDepth &&curr && curr->tagName == tagName; curr = curr->next, i++) { } 
    return i != cMaxRedundantTagDepth; 
}

放错了地方的html、body闭合标签

又一次不言自明。

支持不完整的html。我们从来不闭合body,因为一些愚蠢的网页总是在还未真正结束时就闭合它。我们依赖调用end方法去执行关闭的处理。

代码:

if (t->tagName ==htmlTag || t->tagName == bodyTag )
 return

所以,web开发者要小心了,除非你想成为webkit容错代码的范例,否则还是写格式良好的html吧。


« 上一篇下一篇 »

评论列表:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。