用状态机实现XML解析器 - C++环境

另一个经典xml解析器tinyxml

用状态机实现XML解析器 - C++环境

作者: 阙荣文 (querw)

 

摘要 本文介绍一种文本解析的方法:状态切换法 (状态机), 并给出C/C++下的实现.

-------------------------------------------------------------------------------------------------------------------------

 

    这是我3年前写的代码,用C++实现一个XML解析器.现在再翻出来看,觉得还是有些可取之处,尤其是实现XML文本解析时采用的状态切换法(姑且先这么叫吧,后文有详细解释这个方法的实现)不仅仅可以用来解析XML,几乎所有的文本流都可以用这种方法来解析 (我记得以前上编译原理时,讲到过词法分析器,用状态机,方法类似, 看来上课还是要认真听讲,不定什么时候就用上了.) 同时也有一些不足,主要是当时对UNICODE编程还懵懵懂懂,导致接口全是多字节的.所以要把我的代码加到UNICODE环境下还要做一些修改. 还有很重要的一点要事先说明:我对XML标准并没有做太多研究,写这些代码以实用为主,为的就是让我的程序有一个很简单快捷的方式读取,修改,保存XML文件,所以可能有相当一部分的XML特性没有实现,如果只是使你的C++程序可以使用XML文件作为你的配置文件,(INI文件过于简单了)那么我这个XML解析器还是很方便的.

 

XML文档的基本概念


字符存储要面对编码问题,我们在中文环境下,最常碰到的就3种编码方式: GB2312, UTF8 和Unicode. 根据XML标准,XML文件应该在第一行标明编码方式: <?xml version="1.0" encoding="gb2312" ?>. 我的做法是:不管它存储为什么编码方式,读到内存后,统统给它转化为宽字符(UNICDOE).现在就可以把XML文件看作一个宽字符流,这点很重要,是我实现解析器的前提.

XML文档是一个结构化的文档,一个XML文档对应一棵树.XML树由节点构成,XML里有以下几种节点:

enum xmlnode_type
{
    et_none = 0,
    et_xml,            // <?xml ...?>
    et_comment,        // <!-- ... -->
    et_normal,        // <tag />
    et_text,        // content text
    et_cdata,        // <![CDATA[ ... ]]>
};

我们以这样一个XML文件作为范例,以方便后面的解说:

<?xml version="1.0" encoding="gb2312" ?>
<company name="Que's C++ studio">
    <sales>
        <salesman age="28" level="1">小王</salesman>
    </sales>
    <develop>
        <programmer>小张</programmer>
    </develop>
</company>

一个很重要的概念是: 一棵XML树往往只有2个节点 1. XML节点,就是文件的第一行 <?xml ...?> 2. XML根节点<company>,<sales>和<develop>只是<company>的子节点.而文件的第一行,我们也把它看成一个节点. 这样理解的话,只要我们能解析一个节点,我们就可以解析整棵树.


状态分析法


所谓状态分析法,就是指一个解析函数,它可以根据不同的状态,运行不同的代码.对于解析xml文档,我设计了如下状态:

enum xmlnode_state    // 分析状态
{
    st_begin,        // 开始

    st_tagstart,    // /*tag开始 - "<"后的第一个字符*/
    st_tagend,        // tag结束 - />,>,?> 前的第一个字符

    st_attrnamestart,    // 属性名开始 - tag后的第一个非空格字符
    st_attrnameend,        // 属性名结束 - =, ,前的第一个字符

    st_attrvaluestart,    // 属性值开始 - ',",后的第一个字符
    st_attrvalueend,    // 属性值结束 - ',",前的第一个字符

    st_child,            // 开始分析子节点

    st_contentstart,    // 内容开始 - >后的第一个字符
    st_contentend,        // 内容结束 - <前的第一个字符

    st_commentstart,    // 注释开始 <!--后的第一个字符
    st_commentend,        // 注释结束    -->前的第一个字符

    st_endtagstart,        // 结束TAG 开始 </,<?后的第一个字符
    st_endtagend,        // 结束TAG 结束 >前的第一个字符

    st_cdatastart,
    st_cdataend,

    st_end,        // 分析结束
};

假设pCur指向XML文档的输入流的当前位置, 现在来模拟一下解析过程: 在初始状态st_begin下,一直移动pCur,直到pCur[0] = '<',意味着节点开始了. 此时根据后面字符切换状态: 如果后面连续3个字符时 "!--" 那么说明这是一个注释节点,形如"<!-- ... -->",把状态切换为st_commentstart并继续运行相应代码; 如果后面一个字符是'?', 那么说明它是这种节点的开始 "<? ...",应该把状态切换为st_tagstart; 如果后面的字符是"![CDATA[",则说明这是一个CDATA节点的开始 "<![CDATA[ ... ]]>" (图1中没有标明CDATA节点的情况,因为作图的时候没有考虑到CDATA节点);如果是其他字符,则说明开始读取节点名, "<company> ..."

根据图1所示,其他的代码都类似:从输入流不断读出字符,根据当前状态,把读到的内容解析为XML文档中不同的项.

 

(CSDN博客上传不了图片.. 上传到我的相册去了.)

用状态机实现XML解析器 - C++环境_第1张图片

(图1)

 

特别说明: 我把节点的内容理解为当前节点的一个子节点,比如 "<company>这里是节点内容</company>"这一段XML文本会被解析为一个父节点"company"和一个子节点"这里是节点内容". 这样做是有好处的,看这个例子"<company>这里是节点内容<any></any>另一段节点内容</company>"如果只是把节点内容作为节点的一个属性,在碰到刚刚这种情况时就束手无策了.

 

关键代码分析


用C/C++的switch语句,很容易实现状态分析法:每一种状态对应一段case代码.

BOOL XMLNode::LoadNode(const wchar_t* pszContent, const wchar_t* &pszEnd)
{
    ResetNode();
    const wchar_t* pCur = pszContent;
    const wchar_t* pBegin = NULL;
    const wchar_t* pEnd = NULL;
    xmlnode_state st = st_begin;

    wstr2wstr s2s;
    wchar_t chValueFlag;    // ' 或者 " 应该成对出现

    bool bStop = false;
    try
    {
        while(*pCur != 0 && !bStop)
        {
            switch(st)
            {
            case st_begin:
                {
                    if(pCur[0] == L'<')
                    {
                        _trace("########################################", NULL);
                        _trace("开始分析节点", pCur);

                        // 判断节点类型
                        if(pCur[1] == L'?')
                        {
                            // (1) "<?" 开头的是XML节点
                            pCur++;
                            m_type = et_xml;
                            st = st_tagstart;
                        }
                        else if(pCur[1]== L'!' && pCur[2] == L'-' && pCur[3] == L'-')
                        {
                            // (2) "<!--" 开头的是注释节点
                            pCur += 3;
                            m_type = et_comment;
                            st = st_commentstart;
                        }
                        else if(wcsncmp(pCur, L"<![CDATA[", 9) == 0)
                        {
                            // (2) "<![CDATA[" 开头 "]]>"结尾的是CDATA部件
                            pCur += 8;
                            m_type = et_cdata;
                            st = st_cdatastart;
                        }
                        else
                        {
                            st = st_tagstart;
                            m_type = et_normal;
                        }   
                    }
                    else
                    {
                        // 忽略所有'<'之前的字符
                        if(pCur[0] == L' '
                            || pCur[0] == L'/r'
                            || pCur[0] == L'/n'
                            || pCur[0] == L'/t')
                        {
                        }
                        else
                        {
                            goto error;
                        }
                    }
                }
                break;
            case st_tagstart:
                {
                    pBegin = pCur;
                    pEnd = NULL;
                    st = st_tagend;
                    pCur--;
                }
                break;
            case st_tagend:
                {
                    if(pCur[0] == L' ' ||
                        pCur[0] == L'>' ||
                        pCur[0] == L'/' && pCur[1] == L'>' && m_type == et_normal ||
                        pCur[0] == L'?' && pCur[1] == L'>' && m_type == et_xml
                        )
                    {
                        pEnd = pCur - 1;
                        st = st_attrnamestart;
                        pCur--;
                    }
                    else
                    {
                        // 非法tag名字符在此判断
                        if(pCur[0] == L'<' || pCur[0] == L'/')
                        {
                            _trace("tag名中出现了非法的字符", pCur);
                            goto error;
                        }
                    }

                    // 得到节点名称
                    if(pEnd != NULL)
                    {
                        if(getStr(pBegin, pEnd, m_strName))
                        {
                            pBegin = NULL;
                            pEnd = NULL;
                            _trace("tag Name", m_strName.c_str());
                        }
                        else
                        {
                            _trace("非法的tag", pBegin);
                            pCur = pBegin;
                            goto error;
                        }
                    }
                }
                break;
            case st_attrnamestart:
                {
                    if(L' ' == pCur[0])
                    {
                        // 跳过属性名前的空格
                    }
                    else
                    {
                        pBegin = pCur;
                        pEnd = NULL;
                        st = st_attrnameend;
                        pCur--;
                    }
                }
                break;
            case st_attrnameend:
                {
                    if(L'>' == pCur[0])
                    {
                        st = st_contentstart;
                    }
                    else if(L'/' == pCur[0] && L'>' == pCur[1] && m_type == et_normal ||
                        L'?' == pCur[0] && L'>' == pCur[1] && m_type == et_xml)
                    {
                        st = st_end;
                        pCur++;
                    }
                    else if(L'=' == pCur[0] || L' ' == pCur[0])
                    {
                        st = st_attrvaluestart;
                        pEnd = pCur - 1;
                    }
                    else
                    {
                    }
                    if(pEnd)
                    {
                        s2s.first = L"";
                        s2s.second = L"";
                        if(getStr(pBegin, pEnd, s2s.first))
                        {
                            _trace("属性名", s2s.first.c_str());
                        }
                        else
                        {
                            _trace("非法的属性名", pCur);
                            pCur = pBegin;
                            goto error;
                        }
                    }
                }
                break;
            case st_attrvaluestart:
                {
                    if(L'/'' == pCur[0] || L'/"' == pCur[0])
                    {
                        pBegin = pCur + 1;
                        pEnd = NULL;
                        st = st_attrvalueend;
                        chValueFlag = pCur[0];    // 记录'/"要成对出现
                    }
                    else if(L' ' == pCur[0])
                    {
                        // 属性名=后的空格过虑掉
                    }
                    else
                    {
                        _trace("属性名后有非法的字符", pCur);
                        goto error;
                    }
                }
                break;
            case st_attrvalueend:
                {
                    if((L'/'' == pCur[0] || L'/"' == pCur[0]) && pCur[0] == chValueFlag)
                    {
                        pEnd = pCur - 1;
                        getStr(pBegin, pEnd, s2s.second);
                        _trace("属性值", s2s.second.c_str());
                        m_AttrList.push_back(s2s);

                        if(
                            L' ' == pCur[1] ||
                            L'/' == pCur[1] && L'>' == pCur[2] && m_type == et_normal ||
                            L'?' == pCur[1] && L'>' == pCur[2] && m_type == et_xml ||
                            L'>' == pCur[1]
                            )
                        {
                            // 分析下一个属性
                            st = st_attrnamestart;
                        }
                        else
                        {
                            _trace("属性值/"//'之后发现非法字符", pCur);
                            goto error;
                        }
                    }
                    else
                    {
                        // 非法的属性值字符在此判断
                        // ..
                        // ..
                    }
                }
                break;
            case st_contentstart:
                {
                    // 不过虑空格
                    pBegin = pCur;
                    pEnd = NULL;
                    st = st_contentend;
                    pCur--;
                }
                break;
            case st_contentend:
                {
                    if(L'<' == pCur[0])
                    {
                        wstring strText;
                        pEnd = pCur - 1;
                        if(getStr(pBegin, pEnd, strText))
                        {
                            // 普通文本也作为一个子节点
                            _trace("content", strText.c_str());
                            if(isValidText(strText.c_str()))
                            {
                                XMLNode *pNode = new XMLNode;
                                pNode->m_type = et_text;
                                pNode->m_strText = strText;
                                linkChild(pNode);
                            }
                            else
                            {
                                _trace("无效内容文本", strText.c_str());
                            }
                        }
                        else
                        {
                            _trace("空内容", pBegin);
                        }

                        // 内容结束了,判断下一步操作
                        if(L'/' == pCur[1] && m_type == et_normal ||
                            L'?' ==pCur[1] && m_type == et_xml)
                        {                       
                            st = st_endtagstart;
                            pCur++;
                        }
                        else
                        {
                            st = st_child;
                            pCur--; // 子节点从"<"开始,所以回退1格
                        }
                        pBegin = NULL;
                        pEnd = NULL;
                    }
                    else
                    {
                        // 非法的内容字符在此判断
                        // ..
                        // ..
                    }
                }
                break;
            case st_cdatastart:
                {
                    pBegin = pCur;
                    pEnd = NULL;
                    st = st_cdataend;
                    pCur--;
                }
                break;
            case st_cdataend:
                {
                    if(wcsncmp(pCur, L"]]>", 3) == 0)
                    {
                        pEnd = pCur - 1;
                        getStr(pBegin, pEnd, m_strText); // CDATA文本也作为一个子节点
                        _trace("cdata content", m_strText.c_str());
                        // cdata结束了,判断下一步操作
                        pCur += 2;
                        st = st_end;
                    }
                    else
                    {
                        // 非法的内容字符在此判断
                        // ..
                        // ..
                    }
                }
                break;
            case st_commentstart:
                {
                    pBegin = pCur;
                    st = st_commentend;
                    pEnd = NULL;
                    pCur--;
                }
                break;
            case st_commentend:
                {
                    if(L'>' == pCur[0] && L'-' == *(pCur - 2) && L'-' == *(pCur - 1))
                    {
                        pEnd = pCur - 3;
                        getStr(pBegin, pEnd, m_strText);
                        _trace("comment content", m_strText.c_str());
                        st = st_end;
                    }
                    else
                    {
                        // 非法的注释字符在此判断
                        // ..
                        // ..
                    }
                }
                break;
            case st_endtagstart:
                {
                    pBegin = pCur;
                    pEnd = NULL;
                    st = st_endtagend;
                    pCur--;
                }
                break;
            case st_endtagend:
                {
                    if(L'>' == pCur[0])
                    {
                        pEnd = pCur - 1;
                        wstring strTag;
                        getStr(pBegin, pEnd, strTag);
                        _trace("endtagname", strTag.c_str());
                        if(strTag == m_strName)
                        {
                            st = st_end;
                        }
                        else
                        {
                            pCur = pBegin;
                            goto error;
                        }
                    }
                    else
                    {
                        //
                    }
                }
                break;
            case st_child:
                {
                    // 递归分析子节点
                    _trace("开始分析子节点", pCur);
                    XMLNode *pNode = new XMLNode;
                    if(pNode->LoadNode(pCur, pCur))
                    {
                        linkChild(pNode);
                        pCur--;
                        _trace("继续分析下一段内容(多一个字符)", pCur);
                        st = st_contentstart;   
                    }
                    else
                    {
                        delete pNode;
                        goto childerror;
                    }
                }
                break;
            case st_end:
                {
                    bStop = true;
                    pCur--;
                }
                break;
            default:
                {
                }
                break;
            }

            pCur++;
        }
    }
    catch (...)
    {
        _trace("捕捉到异常", NULL);
        goto error;
    }

    pszEnd = pCur;
    return st == st_end || st == st_begin;

error:
    _trace("发生错误, 原始内容", pszContent);
    _trace("错误位置", pCur);
childerror:
    pszEnd = pCur;
    return FALSE;
}


使用范例


1. 在内存中构建XML文档,并保存.

    XMLDocument xml;
    XMLHANDLE hXml = xml.CreateXml("1.0", "gb2312");
    hXml = xml.NewNode(NULL, "root");  // 创建根节点
    xml.SetAttrValue(hXml, "type", "company"); //添加一个根节点属性
    xml.SetAttrValueInt(hXml, "value", 1);

    XMLHANDLE hContent = xml.NewNode(hXml, NULL, et_text); // 为根节点创建一个子节点(内容)
    xml.SetText(hContent, "这是内容");

    hXml = xml.NewNode(hXml, "software"); // 创建子节点
    hXml = xml.NewNode(hXml, "软件部门");
    XMLHANDLE hChild = xml.NewNode(hXml, "person");
    xml.SetAttrValueInt(hChild, "id", 100020001);
    hChild = xml.NewNode(hXml, "person");
    xml.SetAttrValueInt(hChild, "id", 100020002);
    xml.SaveXml("C://my.xml"); // 保存文档

运行结果:

<?xml version="1.0" encoding="gb2312"?>
<root type="company" value="1">
    这是内容
    <software>
        <软件部门>
            <person id="100020001"/>
            <person id="100020002"/>
        </软件部门>
    </software>
    </root>

说明: XMLHANDLE 是我定义的一个用来标识XML节点的"句柄",任何时候,只要传入句柄,接口就可以操作这个节点.

 

2. 读取XML文档.

XMLDocument xml1;
if(xml1.LoadXml(strFilePath))
{
    XMLHANDLE hRoot = xml1.GetRootNode(); //获取根节点
    string strTypeName = xml1.GetAttrValue(hRoot, "type"); // 获取根节点的属性
    string strContent = xml1.GetContent(xml1.FirstChild(hRoot)); // 获取根节点的第一段内容
    XMLHANDLE hSoftware = xml1.GetChildByName(hRoot, "software");  // 通过节点名定位子节点
    XMLHANDLE hSoftware2 = xml1.GetChildByName(hSoftware, "软件部门");
    XMLHANDLE hPerson = xml1.GetChildByAttr(hSoftware2, "person", "id", "100020002"); //通过属性值定位子节点
    string strId = xml1.GetAttrValue(hPerson, "id");
}


说明:搜索定位接口 GetChildByName() 和 GetChildByAttr() 只搜索指定节点的一级子节点,并不搜索全文.如果要实现全文检索,也不难,用一个栈/队列就可以实现深度优先/广度优先搜索,这是树算法的基本功了.

 

后记

对于本文介绍的状态分析法,画出那张图是关键. 只要能把流程和各个分支想清楚,写代非常容易按部就班就可以写出来.由此也可以看出,用状态分析法来解析XML文档只是这个方法的一次应用.如果你能定义其他的规则,并画出流程,完全可以用这个方法来解析.



你可能感兴趣的:(用状态机实现XML解析器 - C++环境)