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

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

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

一、引言

工作中接触到 xml 的机会比较多,比如使用 xml 文件来配置界面显示。于是,也就慢慢萌生了想要自己去实现一个简易的 xml 解析器的想法。

首先,让我们看看 xml 配置文件都长什么样子,这是来自 W3school 的示例 xml 代码:

<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>

让我们看看这份 xml 代码,我们可以看到标签对、树形结构,当然还有这里并没有展现出来的元素的属性值。当然了,为了更好的开始我们的 XmlParser 的开发,我自己随意写了一份 xml 代码:

<name sex="male">hello world</name>
<age>12</age>
<love action="coding"/>

这一份简单的没有涉及到嵌套的 xml 代码也就是这篇博客的解析目标了。当然,后期会有更多的详细的任务,这里为了更快的开发,所以暂且简化任务目标,就以这么简单的 xml 代码进行解析吧(所谓迭代开发^_^)。

xml 是一种自描述性非常强大的超文本语言,理论上来说,我们只需要将指定格式的内容解析出来,存储到适合的结构中去,就可以实现我们自己的 xml 解析器了。

二、架构设计:麻雀虽小,但也要五脏俱全

经过思考,xml 解析器的设计,暂时可以分为这三个模块:

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

这三个模块体现了 xml 解析器的工作过程:

  1. 首先,读取 xml 代码。这一过程可能是从文件中读取,可能是直接拿到了字符串;这一过程中或许需要对 xml 代码进行初步的合法性检查;这一过程甚至还可以封装比较漂亮的界面,可以通过打开文件、粘贴代码到某一个输入框中等等,都是属于这个模块的任务。不过为了更快建立起整体架构,这一块的任务会暂时简化(直接写死字符串)。

  2. 其次,解析 xml 模块是最核心的模块。我们需要将获取到的 xml 代码进行解析,解析出来它的元素、元素的属性及其属性值等等信息。甚至包括 xml 的一些特别的表达比如注释也要甄别出来。不过同样为了简化任务,这一块暂时不考虑嵌套着的元素解析(后期会做出来)。

  3. 最后,获取 xml 数据模块。我们解析了 xml 代码,最重要的是要使用里面的携带信息,通过这些,我们才能达到我们 xml 解析器的最终意义。这一块的使用范围是很大的,你可以输出所有的元素和属性的数据、在界面上将 xml 配置的元素和属性拼凑成一个漂亮的动态界面等等。同样为了简化任务,我们依然在这里只作输出操作(有趣的实现后期会考虑实现)。

至此,我们已经考虑好了 xml 解析器的整体架构了,那么我们是否就可以开始了呢?

还有一步,非常重要的,数据结构的设计。

二、设计先行:存储结构设计

我们程序员或许都听说过这么一句话:

程序 = 数据结构 + 算法

暂不论这句话是否合理,这至少说明了数据结构的设计在编程实践中的重要性。这里我们要解析 xml 代码,那么解析出来的结果放在哪里呢?这就需要我们来设计了。

我们仔细观看 xml 的结构,发现它就是一个树形结构。其中的每一个节点我们可以理解为对应着元素,每个元素会有各种各样的属性,那么我们就可以设计出来我们想象中的 xml 数据结构了。

怎么开始呢?

先从简单的来,先设计一个属性结构吧:

// 属性
struct CAttribute {
    std::string name;       // 属性名称
    std::string value;      // 属性值
};

可以看到,属性结构非常简单,包括名称和值。

属性依附在什么上呢?元素,那么我们再设计一个元素结构:

// 项目
struct CItem {
    std::string name;               // 项目名称
    std::vector<CItem> subitems;    // 子项目
    std::vector<CAttribute> attributes;
};

可以看到,元素结构中包含了三个结构,名称自然不必说,subitems 是当前元素的子元素的集合,attributes 是当前元素的属性的集合。有没有觉得这样的简化让问题变得更加简单了。

最后,为了方便我们将整个 xml 代码看作一个对象,我们定义了一个最高等级的结构:

// 文档
struct CDocument {
    std::vector<CItem> items;   // 项目列表
};

这个结构中包含了多个元素,通过这个结构,我们可以访问整个 xml 代码的数据。

三、编码实现:XmlParser 雏形展现

我们前面设计了那么多,是时候开始实现它了。

我们在 main() 函数中读取一个写死了的 xml 字符串代码,然后调用我们写好的 XmlParser 类解析它,最后再通过它获取到 xml 的所有信息,最后输出我们想要输出的信息。

以下是 main() 函数处的实现:

int main()
{
    CXmlParser xmlParser;
    // 解析
    std::string strXml = "<name sex=\"male\">hello world</name><age>12</age><love action=\"coding\"/>";
    std::cout << "得到的 xml 文本:" << std::endl << strXml << std::endl << std::endl;
    xmlParser.ParseXml(strXml);
    // 获取
    XmlParser::CDocument xmlDocument;
    xmlParser.GetXmlDocument(&xmlDocument);
    int nItemIndex = 0;
    int nAttriIndex = 0;
    std::cout << "解析后得到的信息:" << std::endl;
    for (auto item : xmlDocument.items) {
        std::cout << "item" << nItemIndex++ << "            " << item.name << std::endl;
        nAttriIndex = 0;
        for (auto atrribute : item.attributes) {
            std::cout << "attribute" << nAttriIndex++ << "              " \
                << atrribute.name << "      " << atrribute.value <<std::endl;
        }
    }
    std::cout << std::endl;
    system("pause");
    return 0;
}

看到了我们调用处的实现,我们剩下的任务就是如何实现 XmlParser 这个类了。

这个类的实现也并不复杂,主要是其中解析部分的实现。这一块主要利用了 C++11 的正则库 <regex>从 xml 代码中匹配每一句 xml 代码(也就是一个标签对或者一个闭合标签),然后利用同样的方法去匹配每一句 xml 代码中的属性等式,获取其中的属性信息。

整个代码逻辑如下:
XmlParser: 简易的 Xml 解析器的实现(一)

这里我声明了三个解析函数,分别对应了整个 xml 文本、一句 xml 代码、一个元素属性的解析:

// 解析:解析文本,将 xml 文本解析成一个一个项目
bool _ParseSource(std::string strXml);

// 解析:解析一个项目
bool _ParseOneItem(std::string strOneItemXml, XmlParser::CItem *pItem);

// 解析:解析一个属性
bool _ParseOneAttribute(std::string strOneAttribute, XmlParser::CAttribute *pAttribute);

这三个函数的具体实现涉及到了正则表达式的用法,涉及到了 C++ 的 <regex> 库的用法,这里简单的以匹配每一个元素的 _ParseSource() 函数进行介绍:

// 解析:解析文本,将 xml 文本解析成一个一个项目
// 匹配 <name sex="male">hello world</name> 文本
// <\w+.*>.*</\1>
// 匹配 <name sex="male" /> 文本
// <\w+[^/>]*/>
// 这里按照以上两种格式匹配 xml 文本中的每一个 Item
// 这个函数将 xml 文本解析成一个一个的项目
bool CXmlParser::_ParseSource(std::string strXml)
{
    std::string s = strXml;
    std::smatch m;
    std::regex e("<(\\w+).*>.*</\\1>|<\\w+[^/>]*/>");
    while (std::regex_search(s, m, e)) {
        XmlParser::CItem newItem;
        _ParseOneItem(m.str(), &newItem);
        s = m.suffix().str();
        m_cDocument.items.push_back(newItem);
    }
    return true;
}

这段代码里面主要使用了两个知识点,一个是正则表达式,另一个是使用了 C++ 的<regex> 库中的 std::smatchstd::regexstd::regex_search 这三个函数来遍历多个匹配项。

还是简单讲解下吧:

  1. 正则表达式:\w表示字母或数字或下划线或汉字等,.表示非换行符的一切字符,紧接着的 *表示紧接着的前一个字符出现任意次数。这里 <\w+.*> 正好匹配了标签开头,然后标签对中间使用了 .*去匹配,这里为了匹配前后两个相同的单词,使用了后向引用也就是将前面那个单词括号括起来,使用它的默认组名后面进行匹配,</\1> 其中的 \1指代的就是前面打了括号的 (\w+)单词。有些同学可能还会发现,|,这个是什么意思呢?这个是在前面一个正则表达式匹配失败后调用的正则表达式,目的是匹配单标签元素。另外,还需要注意 C++ 字符串有些特殊字符需要多打一个斜线进行转义。

  2. C++正则库:C++ 正则库的使用确实需要一点时间去学习。这里 std::regex 定义了一个正则表达式对象,std::smatch 存储了匹配现场的数据,包括 std::smatch::str() 可以输出当前匹配字符串,std::smatch::suffix()::str() 可以输出从当前匹配字符串结尾到输入字符串的结尾的字符串,std::smatch::prefix()::str() 可以输出从输入字符串的开头到当前匹配字符串的开头的前一个字符结尾的字符串。另外,std::regex_search 是一个查找匹配函数。我们可以利用 std::regex_search 进行匹配查找,通过 std::smatch::suffix()::str() 来进行循环截断匹配查找(具体理解见代码,或者自行点击下列资料)。

这里讲解的比较粗略,想要详细了解正则表达式的同学可以点击这里:
正则表达式30分钟入门教程

想要详细了解 C++ 正则库的同学可以点击这里:
std::match_results::suffix
std::regex_search(重点看示例代码)

四、运行代码:沉醉于 Coding 不能自拔

最后,让我们运行下代码看看结果:

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

我们可以看到,输入了 xml 文本,我们解析后得到了所有的元素和属性数据。

完结,撒花!

五、总结

这是一个非常有趣的尝试,从一开始的架构设计,到存储结构的选择,再到最后的解析模块的难点的突破,对于一个程序员来说,都是一次不错的体验。

当然了,这份工程还是有很多问题的,比如不支持 xml 的标签嵌套(这貌似是一个非常严重的问题 T_T),不支持复杂的输入合法性检测,UI 不够好看(使用一个界面库可以解决)等等问题。这些有待我们后期继续研究。

不论怎么说,学会自己设计、实现、继续开发一个东西,总是有着很强的成就感的。

最后啰嗦一句,想要获取本项目代码的同学,可以点击这里下载:

wangying2016/WangYingXmlParser

To be Stronger!