tinyXML是我接触的第一个源码,花了几天的时间把整个源码研究了一遍,虽然有些细节的实现还不是很懂,但整个程序的框架和各部分之间的关系已经有了些了解,把学习的成果写下来,也算整理一下思路,可能有不对的地方,希望各位看官批评指正。
对XML文件的介绍请转向: https://www.ibm.com/developerworks/cn/xml/x-newxml/#list1
使用tinyXML解析XML文件的实例,请看 https://blog.csdn.net/u012796139/article/details/46706853
各个文件之间的组织关系:
tinyxml.h头文件写了几乎所有类的声明,与之对应的类的实现分别在tinyxml.cpp,tinyxmlerror,tinyxmlparser中。
tinystr.h头文件中声明了tinyxml自己定义的字符串类(程序没有使用STL的string类),与之对应的类的定义在tinystr.cpp中。
xmltest.cpp是测试程序,包含了tinyxml.h和tinystr.h两个头文件。(运行前还需要把类的实现,即三个cpp文件添加到工程中)
tinyXML定义了TiXmlBase一个虚基类,是其他所有类的基类,各个类之间的关系:tinyXML采用文档对象模型(DOM)来解析XML文件,即把整个文档解析为一个多个对象组成的树形结构,方便用户修改对象的内容。
拿下面这个官方文档里给的例子来解释
<?xml version="1.0" standalone=no>
<!-- Our to do list data -->
<ToDo>
<Item priority="1"> Go to the <bold>Toy store!</bold></Item>
<Item priority="2"> Do bills</Item>
</ToDo>
TiXmlDocument:指向整个文档
TiXmlDeclaration:文件的声明部分,对应 <?xml version=“1.0” standalone=no>
TiXmlComment:文件的注释部分,对应 < !-- Our to do list data -->
TiXmlElement:xml文件的元素,对应 < ToDo > < /ToDo >, < Item priority=“1”>< /Item>, < bold> < /bold>等
TiXmlText:文本内容,对应“Go to the”,“Do bills”等。
其他部分都是TiXmlUnknown。
tinyXML把文档解析为这样的树:
TiXmlDocument "demo.xml"
TiXmlDeclaration "version='1.0'" "standalone=no"
TiXmlComment " Our to do list data"
TiXmlElement "ToDo"
TiXmlElement "Item" Attribtutes: priority = 1
TiXmlText "Go to the "
TiXmlElement "bold"
TiXmlText "Toy store!"
TiXmlElement "Item" Attributes: priority=2
TiXmlText "Do bills"
在介绍图中的各个类之前,首先介绍几个工具 类/结构体
- struct TiXmlCursor 类似指针作用的类,指示解析到了文档的哪一行哪一列
- class TiXmlParsingData 包含要解析数据的所有信息----在文档中的位置,要解析的char型数组等信息。
- class TiXmlAttributeSet, 用于在多个TiXmlAttribute对象间构建循环链表,主要用于TiXmlElement 的属性中,因为TiXmlElement 属性的类型和数目都不确定,所以需要统一组织。但是TiXmlDeclaration由于属性只有version,encoding,standalone三个,而且顺序已经定死,所以不需要再用TiXmlAttributeSet管理。
TiXmlBase类:
派生类直接继承的成员:
- 静态成员–Entity 结构体数组,用来定义5个实体引用
- 静态成员–bool型变量condenseWhiteSpace,用来决定是否要压缩空格
- 静态成员函数-- ConvertUTF32ToUTF8( unsigned long input, char* output, int* length ),用来把UTF32转换为UTF8
- 静态成员函数–static int IsAlpha( unsigned char anyByte, TiXmlEncoding encoding );输入的字符是否为字母。
- 静态成员函数–static int IsAlphaNum( unsigned char anyByte, TiXmlEncoding encoding );输入的字符是否为数字或字母
- 静态内联函数–inline static int ToLower( int v, TiXmlEncoding encoding )把大写字母转化为小写
- 静态字符串数组static const char* errorString[TIXML_ERROR_STRING_COUNT ];定义了发生错误时输出的表示错误类型的字符串
- TiXmlCursor 型对象–TiXmlCursor location ,用来保存文档解析的位置
TiXmlAttribute类:
定义了属性的名字和值,以及指向前方和后方属性的指针,用于在TiXmlElement中组建TiXmlAttributeSet对象。
TIXML_STRING name;
TIXML_STRING value;
TiXmlAttribute* prev;
TiXmlAttribute* next;
const char* TiXmlAttribute::Parse( const char* p, TiXmlParsingData* data, TiXmlEncoding encoding )
最重要的,是TiXmlAttribute的parse函数
它首先调用TiXmlParsingData::stamp()函数标记开始解析处在文档中的位置
再调用readname,把属性的名字写入TIXML_STRING name中
最后调用ReadText函数,把属性的值写入value中
TiXmlDeclaration类:
定义了三个 TIXML_STRING对象来保存对应的属性。
在TiXmlDeclaration的parse函数中按顺序分别判断属性是否为version,encoding,standalone,判断一致后创建三次TiXmlAttribute对象来解析,解析的结果存储在三个TIXML_STRING中。因为有自己定义的三个TIXML_STRING,所以就没有用从TiXmlNode继承来的TIXML_STRING value。
TiXmlNode类:
定义了所有节点类都需要的数据成员:
指向父节点,子节点和兄弟节点的指针TiXmlNode* parent,firstChild,lastChild,prev,next;
保存每个节点内容的TIXML_STRING value;
TiXmlDocument类:
独有的成员:int tabsize;
独有的函数:
bool LoadFile( const char * filename, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING ).
LoadFile函数做了什么?
1.利用TiXmlFOpen函数把xml文件读入文件流中
2.再用fread函数把流中的文件写入一个char型数组中
3.把char型数组中"\r\n",’\r’,统一转换为’\n’,以统一不同系统的文件换行方式。
4.用parse函数对第三步转化得到的char型数组进行解析,并按节点形成树型结构
第四步是整个程序的核心,接下来仔细的解析整个parse的过程。
const char* TiXmlDocument::Parse(const char* p, TiXmlParsingData* prevData, TiXmlEncoding encoding)
{
ClearError();
if ( !p || !*p )
{
SetError( TIXML_ERROR_DOCUMENT_EMPTY, 0, 0, TIXML_ENCODING_UNKNOWN );
return 0;
}
// Note that, for a document, this needs to come
// before the while space skip, so that parsing
// starts from the pointer we are given.
location.Clear();
if ( prevData )
{
location.row = prevData->cursor.row;
location.col = prevData->cursor.col;
}
else
{
location.row = 0;
location.col = 0;
}
TiXmlParsingData data( p, TabSize(), location.row, location.col );//需要解析的数据
location = data.Cursor();//返回解析数据的位置
//当不知道编码模式时,检查UTF-8先导字节,如果存在,则用UTF-8解码文件
if ( encoding == TIXML_ENCODING_UNKNOWN )
{
// Check for the Microsoft UTF-8 lead bytes.
const unsigned char* pU = (const unsigned char*)p;
if ( *(pU+0) && *(pU+0) == TIXML_UTF_LEAD_0
&& *(pU+1) && *(pU+1) == TIXML_UTF_LEAD_1
&& *(pU+2) && *(pU+2) == TIXML_UTF_LEAD_2 )
{
encoding = TIXML_ENCODING_UTF8;
useMicrosoftBOM = true;
}
}
//跳过重复的空格字符
p = SkipWhiteSpace( p, encoding );
if ( !p )
{
SetError( TIXML_ERROR_DOCUMENT_EMPTY, 0, 0, TIXML_ENCODING_UNKNOWN );
return 0;
}
//这个while循环是关键,按节点把整个文档解析为一棵树
while ( p && *p )
{
//Identify语句识别出下一个节点的类型,创建一个空的对象,并把它的父指针设为document,返回对象指针。
TiXmlNode* node = Identify( p, encoding );
if ( node )
{ //通过动态绑定,调用对应类型的节点的parse函数进行解析。
p = node->Parse( p, &data, encoding );
LinkEndChild( node );//解析出来的节点链接到父节点的最后一个孩子上
}
else
{
break;
}
// 如果没有识别出来UTF-8的先导字节,那再去找declaration里的encoding信息
if ( encoding == TIXML_ENCODING_UNKNOWN
&& node->ToDeclaration() )
{
TiXmlDeclaration* dec = node->ToDeclaration();
const char* enc = dec->Encoding();
assert( enc );
if ( *enc == 0 )
encoding = TIXML_ENCODING_UTF8;
else if ( StringEqual( enc, "UTF-8", true, TIXML_ENCODING_UNKNOWN ) )
encoding = TIXML_ENCODING_UTF8;
else if ( StringEqual( enc, "UTF8", true, TIXML_ENCODING_UNKNOWN ) )
encoding = TIXML_ENCODING_UTF8; // incorrect, but be nice
else
encoding = TIXML_ENCODING_LEGACY;
}
//跳过空格,\r,\n等字符
p = SkipWhiteSpace( p, encoding );
}
// Was this empty?
if ( !firstChild ) {
SetError( TIXML_ERROR_DOCUMENT_EMPTY, 0, 0, encoding );
return 0;
}
// All is well.
return p;
}
以上面那个解析树为例,在TiXmldocument的解析程序中,
- 第一次循环identify函数识别出 TiXmlDeclaration,然后调用 TiXmlDeclaration::parse(),解析出来的结果保存在 TiXmlDeclaration定义的string中。
- 第二次循环identify函数识别出TiXmlComment,调用TiXmlComment::parse(),把注释的内容保存到从TiXmlNode::value中。
- 第三次循环identify函数识别出 TiXmlElement,调用 TiXmlElement::parse(),把Element的值读入TiXmlNode::value中,再读入TiXmlElement中可能有的属性值。读完之后重头戏来了,要开始读这个< ToDo>…</ ToDo>中间夹的东西。中间可能包含:TiXmlText, TiXmlDeclaration,TiXmlComment,TiXmlElement。
识别出来中间包含的东西之后,建立对应的节点,例如识别出来另一个TiXmlElement,就建立一个TiXmlElement对象,然后把它加入树中。如果这个TiXmlElement中又包含了其他节点就继续识别,深度优先的把包含的 TiXmlElement识别完,再退回去识别其他的内容。
至此整个document已经全部解析完,生成了一棵解析树 。
如何输出这个解析树,并且更改树的节点的内容,在下一篇文章《tinyXML源码刨析(下)–解析树的使用》中总结。