Pugixml是一个轻量级的C++ XML开源解析库,DOM形式的解析器、接口和丰富的遍历和修改操作,快速的解析,此外支持XPath1.0实现数据查询,支持unicode编码;
使用Pugixml可通过直接在项目中包含其几个文件或者编译为动态库dll、静态库lib的形式供其他项目使用、比较方便,如果需要推荐编译为静态库或文件包含即可;
Pugixml项目中提供了文档手册、快速使用指南,可参考文档说明和smaples中的示例代码尝试快速上手使用,以及源码分析;
搭建好环境、工程(具体可参照文档、手册),我们以smaples中的load_file.cpp文件作为分析的出发点,运行程序,观察执行结果,当然需要当前路径下的tree.xml文件,否则会加载失败;可以预先查看tree.xml文件内容,该文件作为经典例子来学习,内容基本涵盖了整个解析器可实现的解析功能,继续调试跟踪;
首先pugi::xml_document作为文档类也作为DOM树的根节点类,其继承于xml_node节点类;在Pugixml中xml_node节点类作为操作节点的轻量级基础类,基本上大多数操作基于此类;xml_node节点类实现的操作接口比较多,但是成员变量仅有一个_root,该变量类型为节点结构,作为当前节点的根;继续跟踪节点结构定义;
xml_node_struct:节点结构
header:目前还不知道含义,根据初始化可推测为指向分配的内存页首地址;
name:节点名称;
value:节点的值;
parent:父节点;
first_child:第一个子节点;
prev_sibling_c:上一个兄弟节点;
next_sibling:下一个兄弟节点;
first_attribute:节点的第一个属性;
xml_attribute_struct:节点的属性结构
header:指向内存地址首地址;
name:节点属性名称;
value:节点属性的值;
prev_attribute_c:上一个兄弟属性;
next_attribute:下一个兄弟属性;
事实上xml_node节点类的成员_root标识当前节点的(作为当前节点的根节点),所以xml_document的_root则为整个DOM树的根节点;xml_node节点类的其他成员暂不分析后面会分析到;
xml_memory_page:内存页
allocator:内存分配器对象;
prev:上一个内存页;
next:下一个内存页;
busy_size:正使用的内存页大小;
freed_size:空闲的内存页大小;
xml_allocator:内存分配器(提供了分配和释放内存的操作接口)
_root:内存页根节点;
_busy_size:已使用内存大小;
xml_document_struct:文档结构类(继承于xml_node_struct、xml_allocator)
buffer:文档结构缓冲区;
extra_buffers:额外的缓冲区;
xml_extra_buffer:额外缓冲区
buffer:缓冲区;
next:下一个额外缓冲区;
xml_document类,可以发现继承了xml_node类操作还增加了一些加载和保存相关的操作接口,以及create、destory、reset,这几个函数结合_buffer、_memory主要用来预分配、初始化页内存分配、对齐或释放操作;
create: 内部操作;
1.检验哨兵页_memory是否够用以保证分配页起始位置仍在_memory范围内;
2.对齐分配页起始内存位置(按照xml_memory_page_alignment长度对齐);
3.将在_memory中的得到的起始内存页位置初始化得到内存页page;
4.将page的正使用的内存页大小busy_size设置为xml_memory_page_size(32768个字节);
5.在_memory的page页结构后new重分配xml_document_struct大小的空间作为_root根节点,并将_root的上一个兄弟节点指向自身,_root节点作为page的内存分配器;
6.再次检验page后重分配xml_document_struct大小的空间是否超过_memory范围;
destroy:内部操作
释放_buffer缓冲空间、_root下的额外缓冲空间以及_root的兄弟缓冲空间;
reset:内部调用create、destroy,另外一个重载版本支持拷贝另一个xml_document来重新初始化DOM树;
基本上我们已确定的当前内存布局方式:
以_memory作为基础,在_memory上建立page(root_page)、_root布局和位置定位,确定page与_root间的关系,此外_root扮演着内存分配器的功能负责分配额外缓冲区xml_extra_buffer以及分配页缓冲区xml_memory_page;而xml_exrta_buffer维护一个额外缓冲区链表xml_extra_buffer交由_root管理, xml_memory_page维护自己的页缓冲区链表交由page管理,不过xml_document只保存了_root,但_root
成员已保存了page的首地址,可以追寻到page;总结:目前已经存在上述的两套链表;
文件加载和解析:
load或load_XXX:加载XML文件或文件内容,先暂时直接分析load_file接口,load_file()-->impl::load_file_impl()-->load_buffer_impl()-->impl::xml_parser::parse();以下将依次按照函数调用顺序进行分析:load_file:提供重载版本,参数path为xml文件路径,options为解析选项,默认解析模式为parse_default,即在DOM树种元素、PCDATA、CDATA块被扩展,结束换行符标准化、属性值按照CDATA块方式进行标准化处理;若选项为parse_full,则解析所有包含parse_default以及pi数据、注释数据、声明数据等;参数encoding为编码方式,默认为自动识别,pugixml提供了可支持的多种编码方式xml_encoding如:UTF8、Little-endian UTF16、Big-endian UTF16、Little-endian UTF32、Big-endian UTF32等;impl::load_file_impl:加载文件实现接口,增加参数_root,文件描述符,_buffer保存文件内容缓冲区;函数内部通过get_file_size获取文件大小并通过impl::xml_memory::allocate分配足够容纳所有文件内容的缓冲区(该分配器内部默认调用malloc和free,用户可通过set_memory_management_functions修改其为自己的内存分配器),通过get_buffer_encoding获取缓冲区内容真实的编码方式,最后通过调用zero_terminate_buffer修正buffer结束终止符号;load_buffer_impl:内部调用impl::convert_buffer实现编码格式的转化,具体的转化过程暂时跳过,不作分析(不过内部重新申请了一片新的转化后的缓冲区内容,并将早期的_buffer通过impl::xml_memory::deallocate释放掉了),_buffer指向了转化后的文件内容缓冲区,此外doc->buffer亦保存该新的缓冲区地址;impl::xml_parser::parse:解析文件内容缓冲区,内部通过xml_parser解析器调用parse_tree解析文件内容,完成DOM树构建(事实上每个节点都一个_root成员表示以自己为根节点时可以遍历其兄弟节点和子节点,故xml_xml_document的_root作为根节点可以遍历整个树的信息() ),此外每个节点或属性均通过内存分配器在堆上分配的(不过不用担心这些节点已在xml_memory_page和xml_extra_buffer中管理和分配,其已尽可能减少内存分配和内存碎片),最后说明所有节点的前一个兄弟节点若不存在时则指向自己,下一个兄弟节点不存在时指向空,所有的属性的前一个兄弟属性若不存在时也指向自己,下一个兄弟属性不存在时指向空;具体的解析过程不再去分析,比较繁琐,有时间可以去细化深入分析;
再次分析load/load_xxx加载接口,目前load接口提供了三个重载版本,分别支持std::basic_istream流、XML文件内容格式的字符串;无论哪种最终转化为调用接口load_buffer_impl()实现加载解析;load_string、load_buffer支持加载其他内存缓冲区或xml文件内容字符串;此外还有load_buffer_inplace、load_buffer_inplace_own,前者需要用户提供内容缓冲区且保证整个DOM树生存期内仍然存活,与其他加载方式不同,此接口下DOM内部不再拷贝副本;load_buffer_inplace_own也由用户提供内容缓冲区,但是DOM会接管该缓冲区,外部不再允许释放(!DOM会释放,意味着该缓冲区必须在堆上创建,建议不使用该接口);
文件保存:
save/save_file:保存XML内容至数据流或文件中;现在暂时分析save_file接口;save_file()-->impl::save_file_impl()-->save()-->impl::node_output();
以下将依次按照函数调用顺序进行分析:
save_file:参数path_为保存xml文件路径名,indent为缩排字符串,默认为”\t”,flags为输出格式选项,默认值format_default(节点缩进依赖于其在DOM树的深度,以及一个默认的声明),参数encoding为输出文件的编码方式,默认为自动encoding_auto(即默认的编码);impl::save_file_impl:保存文件实现,增加参数文档对象doc,文件描述符,内部通过创建一个xml_writer_file对象,并将该对象传入文档对象doc的save方法
实现文件保存;
save:保存文件操作,内部通过传入的xml_writer_file对象创建impl::xml_buffered_writer实例,该实例对象进行真正的写文件操作对象(内含一成员buffer当保存大于该容量阈值才进行文件flush,以减少写文件的次数),默认将声明写入文件,此后调用impl::node_output遍历DOM树节点并按照缩进格式和节点的当前树深度写入指定文件;
再次分析save接口,重载三个版本,分别支持输出到xml_writer、
std::basic_ostream,用户可以继承并重写xml_writer的write函数增加自己的操作,也可以输出到ostream流中;
节点添加、修改、删除:
pugixml提供了丰富的操作接口,具体可查看xml_node节点类相关接口,此外最重要的是内存布局、分配的问题,所有节点均在分配页中进行管理,基本上不用单独申请或释放任何节点,释放时调用destroy时会释放掉所有的那些节点或属性依附的分配页即可;
节点的遍历和查找:
pugixml提供了基本的遍历方式即通过child、next_sibling、next_attribute等可以较为方便的实现节点或属性的遍历;也提供了xml_node_iterator、xml_attribute_iterator节点迭代器和属性迭代器(事实上是对基本遍历方式的简单封装);此外提供了谓语查找,用户可以提供自己的查找函数(只需要提供仿函数或函数对象即可)主要用在find_child、find_node、find_attribute这几个接口;xml_document类提供了一个接口traverse,其可支持用户自定义的遍历“步行者”,通过继承xml_tree_walker类并实现for_each回调函数接口,该接口将遍历所有节点,返回值为true则继续爬行,否则停止爬行,此外用户也可以实现其begin与end接口,这两个接口分别在for_each执行前后,可增加必要的初始化操作或收尾操作,返回false则退出;该类中成员_depth为当前节点的爬行深度,可通过depth()接口获取;
额外说明:
值得说明的是pugiconfig.hpp配置文件,里面定义了许多宏,如:
PUGIXML_WCHAR_MODE:开启unicode支持;
PUGIXML_COMPACT:支持紧凑型内存管理布局;
PUGIXML_NO_XPATH:不需要xpath支持,可减少pugixml编译为库的体积;
PUGIXML_NO_STL:不使用STL支持,对于不想使用STL环境下比较有用;
PUGIXML_NO_EXCEPTIONS:不允许抛出异常;
PUGIXML_API:编译导出为DLL;
PUGIXML_MEMORY_PAGE_SIZE:内存页大小;
PUGIXML_HAS_LONG_LONG:支持long long数据类型;
PUGIXML_HEADER_ONLY:只包含头文件,这样头文件内部会自己包含cpp文件;
可根据需要使用或修改相应的宏;
总结:
pugixml提供了丰富的节点操作和遍历接口并以DOM形式构建,此外内存管理提供用户内存分配器以及支持对齐或紧凑型内存布局、文件解析并支持多种编码方式xml文件和编码格式的相互转化,支unicode,默认为utf-8格式;(事实上xml文件加载或文件内存缓冲区加载的时候,可以提前释放xml_document类成员_buffer的空间,因后面一直没有用到,且释放空间可供进程重新申请使用该释放的空间);接口使用简便、操作解析也比较快速,内存管理相对tinyxml内存碎片也会少很多且指针访问比较集中(tinyxml内部无内存管理,可能也会导致内存碎片以及轻微的访问、操作较慢一些)。