如何构建 HTML 解析器?

Posted

技术标签:

【中文标题】如何构建 HTML 解析器?【英文标题】:How to build an HTML parser? 【发布时间】:2013-08-02 16:28:04 【问题描述】:

在开始链接到 RegEx match open tags except Xhtml self-contained tags 之前,请阅读整个问题。

我想写一个 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>

现在,谁能告诉我如何解析这个(最终形式无关紧要,只是一个概念)?我有一些想法(比如使用递归函数,或者引用包含实际标签的数组),但我不认为这些是最好的概念。 我应该逐个检查char然后调用特定的函数还是使用正则表达式(解释如下)?

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

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

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

【问题讨论】:

我不认为你的回答对我有帮助......我之前看到过这个问题,我在我的问题中写道“通过使用正则表达式我并不是指整个标签的一种模式。 " 顺便问一下,你是怎么在不到 2 分钟的时间内读完这篇文章的? 这能回答你的问题吗? Writing an HTML Parser 【参考方案1】:

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

在我看来,正则表达式可能是矫枉过正/不合适的 - 您希望匹配 HTML 标记,逐个字符解析可能是最好的方法。

【讨论】:

+1 用于提及词法分析器。这就是 OP 真正需要关注的问题。关于需要词法分析器的更多观点,@JXG 在此线程中的评论具有指导意义:***.com/questions/2400623/… 嗨,我是那篇博文的作者,只是想提一下,如果你知道你的语法,你可以使用像 ANTLR 这样的解析器生成器更快地启动和运行。根据您的需要,从头开始构建整个词法分析器可能会有些过大。此外,值得研究解析器组合器和组合器库以获取 AST 而无需经过标记化阶段 链接失效:((((【参考方案2】:

@Kian 的回答提到使用词法分析器,但就算法而言,我认为你会想要使用递归。 HTML毕竟是一个递归结构:

<div>
    <div>
        <div>
        </div>
    </div>
</div>

这是一个简单的 JS 示例——尽管它不是一个完整的实现。 (我不支持&lt;empty /&gt; 元素;&lt;!-- comments --&gt;&amp;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;

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

element    ::=    EmptyElemTag | STag content ETag

...所以理想情况下,我们希望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);
    

...但是我在编写示例时已经走捷径,所以它并没有尽可能地反映实际语法。

【讨论】:

以上是关于如何构建 HTML 解析器?的主要内容,如果未能解决你的问题,请参考以下文章

如何构建 html5lib 解析器来处理混合的 XML 和 HTML 标签?

如何创建 XML 解析器?

当我在 Chrome 中单击检查时,如何让 Beautiful soup html 解析器与显示的代码相同?

如何从可动手做的构建中排除热公式解析器?

如何使用 SAX 解析器解析 XML

模块构建失败:错误:没有给出解析器和文件路径,无法在 nuxtjs 中推断出解析器