TinyXML-2学习笔记

初中同学拜托做的一个调研,正好自己也在做相关的内容,也就拿来看看啦

背景材料

XML 可拓展标记语言

XML是Extensible Markup Language的缩写。主要用于将信息保存为便于计算机程序读取、解析的格式。

XML 解析方式

参考资料:廖雪峰Java教程XML部分

因为XML是一种树形结构的文档,它有两种标准的解析API:

  • DOM:一次性读取XML,并在内存中表示为树形结构;
  • SAX:以流的形式读取XML,使用事件回调。

DOM是Document Object Model的缩写,DOM模型就是把XML结构作为一个树形结构处理,从根节点开始,每个节点都可以包含任意个子节点。

其他内容可以参考参考资料中的图示进行理解。虽然那个是以Java语言为载体写的例子,但是也能看,Java和C++在这方面区别不大。DOM应该一开始是为JS设计的,也能比较方便在其他语言上使用。

项目概述

项目功能

TinyXML-2是一个轻量级的、高效的XML的C++语言解析器,能够十分轻松地将之集成进其他项目。

简单来说,TinyXML-2能够解析XML文档,然后根据解析出的信息建立C++对象(DOM模型),这个模型可以用于读取、修改和保存。

这个操作也可以反过来,也可以使用C++的对象来从头开始构建一个XML文档,支持新建节点、新建元素、添加属性等操作。

主要的算法原理是First Child and Next Sibling Tree,即从根节点开始,顺序读取XML文档,建立DOM树。

项目特点

  • TinyXML-2是个可以用于开源软件以及商业的小插件,使用的ZLib证书

  • TinyXML-2不依赖于C++的异常(Exception),RTTI 以及 STL(说实话我不知道RTTI是啥)

  • TinyXML-2无法处理通过DTD(文档类型定义)或者是XSL(拓展样式表语言)约束的内容

    菜鸟教程 DTD教程 菜鸟教程 XSL语言

  • TinyXML-2使用UTF-8编码,注意C++代码以及XML文件的编码(好像有些机子上C++代码的默认编码是GBK?

DEMO

考虑到上一个文档写的太烂了,这次具体写写怎么让这个东西跑起来。

运行环境:CodeBlocks

  1. 新建一个project

  2. 从GitHub上下载源代码,将源代码中tinyxml.2.h以及tinyxml2.h,添加到项目中

  3. 在同一个文件夹下新建一个dream.xml,内容需要符合xml的内容格式(见上文)

    <?xml version="1.0" encoding="UTF-8"?>
    <PLAY>
        <TITLE>
            瞎写点东西就行
        </TITLE>
    </PLAY>
    
  4. 将README中给出的demo抄进main,具体如下

    #include <iostream>
    #include "tinyxml2.h"
    using namespace std;
    using namespace tinyxml2;
    
    void example() {
        XMLDocument doc;
        doc.LoadFile("dream.xml");
    
        // Navigate to the title, using the convenience function,
    	// with a dangerous lack of error checking.
    	const char* title = doc.FirstChildElement( "PLAY" )->FirstChildElement( "TITLE" )->GetText();
    	printf( "Name of play (1): %s\n", title );
    
    	// Text is just another Node to TinyXML-2. The more
    	// general way to get to the XMLText:
    	XMLText* textNode = doc.FirstChildElement( "PLAY" )->FirstChildElement( "TITLE" )->FirstChild()->ToText();
    	title = textNode->Value();
    	printf( "Name of play (2): %s\n", title );
    
    }
    
    int main() {
        example();
        return 0;
    }
    
  5. 编译运行该project

代码组成

代码中类的结构

类的继承关系如下

  • tinyxml2::XMLAttribute
  • tinyxml2::XMLConstHandle
  • tinyxml2::XMLHandle
  • tinyxml2::XMLNode
    • tinyxml2::XMLComment
    • tinyxml2::XMLDeclaration
    • tinyxml2::XMLDocument
    • tinyxml2::XMLElement
    • tinyxml2::XMLText
    • tinyxml2::XMLUnknown
  • tinyxml2::XMLVisitor
    • tinyxml2::XMLPrinter

代码中类的功能(部分)

XMLAttribute

编辑配置标签、元素、文档的属性

XMLNode

处理与节点相关的内容,作为以下六个类的超类。

XMLComment

处理与XML文档中与注释相关的内容。

XMLDeclaration

处理与XML文档中的声明相关的内容。

XMLDocument

处理与XML文件直接相关的内容。

XMLElement

处理与单个元素相关的内容。

XMLText

处理与XML文档中纯文本相关的内容。

XMLUnknown

他是unknown,我也unknown

细节分析

主要思路:从demo的example函数开始,找到程序的入口,根据函数调用的顺序进行分析

文档读入LoadFile

XMLDocument doc;
doc.LoadFile("dream.xml");

找到LoadFile函数,该函数在XMLDocument类下,负责根据文件名打开文件,并开始调用解析函数。

LoadFile分为两个参数不同的函数,代码如下,已附上简单注释

// 根据文件名打开文件
XMLError XMLDocument::LoadFile( const char* filename )
{
    if ( !filename ) { // 当文件名为空时报错
        TIXMLASSERT( false );
        SetError( XML_ERROR_FILE_COULD_NOT_BE_OPENED, 0, "filename=<null>" );
        return _errorID;
    }

    Clear(); // 初始化读取相关内容
    FILE* fp = callfopen( filename, "rb" ); // 二进制形式打开
    if ( !fp ) { // 当文件不存在时报错
        SetError( XML_ERROR_FILE_NOT_FOUND, 0, "filename=%s", filename );
        return _errorID;
    }
    LoadFile( fp ); // 调用另一个LoadFile函数进行进一步解析
    fclose( fp ); // 关闭FILE指针
    return _errorID;
}

// 通过FILE指针进行解析
XMLError XMLDocument::LoadFile( FILE* fp )
{
    Clear();

    TIXML_FSEEK( fp, 0, SEEK_SET ); // 将FILE指针fp指向XML文件开头
    // 在Windows平台下调用的是_fseeki64
    if ( fgetc( fp ) == EOF && ferror( fp ) != 0 ) {
        SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 );
        return _errorID;
    }

    TIXML_FSEEK( fp, 0, SEEK_END ); // 将FILE指针fp指向XML文件结尾

    unsigned long long filelength;
    {
        const long long fileLengthSigned = TIXML_FTELL( fp );
        // 在Windows平台下调用的是_ftelli64,由此得到XML文件的二进制表示下的长度
        
        TIXML_FSEEK( fp, 0, SEEK_SET ); // 回到文件开头
        if ( fileLengthSigned == -1L ) { // 当TIXML_FTELL发生错误时报错
            SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 );
            return _errorID;
        }
        TIXMLASSERT( fileLengthSigned >= 0 ); // 断言保证值的有效性
        filelength = static_cast<unsigned long long>(fileLengthSigned); // 转换类型保存文件长度
    }

    const size_t maxSizeT = static_cast<size_t>(-1);
    // We'll do the comparison as an unsigned long long, because that's guaranteed to be at
    // least 8 bytes, even on a 32-bit platform.
    // 上文注释翻译:我们将以无符号长长的方式进行比较,因为即使在32位平台上,也能保证至少有8个字节。
    if ( filelength >= static_cast<unsigned long long>(maxSizeT) ) {
        // 当文件过长时报错
        // Cannot handle files which won't fit in buffer together with null terminator
        SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 );
        return _errorID;
    }

    if ( filelength == 0 ) { // 当文件为空时报错
        SetError( XML_ERROR_EMPTY_DOCUMENT, 0, 0 );
        return _errorID;
    }

    const size_t size = static_cast<size_t>(filelength); // 转存文件长度为“size”
    TIXMLASSERT( _charBuffer == 0 ); // 保证缓冲区初始值正常
    _charBuffer = new char[size+1]; // 给字符缓冲区分配大小适合将要读取的XML的空间
    const size_t read = fread( _charBuffer, 1, size, fp ); // 将XML文件内容放入字符缓冲区
    if ( read != size ) { // 当读取失败时报错
        SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 );
        return _errorID;
    }

    _charBuffer[size] = 0; // 字符缓冲区末尾设零

    Parse(); // 正式开始解析XML文件
    return _errorID;
}

fseek() ,fseeko(),fseeko64()讲解

C库函数 ftell函数讲解

解析函数Parse()

上文最后开始正式解析XML文件,调用同为XMLDocument库中的Parse()函数

void XMLDocument::Parse()
{
    TIXMLASSERT( NoChildren() ); // Clear() must have been called previously
    // 保证DOM树目前为空(最初已经通过Clear()函数清空上次读取时的残留数据
    TIXMLASSERT( _charBuffer ); // 保证缓冲区已经有文件读入,避免空读出错
    _parseCurLineNum = 1; // 当前行行号
    _parseLineNum = 1; // 已解析行数
    char* p = _charBuffer; // 指针p开始逐位解析读入到缓冲区的XML文件
    p = XMLUtil::SkipWhiteSpace( p, &_parseCurLineNum ); 
    // 调用XMLUtil库的函数,设置解析过程中对WhiteSpace的处理
    p = const_cast<char*>( XMLUtil::ReadBOM( p, &_writeBOM ) );
    // 对XML文件开头的BOM编码进行检查
    if ( !*p ) { // 对空文件(在BOM编码之后为空)进行报错
        SetError( XML_ERROR_EMPTY_DOCUMENT, 0, 0 );
        return;
    }
    ParseDeep(p, 0, &_parseCurLineNum ); // 进一步开始解析文档正文
}

XML的BOM编码

什么是white space属性:MDN web docs

XML正文解析函数ParseDeep

该方法中比较长的注释在此说明:

这是一个递归调用的方法,请尽量从当前层次进行考虑

根据读取的方式(类似DFS的思想),会出现一个问题,成对出现的元素的关闭元素会在这个标签的子元素被解析的时候解析到。

’endTag’指当前节点的关闭标签,他将通过调用子节点进行返回

‘parentEnd’时当前节点的父节点的关闭标签,他将被填写然后返回。

char* XMLNode::ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr )
    // 由于XMLDocument类没有重载这个函数,所以调用的会是其超类的XMLNode::ParseDeep
{
	XMLDocument::DepthTracker tracker(_document);
	if (_document->Error())
		return 0;

	while( p && *p ) { // 循环解析各个节点
        XMLNode* node = 0;

        p = _document->Identify( p, &node ); // 检查XML文档的格式是否完整,是否合法
        TIXMLASSERT( p ); // 确保文档可读取
        if ( node == 0 ) { // 当前节点啥都不是的时候退出循环(相当于读取结束)
            break;
        }

		const int initialLineNum = node->_parseLineNum;
        // 确定初始行

        StrPair endTag;
        p = node->ParseDeep( p, &endTag, curLineNumPtr );
        if ( !p ) { // p为null时报错
            DeleteNode( node );
            if ( !_document->Error() ) {
                _document->SetError( XML_ERROR_PARSING, initialLineNum, 0);
            }
            break;
        }

        const XMLDeclaration* const decl = node->ToDeclaration();
        // 判断是不是"声明",并进行处理
        if ( decl ) { // 当前节点是声明
            // Declarations are only allowed at document level
            // 只允许在文档层面出现声明,且若需要多个声明,需要放置在其他内容前
            // Multiple declarations are allowed but all declarations
            // must occur before anything else. 
            //
            // Optimized due to a security test case. If the first node is 
            // a declaration, and the last node is a declaration, then only 
            // declarations have so far been added.
            bool wellLocated = false;

            if (ToDocument()) {
                if (FirstChild()) {
                    // 若已有第一个节点,需要满足当前节点是声明,最后一个节点也是声明,才算是合乎语法
                    wellLocated =
                        FirstChild() &&
                        FirstChild()->ToDeclaration() &&
                        LastChild() &&
                        LastChild()->ToDeclaration();
                }
                else { // 其他情况均合乎语法
                    wellLocated = true;
                }
            }
            if ( !wellLocated ) {
                _document->SetError( XML_ERROR_PARSING_DECLARATION, initialLineNum, "XMLDeclaration value=%s", decl->Value());
                DeleteNode( node );
                break;
            }
        }

        XMLElement* ele = node->ToElement();
        // 判断是不是元素,并处理
        if ( ele ) {
            // We read the end tag. Return it to the parent.
            if ( ele->ClosingType() == XMLElement::CLOSING ) { // 若是关闭标签,交由父节点处理
                if ( parentEndTag ) {
                    ele->_value.TransferTo( parentEndTag );
                }
                node->_memPool->SetTracked();   // created and then immediately deleted.
                DeleteNode( node );
                return p;
            }

            // Handle an end tag returned to this level.
            // And handle a bunch of annoying errors.
            // 当当前节点不是关闭标签,可以处理调回的关闭标签,与上文对应
            bool mismatch = false;
            if ( endTag.Empty() ) { // 当是调回的情况,当前节点的关闭标签一定是已知并保存的
                if ( ele->ClosingType() == XMLElement::OPEN ) { // 检查调回的关闭标签是否与当前标签匹配
                    // 未保存且标签匹配
                    mismatch = true;
                }
            }
            else {
                if ( ele->ClosingType() != XMLElement::OPEN ) {
                    // 保存但标签不匹配
                    mismatch = true;
                }
                else if ( !XMLUtil::StringEqual( endTag.GetStr(), ele->Name() ) ) {
                    // 标签不匹配
                    mismatch = true;
                }
            }
            if ( mismatch ) { // 根据非正常状况进行报错
                _document->SetError( XML_ERROR_MISMATCHED_ELEMENT, initialLineNum, "XMLElement name=%s", ele->Name());
                DeleteNode( node );
                break;
            }
        }
        InsertEndChild( node ); // 添加子节点
    }
    return 0;
}

当前行格式检查Identify

char* XMLDocument::Identify( char* p, XMLNode** node )
{
    TIXMLASSERT( node ); // 当前节点
    TIXMLASSERT( p ); // 文档位置指针
    char* const start = p; // 开始检查的位置
    int const startLine = _parseCurLineNum; // 开始检查的行号
    p = XMLUtil::SkipWhiteSpace( p, &_parseCurLineNum ); // 跳过空白符
    if( !*p ) {
        *node = 0;
        TIXMLASSERT( p );
        return p;
    }

    // These strings define the matching patterns:
    // 以下字符串规定了匹配规则
    static const char* xmlHeader		= { "<?" }; // 文档开头
    static const char* commentHeader	= { "<!--" }; // 注释开头
    static const char* cdataHeader		= { "<![CDATA[" }; // CDATA内容开头
    static const char* dtdHeader		= { "<!" }; // DTD内容开头
    static const char* elementHeader	= { "<" };	// and a header for everything else; check last.
    // 最后检查最常见的开头

    // 规定开头内容的长度
    static const int xmlHeaderLen		= 2;
    static const int commentHeaderLen	= 4;
    static const int cdataHeaderLen		= 9;
    static const int dtdHeaderLen		= 2;
    static const int elementHeaderLen	= 1;

    TIXMLASSERT( sizeof( XMLComment ) == sizeof( XMLUnknown ) );		// use same memory pool
    TIXMLASSERT( sizeof( XMLComment ) == sizeof( XMLDeclaration ) );	// use same memory pool
    XMLNode* returnNode = 0;
    
    // 以下内容为逐个试着判断当前行的内容,仅解释第一个,剩下的同理
    if ( XMLUtil::StringEqual( p, xmlHeader, xmlHeaderLen ) ) { // 判断是否为XML文档头
        returnNode = CreateUnlinkedNode<XMLDeclaration>( _commentPool ); // 将当前节点标记为文档头
        returnNode->_parseLineNum = _parseCurLineNum; // 当前节点对应的行标记为当前行
        p += xmlHeaderLen; // p指针向后跳到该节点的正文位置
    }
    else if ( XMLUtil::StringEqual( p, commentHeader, commentHeaderLen ) ) {
        returnNode = CreateUnlinkedNode<XMLComment>( _commentPool );
        returnNode->_parseLineNum = _parseCurLineNum;
        p += commentHeaderLen;
    }
    else if ( XMLUtil::StringEqual( p, cdataHeader, cdataHeaderLen ) ) {
        XMLText* text = CreateUnlinkedNode<XMLText>( _textPool );
        returnNode = text;
        returnNode->_parseLineNum = _parseCurLineNum;
        p += cdataHeaderLen;
        text->SetCData( true );
    }
    else if ( XMLUtil::StringEqual( p, dtdHeader, dtdHeaderLen ) ) {
        returnNode = CreateUnlinkedNode<XMLUnknown>( _commentPool );
        returnNode->_parseLineNum = _parseCurLineNum;
        p += dtdHeaderLen;
    }
    else if ( XMLUtil::StringEqual( p, elementHeader, elementHeaderLen ) ) {
        returnNode =  CreateUnlinkedNode<XMLElement>( _elementPool );
        returnNode->_parseLineNum = _parseCurLineNum;
        p += elementHeaderLen;
    }
    else { //当匹配了一圈发现啥都不是的时候,标记当前节点为纯文本
        returnNode = CreateUnlinkedNode<XMLText>( _textPool );
        returnNode->_parseLineNum = _parseCurLineNum; // Report line of first non-whitespace character
        p = start;	// Back it up, all the text counts.
        // 回退到开始检查的位置
        _parseCurLineNum = startLine;
    }

    // 确保这两者均有收获
    TIXMLASSERT( returnNode );
    TIXMLASSERT( p );
    
    // 返回结果
    *node = returnNode;
    return p;
}

CDATA内容是什么

XML DTD

就写到这儿吧,这些也够那哥们交作业了,不写了,毕竟我不常用C++…