C++实现xml解析器示例详解

xml格式简单介绍



    
        
    

我们来简单观察下上面的xml文件,xml格式和html格式十分类似,一般用于存储需要属性的配置或者需要多个嵌套关系的配置。

xml一般使用于项目的配置文件,相比于其他的ini格式或者yaml格式,它的优势在于可以将一个标签拥有多个属性,比如上述xml文件格式是用于配置工作流的,其中有name属性和switch属性,且再work标签中又嵌套了plugin标签,相比较其他配置文件格式是要灵活很多的。

具体的应用场景有很多,比如使用过Java中Mybatis的同学应该清楚,Mybatis的配置文件就是xml格式,而且也可以通过xml格式进行sql语句的编写,同样Java的maven项目的配置文件也是采用的xml文件进行配置。

而我为什么要写一个xml解析器呢?很明显,我今后要写的C++项目需要用到。

xml格式解析过程浅析

同样回到之前的那段代码,实际上已经把xml文件格式的不同情况都列出来了。

从整体上看,所有的xml标签分为:

  • xml声明(包含版本、编码等信息)
  • 注释
  • xml元素:1.单标签元素。 2.成对标签元素。

其中xml声明和注释都是非必须的。 而xml元素,至少需要一个成对标签元素,而且在最外层有且只能有一个,它作为根元素。

从xml元素来看,分为:

  • 名称
  • 属性
  • 内容
  • 子节点

根据之前的例子,很明显,名称是必须要有的而且是唯一的,其他内容则是可选。 根据元素的结束形式,我们把他们分为单标签和双标签元素。

代码实现

完整代码仓库:xml-parser

实现存储解析数据的类——Element

代码如下:

namespace xml
{
    using std::vector;
    using std::map;
    using std::string_view;
    using std::string;
    class Element
    {
    public:
        using children_t = vector;
        using attrs_t = map;
        using iterator = vector::iterator;
        using const_iterator = vector::const_iterator;
        string &Name()
        {
            return m_name;
        }
        string &Text()
        {
            return m_text;
        }
        //迭代器方便遍历子节点
        iterator begin()
        {
            return m_children.begin();
        }
        [[nodiscard]] const_iterator begin() const
        {
            return m_children.begin();
        }
        iterator end()
        {
            return m_children.end();
        }
        [[nodiscard]] const_iterator end() const
        {
            return m_children.end();
        }
        void push_back(Element const &element)//方便子节点的存入
        {
            m_children.push_back(element);
        }
        string &operator[](string const &key) //方便key-value的存取
        {
            return m_attrs[key];
        }
        string to_string()
        {
            return _to_string();
        }
    private:
        string _to_string();
    private:
        string m_name;
        string m_text;
        children_t m_children;
        attrs_t m_attrs;
    };
}

上述代码,我们主要看成员变量。

  • 我们用string类型表示元素的name和text
  • 用vector嵌套表示孩子节点
  • 用map表示key-value对的属性

其余的方法要么是Getter/Setter,要么是方便操作孩子节点和属性。 当然还有一个to_string()方法这个待会讲。

关键代码1——实现整体的解析

关于整体结构我们分解为下面的情形:

C++实现xml解析器示例详解_第1张图片

代码如下:

Element xml::Parser::Parse()
{
    while (true)
    {
        char t = _get_next_token();
        if (t != '<')
        {
            THROW_ERROR("invalid format", m_str.substr(m_idx, detail_len));
        }
        //解析版本号
        if (m_idx + 4 < m_str.size() && m_str.compare(m_idx, 5, " 
 

上述代码我们用while循环进行嵌套的原因在于注释可能有多个。

关键代码2——解析所有元素

C++实现xml解析器示例详解_第2张图片

对应代码:

Element xml::Parser::_parse_element()
{
    Element element;
    auto pre_pos = ++m_idx; //过掉<
    //判断name首字符合法性
    if (!(m_idx < m_str.size() && (std::isalpha(m_str[m_idx]) || m_str[m_idx] == '_')))
    {
        THROW_ERROR("error occur in parse name", m_str.substr(m_idx, detail_len));
    }
    //解析name
    while (m_idx < m_str.size() && (isalpha(m_str[m_idx]) || m_str[m_idx] == ':' ||
                                    m_str[m_idx] == '-' || m_str[m_idx] == '_' || m_str[m_idx] == '.'))
    {
        m_idx++;
    }
    if (m_idx >= m_str.size())
        THROW_ERROR("error occur in parse name", m_str.substr(m_idx, detail_len));
    element.Name() = m_str.substr(pre_pos, m_idx - pre_pos);
    //正式解析内部
    while (m_idx < m_str.size())
    {
        char token = _get_next_token();
        if (token == '/') //1.单元素,直接解析后结束
        {
            if (m_str[m_idx + 1] == '>')
            {
                m_idx += 2;
                return element;
            } else
            {
                THROW_ERROR("parse single_element failed", m_str.substr(m_idx, detail_len));
            }
        }
        if (token == '<')//2.对应三种情况:结束符、注释、下个子节点
        {
            //结束符
            if (m_str[m_idx + 1] == '/')
            {
                if (m_str.compare(m_idx + 2, element.Name().size(), element.Name()) != 0)
                {
                    THROW_ERROR("parse end tag error", m_str.substr(m_idx, detail_len));
                }
                m_idx += 2 + element.Name().size();
                char x = _get_next_token();
                if (x != '>')
                {
                    THROW_ERROR("parse end tag error", m_str.substr(m_idx, detail_len));
                }
                m_idx++; //千万注意把 '>' 过掉,防止下次解析被识别为初始的tag结束,实际上这个element已经解析完成
                return element;
            }
            //是注释的情况
            if (m_idx + 3 < m_str.size() && m_str.compare(m_idx, 4, "
                    

你可能感兴趣的:(C++实现xml解析器示例详解)