65.9K
CodeProject 正在变化。 阅读更多。
Home

ml_reader.hpp: 用于物联网及更广泛应用的标记拉取解析器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2021年7月25日

MIT

10分钟阅读

viewsIcon

7923

downloadIcon

102

高效、低级别地解析HTML和XML等标记

ml_reader

引言

最近,我需要从物联网设备解析一些HTML。市面上几乎没有现成的解决方案,这可以理解,因为它属于一个相对小众的领域。但就读取任意大小、可能非常大的文档而言,即使是XML,也鲜有适用于物联网的方案,更不用说HTML了。

我还需要它不进行任何验证或格式正确性检查。原因是它将作为渲染引擎的一部分使用。渲染不需要验证或格式正确性检查,并且这样做会适得其反。

我能找到的满足这些要求的方案寥寥无几。这就是我构建它的原因。请注意它的局限性。

概念化这个混乱的局面

让我们从一些免责声明和指南开始,然后对这段代码是什么做一个基本描述。

限制

  • 这是一个拉取解析器。它们非常高效,但根据你的具体用途,使用起来可能有点麻烦。对于渲染来说,它们很简单。对于结构化数据导航,它们则不那么方便。
  • 此解析器只进行最少的检查以确保你的文档合规。只有在无法确定下一步操作时它才会停止解析。标签不需要成对出现。它不关心标签名称或它们出现的位置。你可以在文档中任意位置插入<?xml?>声明,它也不会在意。
  • 此解析器无法处理XML命名空间解析、DTD解析、模式解析或自定义实体解析。基本上,它不会在后台进行任何元素和属性数据的映射。如果你需要一个XML命名空间管理器,你需要自己创建一个。
  • 此解析器不执行任何空格规范化。你需要自己进行适当的空格规范化,以确保输出合规。
  • 它仅支持Unicode UTF-8,尽管内部会将所有内容解析为UTF-32代码点。Unicode部分尚未经过大量测试,而且XML似乎主要是围绕UTF-16代码点设计的?
  • HTML实体编码支持会使代码体积大幅增加,相比仅支持XML的部分,会增加30KB-40KB。

何时使用此读取器

  1. 你需要一个扁平化的半结构化标签和文本流,通常用于过滤、搜索或渲染。
  2. 你需要抓取一些HTML。请参阅#1。
  3. 你想在此基础上构建一个XML读取器
  4. 你只需要一个快速简陋的读取器,并且愿意接受它的不足之处。

何时不应使用此读取器

  1. 你想要访问高度结构化的XML数据。(你必须在此读取器之上构建XML合规性检查、添加堆栈以及XML命名空间和自定义实体解析支持。)
  2. 你想要一个丰富、易于使用的XML处理器。(你必须在此基础上构建更高级别的功能,例如DOM。)
  3. 你想将HTML转换为XML。(你必须构建HTML合规性检查并添加堆栈。)
  4. 你想安全可靠地访问基于SOAP和WSDL的Web服务。请参阅#1。

什么是拉取解析器?

拉取解析器是一种高效的解析器,它以增量方式、一次一步地解析。你通常在一个循环中调用它,在每个循环迭代中,你查询各种解析器状态来确定你在文档中的位置以及光标下的内容。使用它的大致方式如下:

while(reader.read()) {
    switch(reader.node_type()) {
        case ml_node_type::element:
            // TODO: do element processing
            // reader.value() gets the element name
            break;
        case ml_node_type::content:
            // TODO: do content processing
            // reader.value() gets the content
            break;

        case ml_node_type::element_end:
           // TODO: do end element processing
           // reader.value() gets the element name
           break;
        case ml_node_type::attribute:
           // do attribute processing
           // reader.value() gets the attribute name
           break;
        ...
    }
}

使用它们的优点是它们不需要太多内存,它们可以与仅前向流式源(如来自网络的HTTP流)一起工作,并且总的来说,它们非常快。

缺点是它们可能难以直接使用,尽管你可以很容易地在它之上实现更高级别的代码。返回的数据通常是一个扁平的流。要在此流上施加层次结构,你通常需要使用堆栈,例如在element时压栈,在element_end时出栈。在我的用例中,我不需要这种额外的结构,所以我想找一个能让我更接近底层的东西。

拉取解析标记

拉取解析标记涉及到处理各种标记文档结构,如元素<foo>、结束元素</foo>、属性foo/foo="bar"、注释<!--foo-->、处理指令<?foo?>和声明<!foo>

此外,由于此拉取解析器旨在占用极少的内存并避免堆分配,因此它会流式传输内容——包括属性值内容——作为一系列块,这意味着你不会在一次read()调用中获得整个值,除非整个内容可以放入你的捕获缓冲区。为了避免复杂性过高,解析器不会流式传输名称,如元素名或属性名。整个名称必须适合捕获缓冲区,但此限制不适用于元素、文档或属性内容。默认情况下,ml_reader使用1KB的捕获缓冲区,这已经相当充裕了。这意味着文档中的元素和属性名称可以长达1KB,并且值和内容也可以这么长而不会被拆分并一块一块地返回。因此,元素和属性名称的长度最多可以达到1KB,而值和内容也可以达到这个长度而不会被分割并分块返回。

实际解析

请记住,以下很多内容在物联网设备上可能不那么实用。我列出以下内容不应被视为一种推广。例如,我看不出在Arduino上运行HTML美化打印程序有什么意义。然而,我出于完整性的考虑提供了这些用途,因为我不想过于武断地假设我能想到在物联网设备上美化打印HTML的所有可能原因。我只是笼统地涵盖了我能想到的所有方面,无论我是否看到了使用场景。

用这个玩意儿渲染

要用这个东西进行实际渲染,你可能需要在循环时维护某种上下文结构。当你遇到开标签时,比如<b>标签,你会在上下文中指示粗体已启用。从那时起,你将以粗体绘制文本,直到遇到</b>标签。整个文档都是这样渲染的。如果你想处理CSS样式或任何更复杂的东西,你可能需要一个堆栈。商业网络浏览器就是这样做的。

用这个玩意儿搜索和抓取

用这个东西做起来其实相当容易。你只需循环并忽略除你想要的特定属性或元素之外的所有内容。例如,如果我想获取文档中的所有URL(包括图片),我会循环查找hrefsrc属性,并暂存它们的属性内容。

美化打印和合规性检查

这些实际上是底层非常相似的任务,这就是为什么我把它们放在一起。无论哪种情况,你都必须编写代码来施加一个层次结构并验证输入文档的格式是否正确。这需要一个堆栈。如果你解析的是XML,你还必须小心地限制它能接受的实体类型,并确保属性引号是一致的。

用这个玩意儿进行数据交换

噢,你以为你想用它来处理XML结构化数据,是吧?哈!祝你好运。仅靠它本身,这个读取器甚至不能检查你的XML是否格式正确,更不用说合规了,当然也没有验证。这些功能对于使你的代码安全可靠是必不可少的。不过,正如我在“美化打印和合规性检查”部分提到的,你可以基于这个读取器来构建它们。你可能还需要某种XML命名空间解析器代码。如果你只需要XML,并且大部分是合规的XML,那么你可能会从这里的代码中获得更多帮助。

编写这个混乱的程序

下面,我们将对一个标记文档执行一种简陋的“身份转换”。我们从输入流中读取它,将其转换为结构化形式,然后在输出流上重现它。这样做基本上可以演示和测试解析器的所有(或至少大部分)功能。

身份转换并不精确。它没有完成一个重要的事情——它不会重新编码之前解码过的实体引用,所以例如你的&nbsp;不会被完整地返回。在某些情况下,这会导致输出错误,所以请不要在生产环境中使用下面列出的示例代码。它仅用于测试和演示。

解析很奇怪——拉取解析更是如此。下面代码中的一个粗糙之处——说实话,如果这是生产代码,我会组织得更好,重复也更少——是读取器没有明确指示它何时从元素过渡到其内容,代码必须处理这一点。问题在于从属性属性内容元素节点类型移动到结束标签、嵌套标签或嵌套内容时。读取器不会说“嘿,我们刚刚越过了开标签,是时候打印一个>了!”,因为在一般情况下没有充分的理由让它这样做。只有当我们实际上试图将标记文本重构到输出流时,它才重要,而这在实践中几乎从不发生,除非你在进行美化打印,而你并没有这么做,对吧?

除此之外,我们必须小心地记住,对于同一个属性,你可能会多次收到属性内容,而且你也可以得到没有属性内容的属性,例如<input disabled>。只有当你的属性实际上有内容时,你才会得到ml_node_type::attribute_contentml_node_type::attribute_end的调用。对于上面的标签,你不会得到这些调用。

除了value()成员,我们还查找attribute_quote()is_empty_element()来确定用于属性的引用分隔符(如果有)以及元素是否以/>结尾。

下面我提供了普通的C++代码。但是,这也可以在Arduino框架上工作,只是缺少printf()会让这段代码更长。

const char* path ="ch01.xhtml";
file_stream fs(path);
if(!fs.caps().read) {
    printf("couldn't find file\r\n");
    return -1;
}
ml_reader mr(&fs);
int first_attr_val = 0;
ml_node_type ont = ml_node_type::initial;
int empty_elem = 0;
while(mr.read()) {
    switch(mr.node_type()) {
        case ml_node_type::comment:
        case ml_node_type::pi:
        case ml_node_type::notation:
        case ml_node_type::element:
        case ml_node_type::content:
            if(ont==ml_node_type::attribute||
                ont==ml_node_type::attribute_end||
                ont==ml_node_type::element) {
                if(empty_elem) {
                    printf("/");
                }
                printf(">");
            }
            break;
    }
    switch(mr.node_type()) {
        case ml_node_type::comment:
            printf("<!--");
            break;    
        case ml_node_type::comment_end:
            printf("-->");
            break;
        case ml_node_type::pi:
            printf("<?%s ",mr.value());
            break;    
        case ml_node_type::pi_end:
            printf("?>");
            break;
        case ml_node_type::notation:
            printf("<!%s ",mr.value());
            break;
        case ml_node_type::element:
            printf("<%s",mr.value());
            break;
        case ml_node_type::attribute:
            printf(" %s",mr.value());
            first_attr_val = 1;
            break;
        case ml_node_type::attribute_content:
            if(0!=first_attr_val) {
                first_attr_val = 0;
                if(0!=mr.attribute_quote())
                    printf("=%c",mr.attribute_quote());
                else
                    printf("=");
            }
            printf("%s",mr.value());
            break;
        case ml_node_type::element_end:
            if(!mr.is_empty_element()) {
                if(ont==ml_node_type::attribute||
                    ont==ml_node_type::attribute_end||
                    ont==ml_node_type::element) {
                    printf(">");
                }
                printf("</%s>",mr.value());
            } else {
                printf("/>");
            }
            break;
        case ml_node_type::attribute_end:
            first_attr_val=0;
            if(0!=mr.attribute_quote())
                printf("%c",mr.attribute_quote());
            break;
        case ml_node_type::comment_content:
        case ml_node_type::pi_content:
        case ml_node_type::notation_content:
        case ml_node_type::content:
            printf("%s",mr.value());
            break;
        case ml_node_type::notation_end:
            printf(">");
            break;
    }
    ont = mr.node_type();
    if(ont==ml_node_type::element) {
        empty_elem = mr.is_empty_element();
    }
}
printf("\r\ndone!\r\n");
return 0;

与我最近的其他作品一样,这使用了我的流库(已包含)来处理I/O。再次强调,在IoT环境下STL并非总是可用,这就排除了使用std::iostream<>的可能性,因为在任何给定的Arduino框架实现中,它有多少被实现都是值得怀疑的。

关注点

ml_reader_fa.hpp中,你会发现几个大型整数数组。这些是确定性有限状态自动机表。它们的作用是帮助解码实体,如&nbsp;&#65;。在该文件的底部是基于这些状态机匹配文本的代码,其中有两个——一个用于XML,一个同时包含HTML和XML。它的工作方式是逐个字符地从流中读取字符,并在选定的状态机上运行输入,直到达到一个“接受状态”。一旦达到,接受状态就包含实体翻译成的Unicode代码点,例如,&nbsp;将解析为0xA0

我没有手动构建这些数组。那将是几乎不可能的。相反,我编写了一个C#应用程序ml_entity_gen。它基于我去年为C#和VB.NET编写的词法分析器生成器Rolex的代码。然而,我已经为我需要的代码进行了剥离,然后构建了一些快速简陋的例程来生成C++数组。我已包含代码以供参考,但如果你想要一些易于阅读的词法分析器生成器代码,请下载Rolex项目。上述项目中的源代码为了简洁而被最小化。

历史

  • 2021年7月25日 - 初次提交
© . All rights reserved.