非标准的xml解析器的C++实现:一、思考基本数据结构的设计
前言:
我在C++项目中使用xml作为本地简易数据管理,到目前为止有5年时间了,从最初的全文搜索标签首尾,直到目前项目中实际运用的类库细致到已经基本符合w3c标准,我一共写过3次解析器,我自己并没有多喜欢xml,对于xml最初的需求是客户提出的,有了第一次,就有后来的无数次使用xml的场景,配置文件,数据交换,GUI布局,直到现在,新建一个项目,它基本与日志一样成为了一个必备的功能,哪怕在我已经实现了它的替代方案json的当下情况,我依然认为它还有足够顽强的生命力,举个简单例子,用户看json觉得费劲,看不懂,但用户看xml却很容易,客户第一,大家都懂的。所以在跨平台应用中,它的地位依然坚挺。写下这篇文章,表示我正在第4次写xml解析器。
我第三次写的xml解析,除了xml声明里的东西,已经基本符合xml1.0标准了,文档类型定义,命名空间,都按照w3c规范来设计,说起来相当可笑,这些东西,符合标准,实现花了大量时间,但近两年的时间里,一次都没有被我运用到项目中,根本没有实际场景,需要由文档携带逻辑的地方,都已经被我在C++处理清楚了。
在几年前,我首先参考了html,在js中的getElementById,element.innerHTML之类的东西,是必要实现的。
从element.innerHTML来看,如果不做细节处理,这种方式的内存占用随文档深度增加而增加,没有极限。
所以,一个xml节点完整携带它所有子节点文本的方案,是不可取的。
所以当年我思考了一种方案:
dom中只存在一份完整的文档
解析到具体节点的数据,统一使用位置与长度来保存,
需要获取文档数据时,直接从完整文档中根据位置和长度拷贝出来,
这种方式,开始写的时候,是美好的,直到最后写到删,改,索引逻辑的时候,才发现我当时脑子有坑,坑里有翔,翔里有蛆,蛆里下毒@#¥%。
因为最坏的情况,通过dom修改一个文档数据,需要遍历整个文档的所有节点将新的位置与长度改变,但是当时由于项目在赶工,没有充足的时间让我推翻重来,所以只能硬着头皮这么干,好在,用了几年,没出什么大问题,但由于一直没时间,这个问题这几年一直是我最大的心病,没错的,我有强迫症。
好吧,经验教训总结够了,在我开始动手写代码之前,在这里给我的具体实现方案做足功课,同时,给想做,正在做这件事的人提个醒,能够少走弯路。
开始思考吧:结合了经验教训,这次我打算将一个完整的文档分裂成字符串链表来储存在内存里,用一些简单的文档来模拟,应该怎么做。
我这一次不打算实现标准需求里的由文档携带逻辑的部分内容,例如DTD,namespace我打算抛弃掉。
<a> <b>1</b> </a>
上面这个文档,拆分为字符串链表:{"<a>", "<b>", "1", "</b>", "<a>"},在C++中,使用std::list<std::string>来储存。
所以节点数据结构应该设计成:
struct xnode{ std::list<std::string>::iterator tag_name;//标签名称 std::list<std::string>::iterator inner_begin, inner_end;//内部文本首尾 std::list<xnode> childs;//子节点 xnode *parent;//父节点 std::list<xnode>::iterator self;//自己在父结点中的迭代位置,它的前后节点,拷贝之后,通过运算符++ --来获取。 };
---------------------------------------------------
xnode root;
解析文档后,
root.tag_name => "<a>"
root.inner_begin => "<b>"
root.inner_end => "</a>"
root.childs.begin() 就是标签<b>的节点,我这里临时用b来表示它。
b.tag_name => "<b>";
b.inner_begin => "1";
b.inner_end => "</b>";
这样子,我需要实现获取innerText的逻辑时,只需要:
std::string str;
for(auto i = elem.inner_begin; i != elem.inner_end; ++i)
str += *i;
第一步看起来并没有坑点,希望这个方向是正确的,然后使用一个更复杂一点的文档来看看:
<a attr1='1' attr2 = "2"> <b attr1='1' attr2 = "2">xxx</b> </a>
涉及到标签属性,情况变得复杂了
首先,把一个标签名称包括属性,如果分裂掉,可能会存在很多1个字符,也由一个std::string来储存的问题。
然后是,解析器的性能也降低了,同时,后续的innerText字符串拼接,也会受到影响。
所以需要诞生另外一个容器,用来储存标签名称,属性?
std::map<std::string, std::list<xnode*>>,可以同时实现记录标签名称,以及根据标签索引实现getElementByTagName这种东西。
属性名称 通常在定义上,等同于常量,重复使用的概率会很大,所以应该是:std::set<std::string> ?
属性值 通常是变量,易变的概率很大,采用跟 属性名称统一的方式好像不是很适合,但属性值好像同样可能出现很多重复的字符串,例如true false之类的。
所以属性值,应该设计为:std::map<std::string, unsigned int> 将val设计为引用计数,为0时,erase掉,emm..不太可能有什么神经病用来解析40亿个节点的文档,所以unsigned int足够了。
所以,思考到这,大致的文档源数据结构出来了:
struct xsource{ std::list<std::string> docs; std::map<std::string, std::list<xnode*>> tags; std::set<std::string> attr_names; std::map<std::string, unsigned int> attr_values; };
由此带来的xnode结构的变化之后是:
struct xattr{ std::set<std::string>::iterator name; std::map<std::string, unsigned int> value; };
struct xnode{ std::map<std::string, std::list<xnode*>>::iterator tag; std::list<xnode*>::iterator itag;//用来在删除标签时,从xsource.docs中删除节点指针。 std::list<xattr> attrs; std::list<std::string>::iterator inner_begin, inner_end; std::list<xnode> childs; xnode *parent; std::list<xnode>::iterator self; };
今晚就思考到这,我明天先初步按这个思路实现看看。
未完待续...