C++實(shí)現(xiàn)xml解析器示例詳解
xml格式簡(jiǎn)單介紹
<?xml version="1.0"?>
<!--這是注釋-->
<workflow>
<work name="1" switch="on">
<plugin name="echoplugin.so" switch="on" />
</work>
</workflow>
我們來(lái)簡(jiǎn)單觀察下上面的xml文件,xml格式和html格式十分類似,一般用于存儲(chǔ)需要屬性的配置或者需要多個(gè)嵌套關(guān)系的配置。
xml一般使用于項(xiàng)目的配置文件,相比于其他的ini格式或者yaml格式,它的優(yōu)勢(shì)在于可以將一個(gè)標(biāo)簽擁有多個(gè)屬性,比如上述xml文件格式是用于配置工作流的,其中有name屬性和switch屬性,且再work標(biāo)簽中又嵌套了plugin標(biāo)簽,相比較其他配置文件格式是要靈活很多的。
具體的應(yīng)用場(chǎng)景有很多,比如使用過(guò)Java中Mybatis的同學(xué)應(yīng)該清楚,Mybatis的配置文件就是xml格式,而且也可以通過(guò)xml格式進(jìn)行sql語(yǔ)句的編寫(xiě),同樣Java的maven項(xiàng)目的配置文件也是采用的xml文件進(jìn)行配置。
而我為什么要寫(xiě)一個(gè)xml解析器呢?很明顯,我今后要寫(xiě)的C++項(xiàng)目需要用到。
xml格式解析過(guò)程淺析
同樣回到之前的那段代碼,實(shí)際上已經(jīng)把xml文件格式的不同情況都列出來(lái)了。
從整體上看,所有的xml標(biāo)簽分為:
- xml聲明(包含版本、編碼等信息)
- 注釋
- xml元素:1.單標(biāo)簽元素。 2.成對(duì)標(biāo)簽元素。
其中xml聲明和注釋都是非必須的。 而xml元素,至少需要一個(gè)成對(duì)標(biāo)簽元素,而且在最外層有且只能有一個(gè),它作為根元素。
從xml元素來(lái)看,分為:
- 名稱
- 屬性
- 內(nèi)容
- 子節(jié)點(diǎn)
根據(jù)之前的例子,很明顯,名稱是必須要有的而且是唯一的,其他內(nèi)容則是可選。 根據(jù)元素的結(jié)束形式,我們把他們分為單標(biāo)簽和雙標(biāo)簽元素。
代碼實(shí)現(xiàn)
完整代碼倉(cāng)庫(kù):xml-parser
實(shí)現(xiàn)存儲(chǔ)解析數(shù)據(jù)的類——Element
代碼如下:
namespace xml
{
using std::vector;
using std::map;
using std::string_view;
using std::string;
class Element
{
public:
using children_t = vector<Element>;
using attrs_t = map<string, string>;
using iterator = vector<Element>::iterator;
using const_iterator = vector<Element>::const_iterator;
string &Name()
{
return m_name;
}
string &Text()
{
return m_text;
}
//迭代器方便遍歷子節(jié)點(diǎn)
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)//方便子節(jié)點(diǎn)的存入
{
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嵌套表示孩子節(jié)點(diǎn)
- 用map表示key-value對(duì)的屬性
其余的方法要么是Getter/Setter,要么是方便操作孩子節(jié)點(diǎn)和屬性。 當(dāng)然還有一個(gè)to_string()方法這個(gè)待會(huì)講。
關(guān)鍵代碼1——實(shí)現(xiàn)整體的解析
關(guān)于整體結(jié)構(gòu)我們分解為下面的情形:

代碼如下:
Element xml::Parser::Parse()
{
while (true)
{
char t = _get_next_token();
if (t != '<')
{
THROW_ERROR("invalid format", m_str.substr(m_idx, detail_len));
}
//解析版本號(hào)
if (m_idx + 4 < m_str.size() && m_str.compare(m_idx, 5, "<?xml") == 0)
{
if (!_parse_version())
{
THROW_ERROR("version parse error", m_str.substr(m_idx, detail_len));
}
continue;
}
//解析注釋
if (m_idx + 3 < m_str.size() && m_str.compare(m_idx, 4, "<!--") == 0)
{
if (!_parse_comment())
{
THROW_ERROR("comment parse error", m_str.substr(m_idx, detail_len));
}
continue;
}
//解析element
if (m_idx + 1 < m_str.size() && (isalpha(m_str[m_idx + 1]) || m_str[m_idx + 1] == '_'))
{
return _parse_element();
}
//出現(xiàn)未定義情況直接拋出異常
THROW_ERROR("error format", m_str.substr(m_idx, detail_len));
}
}
上述代碼我們用while循環(huán)進(jìn)行嵌套的原因在于注釋可能有多個(gè)。
關(guān)鍵代碼2——解析所有元素

對(duì)應(yīng)代碼:
Element xml::Parser::_parse_element()
{
Element element;
auto pre_pos = ++m_idx; //過(guò)掉<
//判斷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);
//正式解析內(nèi)部
while (m_idx < m_str.size())
{
char token = _get_next_token();
if (token == '/') //1.單元素,直接解析后結(jié)束
{
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.對(duì)應(yīng)三種情況:結(jié)束符、注釋、下個(gè)子節(jié)點(diǎn)
{
//結(jié)束符
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++; //千萬(wàn)注意把 '>' 過(guò)掉,防止下次解析被識(shí)別為初始的tag結(jié)束,實(shí)際上這個(gè)element已經(jīng)解析完成
return element;
}
//是注釋的情況
if (m_idx + 3 < m_str.size() && m_str.compare(m_idx, 4, "<!--") == 0)
{
if (!_parse_comment())
{
THROW_ERROR("parse comment error", m_str.substr(m_idx, detail_len));
}
continue;
}
//其余情況可能是注釋或子元素,直接調(diào)用parse進(jìn)行解析得到即可
element.push_back(Parse());
continue;
}
if (token == '>') //3.對(duì)應(yīng)兩種情況:該標(biāo)簽的text內(nèi)容,下個(gè)標(biāo)簽的開(kāi)始或者注釋(直接continue跳到到下次循環(huán)即可
{
m_idx++;
//判斷下個(gè)token是否為text,如果不是則continue
char x = _get_next_token();
if (x == '<')//不可能是結(jié)束符,因?yàn)閤ml元素不能為空body,如果直接出現(xiàn)這種情況也有可能是中間夾雜了注釋
{
continue;
}
//解析text再解析child
auto pos = m_str.find('<', m_idx);
if (pos == string::npos)
THROW_ERROR("parse text error", m_str.substr(m_idx, detail_len));
element.Text() = m_str.substr(m_idx, pos - m_idx);
m_idx = pos;
//注意:有可能直接碰上結(jié)束符,所以需要continue,讓element里的邏輯來(lái)進(jìn)行判斷
continue;
}
//4.其余情況都為屬性的解析
auto key = _parse_attr_key();
auto x = _get_next_token();
if (x != '=')
{
THROW_ERROR("parse attrs error", m_str.substr(m_idx, detail_len));
}
m_idx++;
auto value = _parse_attr_value();
element[key] = value;
}
THROW_ERROR("parse element error", m_str.substr(m_idx, detail_len));
}
開(kāi)發(fā)技巧
無(wú)論是C++開(kāi)發(fā),還是各種其他語(yǔ)言的造輪子,在這個(gè)造輪子的過(guò)程中,不可能是一帆風(fēng)順的,需要不斷的debug,然后再測(cè)試,然后再debug。。。實(shí)際上這類格式的解析,單純的進(jìn)行程序的調(diào)試效率是非常低下的!
特別是你用的語(yǔ)言還是C++,那么如果出現(xiàn)意外宕機(jī)行為,debug幾乎是不可能簡(jiǎn)單的找出原因的,所以為了方便調(diào)試,或者是意外宕機(jī)行為,我們還是多做一些錯(cuò)誤、異常處理的工作比較好。
比如上述的代碼中,我們大量的用到了 THROW_ERROR 這個(gè)宏,實(shí)際上這個(gè)宏輸出的內(nèi)容是有便于調(diào)試和快速定位的。 具體代碼如下:
//用于返回較為詳細(xì)的錯(cuò)誤信息,方便錯(cuò)誤追蹤
#define THROW_ERROR(error_info, error_detail) \
do{ \
string info = "parse error in "; \
string file_pos = __FILE__; \
file_pos.append(":"); \
file_pos.append(std::to_string(__LINE__));\
info += file_pos; \
info += ", "; \
info += (error_info); \
info += "\ndetail:"; \
info += (error_detail);\
throw std::logic_error(info); \
}while(false)
如果發(fā)生錯(cuò)誤,這個(gè)異常攜帶的信息如下:

打印出了兩個(gè)非常關(guān)鍵的信息:
內(nèi)部的C++代碼解析拋出異常的位置
解析發(fā)生錯(cuò)誤的字符串
按理來(lái)說(shuō)這些信息應(yīng)該是用日志來(lái)進(jìn)行記錄的,但是由于這個(gè)項(xiàng)目比較小型,直接用日常信息當(dāng)日志來(lái)方便調(diào)試也未嘗不可??
有關(guān)C++的優(yōu)化
眾所周知在C++中,一個(gè)類有八個(gè)默認(rèn)函數(shù):
- 默認(rèn)構(gòu)造函數(shù)
- 默認(rèn)拷貝構(gòu)造函數(shù)
- 默認(rèn)析構(gòu)函數(shù)
- 默認(rèn)重載賦值運(yùn)算符函數(shù)
- 默認(rèn)重載取址運(yùn)算符函數(shù)
- 默認(rèn)重載取址運(yùn)算符const函數(shù)
- 默認(rèn)移動(dòng)構(gòu)造函數(shù)(C++11)
- 默認(rèn)重載移動(dòng)賦值操作符函數(shù)(C++11)
我們一般情況需要注意的構(gòu)造函數(shù)和賦值函數(shù)函數(shù)需要的是以下三類:
- 拷貝構(gòu)造。
- 移動(dòng)構(gòu)造。
- 析構(gòu)函數(shù)。
以下面的代碼為例來(lái)說(shuō)明默認(rèn)的行為:
class Data{
...
}
class test{
pvivate:
Data m_data;
}
額外注意
默認(rèn)情況的模擬
class Data{
...
}
class test{
public:
//拷貝構(gòu)造
test(test const&src) = default;//等價(jià)于下面的代碼
//test(test const& src):m_data(src.m_data){}
//移動(dòng)構(gòu)造
test(test &&src) = default;//等價(jià)于下面代碼
//tset(test&& src):m_data(std::move(src.m_data)){}
pvivate:
Data m_data;
}
從上述情況可以看出,如果一個(gè)類的數(shù)據(jù)成員中含有原始指針數(shù)據(jù),那么拷貝構(gòu)造和移動(dòng)構(gòu)造都需要自定義,如果成員中全都用的標(biāo)準(zhǔn)庫(kù)里的東西,那么我們就用默認(rèn)的就行,因?yàn)闃?biāo)準(zhǔn)庫(kù)的所有成員都自己實(shí)現(xiàn)了拷貝和移動(dòng)構(gòu)造!比如我們目前的Element就全都用默認(rèn)的就好。
需要特別注意的點(diǎn)
- 顯式定義了某個(gè)構(gòu)造函數(shù)或者賦值函數(shù),那么相應(yīng)的另一個(gè)構(gòu)造或者賦值就會(huì)被刪除默認(rèn),需要再次顯式定義了。 舉個(gè)例子:比如我顯式定義了移動(dòng)構(gòu)造(關(guān)于顯式定義,手動(dòng)創(chuàng)建算顯式,手動(dòng)寫(xiě)default也算顯示),那么就會(huì)造成所有的默認(rèn)拷貝(拷貝構(gòu)造和拷貝賦值)被刪除。相反顯式定義了移動(dòng)賦值也是類似的,默認(rèn)的拷貝行為被刪除??截惖膶?duì)于顯式的默認(rèn)行為處理也是一模一樣。
- 如果想要使用默認(rèn)的構(gòu)造/賦值函數(shù),那么對(duì)應(yīng)的成員也都必須支持。 例如以下代碼:
class Data{
...
}
class test{
pvivate:
Data m_data;
}
由于test類沒(méi)有寫(xiě)任何構(gòu)造函數(shù),那么這8個(gè)默認(rèn)構(gòu)造函數(shù)按理來(lái)說(shuō)都是有的,但如果Data類中的拷貝構(gòu)造由于某些顯式定義情況而被刪除了,那么test類就不再支持拷貝構(gòu)造(對(duì)我們?cè)斐傻挠绊懢褪牵簺](méi)法再直接通過(guò)等號(hào)初始化)。
最后,通過(guò)上述規(guī)則我們發(fā)現(xiàn),如果想要通過(guò)默認(rèn)的構(gòu)造函數(shù)偷懶,那么首先你的成員得支持對(duì)應(yīng)的構(gòu)造函數(shù),還有就是不要畫(huà)蛇添足:比如本來(lái)我什么都不用寫(xiě),它自動(dòng)生成8大默認(rèn)函數(shù),然后你手寫(xiě)了一個(gè)拷貝構(gòu)造=default,好了,你的默認(rèn)函數(shù)從此少了兩個(gè),又得你一個(gè)個(gè)手動(dòng)去補(bǔ)了!
故如果成員變量對(duì)移動(dòng)和拷貝行為都是支持的,那么你就千萬(wàn)不要再畫(huà)蛇添足了,除非你需要自定義某些特殊行為(比如打日志什么的)。如果你的成員變量中含有原始指針,那么一定需要手動(dòng)寫(xiě)好移動(dòng)和拷貝行為。如果成員變量對(duì)拷貝和移動(dòng)行為部分支持,那么根據(jù)你的使用情況來(lái)進(jìn)行選擇是否需要手動(dòng)補(bǔ)充這些行為!
以上就是C++實(shí)現(xiàn)xml解析器示例詳解的詳細(xì)內(nèi)容,更多關(guān)于C++ xml解析器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解C++編程中標(biāo)記語(yǔ)句與復(fù)合語(yǔ)句的寫(xiě)法
這篇文章主要介紹了C++編程中標(biāo)記語(yǔ)句與復(fù)合語(yǔ)句的寫(xiě)法,是C++入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2016-01-01
C語(yǔ)言素?cái)?shù)(質(zhì)數(shù))判斷的3種方法舉例
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言素?cái)?shù)(質(zhì)數(shù))判斷的3種方法,質(zhì)數(shù)是只能被1或者自身整除的自然數(shù)(不包括1),稱為質(zhì)數(shù),文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11
atoi和itoa函數(shù)的實(shí)現(xiàn)方法
本文介紹了,atoi和itoa函數(shù)的實(shí)現(xiàn)方法,需要的朋友可以參考一下2013-03-03
為什么要學(xué)習(xí)C語(yǔ)言 C語(yǔ)言優(yōu)勢(shì)分析
不止一個(gè)學(xué)生問(wèn)到我:“老師,為什么我們的應(yīng)用程序設(shè)計(jì)要學(xué)C語(yǔ)言而不是別的?C語(yǔ)言不是已經(jīng)過(guò)時(shí)了嗎?如果現(xiàn)在要寫(xiě)一個(gè)Windows程序,用VB或Dephi開(kāi)發(fā)多快呀,用C行嗎?退一萬(wàn)步,為什么選擇C而不是C++呢?”2013-07-07
OpenCV相機(jī)標(biāo)定的全過(guò)程記錄
這篇文章主要給大家介紹了關(guān)于OpenCV相機(jī)標(biāo)定的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03
C語(yǔ)言?模擬實(shí)現(xiàn)strlen函數(shù)詳解
在 C 語(yǔ)言 中我們要獲取 字符串 的長(zhǎng)度,可以使用strlen 函數(shù),strlen 函數(shù)計(jì)算字符串的長(zhǎng)度時(shí),直到空結(jié)束字符,但不包括空結(jié)束字符,因?yàn)?nbsp;strlen 函數(shù)時(shí)不包含最后的結(jié)束字符的,因此一般使用 strlen函數(shù)計(jì)算的字符串的長(zhǎng)度會(huì)比使用 sizeof 計(jì)算的字符串的字節(jié)數(shù)要小2022-04-04

