如何构建HTML解析器?

use*_*214 6 html parsing structure

在开始链接到RegEx匹配开放标记之前,除了XHTML自包含标记读取整个问题.

我想编写一个HTML解析器(仅针对HTML 5,它应检查它是否为HTML 5,如果不是,则返回错误)只是为了让自己学到新的东西,但我不知道什么是最好的方法去做.让我举个例子:

<!doctype html>
<html>
<head>
    <!-- #TITLE -->
    <title>Just an example</title>
</head>
<body>
    <p class='main'>Simple paragraph with an <a href='/a.html'>anchor</a></p>
</body>
</html>
Run Code Online (Sandbox Code Playgroud)

现在,有人能告诉我如何解析这个(最终形式无所谓,只是一个概念)?我有一些想法(比如使用递归函数,或者引用包含实际标记的数组),但我不认为这些是最好的概念.我应该通过char检查char,然后调用特定的函数或使用正则表达式(如下所述)?

通过使用正则表达式,我并不是指整个标签的一种模式.我的意思是使用一个模式用于标记名(如果这个模式返回true,请检查下一个模式),然后是属性(如果这个返回true,再次检查),最后检查标记的结尾.

找到标签后该怎么办?运行一个循环来检查标签(如果它找到标签,再次调用它......)?但对我来说,当函数X调用Y调用X时,它似乎是递归函数或至少半递归...

所以最后一个问题是:最有效和最正确的结构是什么?

Kia*_*ian 5

编写基于SGML的解析器的最大部分是词法分析器。这是有关构建自定义词法分析器的文章:http : //onoffswitch.net/building-a-custom-lexer/

在我看来,正则表达式可能是过大的/不合适的-您想匹配HTML标记,并且逐字符解析可能是实现此目的的最佳方法。


Ric*_*uen 5

@Kian的答案提到使用词法分析器,但就算法而言,我认为您将要使用递归。HTML毕竟是一种递归结构:

<div>
    <div>
        <div>
        </div>
    </div>
</div>
Run Code Online (Sandbox Code Playgroud)

这是一个朴素的JS示例-尽管它不是一个完整的实现。(我不包含对<empty />元素的支持;对于<!-- comments -->;对于&entities;;对于xmlns:namespaces……编写完整的HTML或XML解析器是一项艰巨的任务,因此请不要掉以轻心)

该解决方案尤其是跳过了词法分析的过程,但是我故意省略了这一点,以将我的答案与@Kian的答案进行对比。

var markup = "<!DOCTYPE html>\n"+
             "<html>\n"+
             " <head>\n"+
             "   <title>Example Input Markup</title>\n"+
             " </head>\n"+
             " <body>\n"+
             "   <p id=\"msg\">\n"+
             "     Hello World!\n"+
             "   </p>\n"+
             " </body>\n"+
             "</html>";

parseHtmlDocument(markup);

// Function definitions

function parseHtmlDocument(markup) {
    console.log("BEGIN DOCUMENT");
    markup = parseDoctypeDeclaration(markup);
    markup = parseElement(markup);
    console.log("END DOCUMENT");
}

function parseDoctypeDeclaration(markup) {
    var regEx = /^(\<!DOCTYPE .*\>\s*)/i;
    console.log("DOCTYPE DECLARATION");
    var matches = regEx.exec(markup);
    var doctypeDeclaration = matches[1];
    markup = markup.substring(doctypeDeclaration.length);
    return markup;
}

function parseElement(markup) {
    var regEx = /^\<(\w*)/i;
    var matches = regEx.exec(markup);
    var tagName = matches[1];
    console.log("BEGIN ELEMENT: "+tagName);
    markup = markup.substring(matches[0].length);
    markup = parseAttributeList(markup);
    regEx = /^\>/i;
    matches = regEx.exec(markup);
    markup = markup.substring(matches[0].length);
    markup = parseNodeList(markup);
    regEx = new RegExp("^\<\/"+tagName+"\>");
    matches = regEx.exec(markup);
    markup = markup.substring(matches[0].length);
    console.log("END ELEMENT: "+tagName);
    return markup;
}

function parseAttributeList(markup) {
    var regEx = /^\s+(\w+)\=\"([^\"]*)\"/i;
    var matches;
    while(matches = regEx.exec(markup)) {
        var attrName = matches[1];
        var attrValue = matches[2];
        console.log("ATTRIBUTE: "+attrName);
        markup = markup.substring(matches[0].length);
    }
    return markup;
}

function parseNodeList(markup) {
    while(markup) {
        markup = parseTextNode(markup);
        var regEx = /^\<(.)/i;
        var matches = regEx.exec(markup);
        if(matches[1] !== '/') {

            markup = parseElement(markup);
        }
        else {
            return markup;
        }
    }
}

function parseTextNode(markup) {
    var regEx = /([^\<]*)\</i;
    var matches = regEx.exec(markup);
    markup = markup.substring(matches[1].length);
    return markup;
}
Run Code Online (Sandbox Code Playgroud)

理想情况下,这些函数中的每一个都可以非常紧密地映射到XML规范中定义的语法。例如,规范定义element如下:

element    ::=    EmptyElemTag | STag content ETag
Run Code Online (Sandbox Code Playgroud)

...因此理想情况下,我们希望parseElement()函数看起来像这样:

function parseElement(markup) {
    if(nextTokenIsEmptyElemTag) { // this kind of logic is where a lexer will help!
        parseEmptyElemTag(markup);
    }
    else {
        parseSTag(markup);
        parseContent(markup);
        parseETag(markup);
    }
}
Run Code Online (Sandbox Code Playgroud)

...但是我在编写示例时走了一些弯路,因此它并没有尽可能准确地反映实际语法。