欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

XmlParser: 简易的 Xml 解析器的实现(二)

程序员文章站 2022-07-13 12:07:19
...

一、引言

在上一篇博客里,我主要架构了一个 xml 解析器的三大模块,分别是读取模块、解析模块、获取数据模块,并在尽快实现整体架构的目标下,实现了一个非常简略的 xml 解析器。

想要了解 xml 解析器的设计流程的同学可以点击这里 XmlParser: 简易的 Xml 解析器的实现(一)

在这一篇博客中,我将填上上一篇博客里面的坑,将读取模块里面的 xml 代码的合法性检查和解析模块里面的对于嵌套元素的解析功能一一实现。

这一篇博客虽然没有像上一篇博客里面那么大刀阔斧设计一个架构的快感,但是这两块实现(合法性检查和嵌套元素解析)都是挺复杂的逻辑,这一块可以说是一个 xml 解析器的难点所在吧。

准备好了就让我们开始吧。

二、准入检查:xml 代码的合法性检查

至于为什么要有合法性检查呢?这里我还是要多啰嗦几句:

作为一个程序员,永远要记得对于一个处理过程的输入的合法性检查保持条件发射般的敏感。只有在你的输入合乎逻辑之后,才能让你后续的处理过程不会产生匪夷所思的错误。

那么这里 xml 的合法性检查,要进行哪些检查呢?这里因为我们实现的毕竟只是一个简易的 xml 解析器,自然不能跟其他强大的库比,因此暂时就只实现以下三点检查吧:

  1. 尖括号 <> 必须搭配出现
  2. 反斜线 / 出现的时候 ,前面必然有一个未配对的 <,并且 /只能以 </ 或者 /> 这两种形式出现
  3. 双标签对元素标签头和标签尾的名称一定要一致

那么就让我们一步一步来实现这三个条件的检查吧。

首先,尖括号 <> 必须搭配出现,也就是说出现了一个 < 就必须有一个 >封住标签,那么使用一个栈来进行检查是最简单的了。只需要遍历 xml 代码,遇到 < 压栈,遇到 > 弹出栈即可。

其次,反斜线的情况比较复杂,不过也好解决。我们在得到了一个 / 之后,首先检查它是不是 </ 或者/> 这两种显示方式(分别对应了双标签对和单标签),解决这个问题可以遍历解决。另外,我们的反斜线是与标签一一对应的。那么出现了 / 则必然有一个 < 还没有得到匹配,这一点也需要记入考虑中去。

上面两条的检查代码如下:

// 检查:输入文本是否合法
// 1. 尖括号 < 和 > 必须搭配出现
// 2. 反斜线 / 出现的时候 < 必须在紧接着之前出现过
// 3. 两个标签对单词一定要一样,单标签元素无需处理
bool CWangYingXmlParser::_CheckXmlValid(std::string strXml)
{
    std::stack<char> stackBracket;      // 栈:尖括号
    std::stack<char> stackBackSlash;    // 栈:反斜线
    std::stack<std::string> statckTab;  // 栈:标签对
    bool bBracketValid = true;          // 合法:尖括号匹配
    bool bBlackSlashValid = true;       // 合法:反斜线
    bool bIsSymmetry = true;            // 合法:标签对称性
    // 检查:括号匹配
    for (int i = 0; i < strXml.size(); ++i) {
        // --记录:当出现 < ,则直接压栈
        if ('<' == strXml[i]) stackBracket.push(strXml[i]);
        // --记录:当出现 > ,则判断栈是否为空、是否栈顶为 '<',满足条件则出栈
        else if ('>' == strXml[i] && stackBracket.size() != 0 && stackBracket.top() == '<')
            stackBracket.pop();
        // --记录:当出现 / ,则判断栈顶是否为 <、左侧是否有 < 或者右侧是否有 >,
        // 满足条件压栈(计算项目个数),否则报错
        else if ('/' == strXml[i] && stackBracket.top() == '<' &&
            ((i != 0 && '<' == strXml[i - 1]) || (i + 1 != strXml.size() && '>' == strXml[i + 1])))
            stackBackSlash.push(strXml[i]);
        else if ('/' == strXml[i]) {
            bBlackSlashValid = false;
            break;
        }
    }
    // 出错:反斜线不匹配
    if (!bBlackSlashValid)
        m_strErroMessage += "parse error: black slash write error.\n";
    // 出错:尖括号不匹配
    if (stackBracket.size() != 0) {
        bBracketValid = false;
        m_strErroMessage += "parse error: bracket does not matched.\n";
    }
    // 出错:标签对不对称
    _CheckTabSymmetry(strXml, &bIsSymmetry);
    if (!bIsSymmetry) {
        m_strErroMessage += "parse error: tab does not symmetry.\n";
    }
    return bBlackSlashValid && bIsSymmetry && bBracketValid;
}

这一块代码是检查 xml 代码合法性的入口函数,因此包含了一共三种条件的检查,以上暂时只讨论了前两种条件的检查,由于代码注释非常清晰,这里也就不再详述了。

关键就是如何实现第三个条件:双标签对元素标签头和标签尾的名称一定要一致。也就是上述代码中的 _CheckTabSymmetry() 函数的具体实现。

首先,我们要清楚,双标签对中的标签里面的名称是要我们通过正则去解析得到的。这里我们就需要写三种正则表达式,第一种是解析标签头和标签尾的正则表达式,第二种则是从标签头中解析出标签名称的正则表达式,第三种是从标签尾解析出标签名称的正则表达式。

// 匹配:双标签对的标签头和尾
<\w+[^\n/<]*>|</\w+[\s]*>
// 匹配:标签头里面的标签名
<\w+\b
// 匹配:标签尾里面的标签名
/\w+

有关这三个正则表达式的具体含义,建议不懂的同学去看下资料正则表达式30分钟入门教程。这里也就不再详述了(不用全部看完,这个正则表达式的教程大概看到后向引用即可)。

其次,我们能够使用正则表达式解析出来需要的标签名之后,就可以使用栈的压入弹出来检查标签名的配对了。

这一块的具体实现代码如下:

// 检查:标签对对称性检查
// 匹配:双标签对的标签头
// <\w+[^\n/<]*>
// 匹配:标签头里面的标签名
// <\w+\b
// 匹配:双标签对的标签尾
// </\w+[\s]*>
// 匹配:标签尾里面的标签名
// /\w+
// 匹配:双标签对的标签头和尾
// <\w+[^\n/<]*>|</\w+[\s]*>
bool CWangYingXmlParser::_CheckTabSymmetry(std::string strXml, bool *pbIsSymmetry)
{
    if (!pbIsSymmetry) return false;
    bool bIsSymmetry = true;
    std::string s = strXml;
    std::smatch m;
    std::regex regexTab("<\\w+[^\\n/<]*>|</\\w+[\\s]*>");
    std::regex regexHeadName("<\\w+\\b");
    std::regex regexTailName("/\\w+");
    std::stack<std::string> stackTab;
    // 找到双标签对的标签头
    while (std::regex_search(s, m, regexTab)) {
        // --检查:根据匹配的是标签头还是标签尾进行不同的处理
        std::string strTemp = m.str();
        std::smatch tempMatch;
        // --匹配:标签头
        if (std::regex_search(strTemp, tempMatch, regexHeadName)) {
                std::string strTabName = tempMatch.str();
                strTabName.erase(0, 1);
                stackTab.push(strTabName);
        } 
        // --匹配:标签尾
        else {
            if (std::regex_search(strTemp, tempMatch, regexTailName)) {
                std::string strTabName = tempMatch.str();
                strTabName.erase(0, 1);
                if (stackTab.size() != 0 && stackTab.top() == strTabName) {
                    stackTab.pop();
                }
            }
        }
        s = m.suffix().str();
    }
    *pbIsSymmetry = stackTab.size() == 0 ? true : false;
    return true;
}

这段代码注释相当详细,相信只要你了解了正则的表达,就应该能够了解这段代码的含义,这里也就不再赘述了(这里主要使用了 C++ 的正则库,有关 C++ 正则库的使用方法可以自行查询资料学习)。

至此,xml 代码的合法性检查的实现完成:)

三、嵌套解析:元素的子元素的子元素的…

我们要清楚的是,xml 是一个树形结构,也就是说一个元素可以有多个子元素,而这些子元素又可以有多个子元素……我们要达到完全解析 xml 代码的目的,必然要使用循环或者递归的方法去穷尽它。

这里我选择了递归的方法。

首先,在获得了一句 xml 代码(也就是一个元素的 xml 代码)之后,我们对当前的元素进行解析。

然后,我们取出这个元素中间携带的内容,用正则表达式去匹配检查,看它到底是子元素还是一个 text 属性值。如果是子元素,我们就递归调用解析元素的代码,如果是 text 属性,我们就为当前元素增加一个 text 属性即可。

代码逻辑非常简单,这块的实现代码如下:

bool CWangYingXmlParser::_ParseOneItem(std::string strOneItemXml, WangYingXmlParser::CItem *pItem) {
    // 解析:元素名称
    ......
    // 解析:元素属性
    ......
    // 解析:解析元素的子元素或者 text 属性
    _ParseSubItem(strTempText, &newItem);
    // 装载:将新的元素加入记录
    ......
}

以上是一个元素的解析过程伪代码,其中我将递归调用的代码封装到了 _ParseSubItem() 函数中去了:

// 解析:解析一个项目的子项目或者 text 属性(在项目的标签对之间的认为是子项目或者 text 属性)
bool CWangYingXmlParser::_ParseSubItem(std::string strOneItemXml, WangYingXmlParser::CItem *pItem)
{
    if (pItem == nullptr) return false;
    std::string s = strOneItemXml;
    std::smatch m;
    std::regex regexItem("<(\\w+).*>.*</\\1>|<\\w+[^/>]*/>");
    // 匹配:识别成项目,递归调用 _ParseOneItem 匹配子项目
    if (std::regex_search(s, m, regexItem)) {
        WangYingXmlParser::CItem newSubItem;
        _ParseOneItem(m.str(), &newSubItem);
        pItem->subitems.push_back(newSubItem);
        s = m.suffix().str();
        while (std::regex_search(s, m, regexItem)) {
            WangYingXmlParser::CItem newSubItem;
            std::string strss = m.str();
            _ParseOneItem(m.str(), &newSubItem);
            pItem->subitems.push_back(newSubItem);
            s = m.suffix().str();
        }
    } 
    // 匹配:识别成 text 属性
    else {
        WangYingXmlParser::CAttribute textAtrribute;
        textAtrribute.name = "text";
        textAtrribute.value = s;
        pItem->attributes.push_back(textAtrribute);
    }
    return true;
}

这段代码,通过正则匹配来检查当前内容是子元素还是 text 属性,如果识别为子元素,则递归调用了 _ParseOneItem()函数来解析子元素内容,否则就识别为 text 属性。代码逻辑非常简单,这里也就不再赘述了。

至此,上一篇博客里面留下的最大的坑也就填上了:)

四、编译运行:让我们看看最后的结果吧

写了这么多,当然需要看看运行结果吧。

首先,我们写死下面这份 xml 代码:

<person><name><ag/e><country </country></monkey>

很明显三条规则都触犯了的不合法的 xml 代码,看看我们的程序输出是什么:

XmlParser: 简易的 Xml 解析器的实现(二)

接着,我们写死下面这份 xml 代码:

<person isStudent="false">
  <name>hello world</name>
  <age>12</age>
  <country isAsia="true">china</country>
  <hobby>
    <coding language="c++"/>
    <sexygirl/>
  </hobby>
</person>

很明显这是一份多层嵌套的 xml 代码,我们来看看程序会输出什么:

XmlParser: 简易的 Xml 解析器的实现(二)

可以看到,我们的程序成功解析出来了三层的元素逻辑,并且正确输出了每个元素的属性值,还对每一层的元素进行了缩进输出。

本节任务完美完成,撒花^_^

五、总结

在上一篇博客实现了从 0 到 1 的架构之后,这一篇博客主要对准入和复杂的嵌套元素解析进行了深入的探讨。

从 0 到 1 的开发过程无疑是令人欣喜的。发现问题和解决问题的过程也是一种成长。

至此,我们的 XmlParser 已经是一个比较完善的解析器了。虽然还有这里那里的不足,毕竟也对得起 “简易”这两个字了。

当然了,有了我们自己的 Xml 解析器之后,我们也可以做很多有趣的玩意儿了。再比如说给我们的 Xml 解析器加上好看的界面,再在读取模块增加更多的人性化功能等等。这些都是后期还可以继续探讨的地方。

最后,还是附上本项目代码的下载地址(毕竟注释非常详尽,想要了解的同学可以点击下面的链接):

wangying2016/WangYingXmlParser

To be Stronger!