用狀態機實現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語法分析機(優化)

(圖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文檔只是這個方法的一次應用.如果你能定義其他的規則,並畫出流程,完全可以用這個方法來解析.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章