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

ARMFLYING 2012-01-25

摘要 本文介绍一种文本解析的方法:状态切换法 (状态机), 并给出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文档中不同的项.

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

(图1)

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

相关推荐