libxml2修改XML文件总结

2025-09-23 07:32:14 欧冠世界杯

libxml2修改XML文件总结

前言

随项目需求,需要对xml文件进行数据节点进行增删。查找一圈,网上例程较多,但大多较为简略,且不够全面。在此结合个人理解,整合在一起,供有需求的人参考。 PS:建议先参考此文章,里面有libxml2相关的背景知识,值得一看libxml2库解析带命名空间的xml文件

测试文件

The Handmaid's Tale

Margaret

Atwood

19.95

The Poisonwood Bible

Barbara

Kingsolver

11.99

测试源码

基于源码example文件夹下xpath1.c做修改,修改部分会标出。下文直接将xpath1.c拷贝为xpath3.c然后进行操作。

正文

格式化保存xml文件

在对xml文件进行操作时,如果插入了节点,是不会自动换行的。如下: 因此需要使用如下API,在打开文件时进行特殊处理,去除空格。然后在输出时重新格式化:

xmlSaveFormatFile();

xmlReadFile();

//具体API的用法请参照官方PI说明

修改xpath1.c中execute_xpath_expression()函数的这两个位置: 再次编译运行,结果如下; 可以看到节点已经被插入,而且文件也已经被重新格式化了。

在xml文件中增加节点

在上一篇文章中提到,libxml2是以树和链表相混合数据结构解析xml文件的,解析后的大致结构图如下(官网提供的): 对应在xml文件中的形式如下: 注意,父元素节点只会指向子元素节点链表的链表头,链表是按照文件的前后顺序构建的。例如在上面的截图中元素节点是<book>元素节点的子节点,同时也是<book>下元素节点链表的链表头,链表顺序为<title> -> <author> -> <price>。xml就是按这种方式一层一层的解析,构建出来的。 在上一篇文章中,也是利用这个结构去遍历xpath下的所有节点。这里面有个小细节,每个子树都有一个类型标识符,如果为XML_ELEMENT_NODE,则对应xml文件中标识符(骨架),只有当子树节点为XML_TEXT_NODE时,这里面的content才会包含xml文件内容(可参照xmlNodeGetContent()函数实现)。xml的attribute也是同理(没用到,所以没怎么研究)。</p> <p>所以对应到我们对xml文件的修改,会有增加子节点、增加兄弟节点的需求。主要使用的xml tree API如下:</p> <p>//在当前节点下插入子树节点</p> <p>xmlNodePtr xmlAddChild (xmlNodePtr parent, xmlNodePtr cur);</p> <p>//在当前节点下插入树链表节点</p> <p>xmlNodePtr xmlAddChildList (xmlNodePtr parent, xmlNodePtr cur);</p> <p>//在当前节点链表前后添加节点</p> <p>xmlNodePtr xmlAddNextSibling (xmlNodePtr prev, xmlNodePtr cur);</p> <p>xmlNodePtr xmlAddPrevSibling (xmlNodePtr next, xmlNodePtr cur);</p> <p>xmlNodePtr xmlAddSibling (xmlNodePtr node, xmlNodePtr cur);</p> <p>//目前推荐用来创建节点的API。(官方推荐啥我用啥,主打一个老实听话^_^</p> <p>xmlNodePtr xmlNewDocNode (xmlDocPtr doc,</p> <p>xmlNsPtr ns,</p> <p>const xmlChar * name,</p> <p>const xmlChar * content);</p> <p>xmlNodePtr xmlNewDocNodeEatName (xmlDocPtr doc,</p> <p>xmlNsPtr ns,</p> <p>xmlChar * name,</p> <p>const xmlChar * content);</p> <p>//减少了参数需求,不过好像有些别的问题,目前官方已不推荐使用</p> <p>xmlNodePtr xmlNewNode (xmlNsPtr ns,</p> <p>const xmlChar * name);</p> <p>xmlNodePtr xmlNewNodeEatName (xmlNsPtr ns,</p> <p>xmlChar * name);</p> <p>接下来我们就用这几个API演示如何对XML文件进行操作。要记住,无论我们怎么修改,最终都是利用这几个函数构建出树-链表混合结构,再和原有节点建立链接,仅此而已。</p> <p>编码测试</p> <p>作为子树添加到当前节点中 /*</p> <p>*首先创建父节点 -> 再在父节点下创建文本子节点 -> 将父节点作为子节点添加到传入节点下</p> <p>*/</p> <p>void CreateAuthorType(xmlDocPtr doc , xmlNodePtr cur )</p> <p>{</p> <p>xmlNodePtr newnode = xmlNewDocNode(doc , NULL , BAD_CAST "writingstyle" , NULL );//这里是作为树节点使用,因此我没有为其赋值</p> <p>xmlNewTextChild(newnode , NULL , BAD_CAST "crusoe" , BAD_CAST "456");</p> <p>xmlNewTextChild(newnode , NULL ,BAD_CAST "art" , BAD_CAST "123");</p> <p>xmlAddChild(cur , newnode);</p> <p>}</p> <p>将上文中的代码片插入xpath1.c中print_xpath_nodes()函数的这个位置: 编译 我们为xml文件中所有的author下都添加写作风格: 可以看到我们通过xpath找到了两个author节点,然后分别为其添加了相同的wrtingstyle属性</p> <p>作为兄弟节点添加到当前节点 /*</p> <p>*首先创建父节点 -> 再在父节点下创建文本子节点 -> 将父节点作为子节点添加到传入节点下</p> <p>*/</p> <p>void CreateAuthorCountry(xmlDocPtr doc , xmlNodePtr cur )</p> <p>{</p> <p>xmlNodePtr newnode = xmlNewDocNode(doc , NULL , BAD_CAST "country" , "USA" );</p> <p>xmlAddNextSibling(cur , newnode);</p> <p>}</p> <p>将上文中的代码片插入xpath1.c中print_xpath_nodes()函数的这个位置: 运行结果如下,可以看到country已经作为兄弟节点,成功被添加到author后面了</p> <p>为指定节点添加内容 在上面的例子中,我们都是通过xpath定位元素节点后,批量替换,但实际上我们可能只需要为xpath结果中的一个节点做设置。就以我目前知道的方法有两种办法可以做到:</p> <p>使用更详细的xpath表达式定位目标节点,只要确保输出结果就是目标节点不就行了。但xpath学起来有点难度(懒),所以使用下面的方法。(但后来发现有些懒是偷不了的,于是更新下xpath定位使用方法。。在xpath表达式定位出的节点中根据xml节点数据结构做过滤,找到期望的content 代码如下:</p> <p>xmlNodePtr FindTargetNode(xmlNodePtr cur , xmlChar *target_content , FILE* output )</p> <p>{</p> <p>if(!cur)</p> <p>return NULL;</p> <p>xmlNodePtr link_node = cur->children;</p> <p>int element_count = xmlChildElementCount(cur);</p> <p>int i=0;</p> <p>fprintf(output, "%s , element_cout %d \n " , __FUNCTION__ , element_count);</p> <p>while(i < element_count &&</p> <p>link_node != NULL)</p> <p>{</p> <p>if(link_node->type == XML_ELEMENT_NODE)</p> <p>{</p> <p>i++;</p> <p>xmlChar *node_content = xmlNodeGetContent(link_node);</p> <p>fprintf(output, "Pos %d ,Node name %s , content %s \n",</p> <p>i , (char *)link_node->name , (char *)node_content);</p> <p>if(!xmlStrcmp(node_content , target_content))</p> <p>{</p> <p>xmlFree(node_content);</p> <p>return link_node;</p> <p>}</p> <p>}</p> <p>link_node = link_node->next;</p> <p>}</p> <p>return NULL;</p> <p>}</p> <p>将上文中的代码片插入xpath1.c中print_xpath_nodes()函数的这个位置:</p> <p>运行结果如下:</p> <p>在xml文件中删除节点</p> <p>使用API如下;</p> <p>void xmlUnlinkNode (xmlNodePtr cur)</p> <p>/*Unlink a node from its tree. The node is not freed. Unless it is reinserted,</p> <p>it must be managed manually and freed eventually by calling xmlFreeNode.*/</p> <p>void xmlFreeNode (xmlNodePtr cur)</p> <p>/*Free a node including all the children. This doesn't unlink the node from the tree*/</p> <p>这个API会把xml整个节点删除掉,网上有的资料说也可以使用xmlNodeSetContent ()函数,但试了下好像不行,只能删掉当前节点。 代码如下:</p> <p>xmlUnlinkNode(cur);</p> <p>xmlFreeNode(cur);</p> <p>将上文中的代码片插入xpath1.c中print_xpath_nodes()函数的这个位置: 运行结果如下;</p> <p>拓展</p> <p>对于xml文件我们知道,在解析后是以树形(多叉树(或通用树)的链表实现形式)构建的相关信息,在实际开发中发现,如果有树有多子节点,如果直接使用递归遍历较为繁琐,于是学习了一下怎么使用xpath定位,整理记录如下(如果有条件的话建议直接问chatGPT,讲的比我清楚多了,hh):</p> <p>XML节点介绍</p> <p>关于xpath条件过滤请参照此链接。基于本文中的示例xml文件,可以看到里面有节点文本值,也有节点属性值。我们使用xpath的时候也可以分别或者融合二者进行条件过滤。</p> <p>注意,如果XML文件带命名空间,需要在xpath的每个节点前加上命名空间的prefix</p> <p>使用节点过滤</p> <p>比如我们想过滤bookstore中book的price是11.99的作者的第一个名字,我们发现11.99是price节点的文本值,因此我们使用节点值过滤,节点值过滤的一般形式是//element[text()='value'],</p> <p>element:元素的标签名称。text():元素的属性名称。</p> <p>如果是节点本身则可以直接使用text(),不然换成你希望的元素名 'value':属性的值。务必注意vlaue要加上'和'这代表这我们希望libxml2是按照字符串去查找匹配的,如果不加则代表希望匹配的值是数值,libxml2会把原来的节点值转换为数值再去和value匹配</p> <p>所以组合如下:</p> <p>xml文件带命名空间(假设我们注册为xmlns),下面二者是等价的</p> <p>/xmlns:bookstore/xmlns:book[xmlns:price='11.99']/xmlns:author/xmlns:first-name</p> <p>在进行节点值过滤时,如果使用节点名称则节点名称需要加上命名空间前缀 //xmlns:book[text()='11.99']/xmlns:author/xmlns:first-name</p> <p>在进行节点值过滤时,如果使用的是text()函数则不需要为text()函数添加命名空间前缀 xml文件不带命名空间</p> <p>/bookstore/book[price='11.99']/author/first-name</p> <p>编译运行:</p> <p>使用属性过滤</p> <p>又比如我们想过滤bookstore中book style为other的title,我们发现style是book节点的属性,而othrer是style属性的值,因此我们使用属性值过滤,属性值过滤的一般形式是//element[@attribute='value'],</p> <p>element:元素的标签名称。@attribute:元素的属性名称。'value':属性的值。务必注意vlaue要加上'和'这代表这我们希望libxml2是按照字符串去查找匹配的,如果不加则代表希望匹配的值是数值,libxml2会把原来的节点值转换为数值再去和value匹配</p> <p>所以组合如下:</p> <p>xml文件带命名空间(假设我们注册为xmlns)</p> <p>/xmlns:bookstore/xmlns:book[@style='other']/xmlns:title</p> <p>注意: 在xpath中使用属性值过滤时,属性不需要加命名空间 xml文件不带命名空间</p> <p>/bookstore/book[@style='other']/title 编译运行:</p> <p>使用组合过滤</p> <p>现在让我们调整一下xml如下:</p> <p><?xml version="1.0" encoding="UTF-8"?></p> <p><bookstore xmlns="urn:newbooks-schema"></p> <p><book genre="novel" style="hardcover"></p> <p><title>The Handmaid's Tale

hello-word

Atwood

19.95

The Poisonwood Bible

hello-word

Kingsolver

11.99

The Poisonwood Bible 2

hello-word

Kingsolver

25

假设我们想过滤出bookstore中book style为other,且price为25的书籍,可使用属性+节点过滤的组合过滤的方式,一般的组合过滤方式主要是两种:

单个谓词(逻辑运算符组合条件)

使用 and 或 or 逻辑运算符,将 属性过滤 和 节点值过滤 写在同一个谓词中。即: //element[@attribute='value' and text()='value'](表示与逻辑) //element[@attribute='value' or text()='value'](表示或逻辑) 多个谓词(依次过滤):

使用多个 [条件] 谓词,每个谓词用于单独的过滤条件。 //element[@attribute='value'][text()='value']

组合如下:

xml文件带命名空间(假设我们注册为xmlns)

/xmlns:bookstore/xmlns:book[@style='other' and xmlns:price='25']/xmlns:title/xmlns:bookstore/xmlns:book[@style='other'][xmlns:price='25']/xmlns:title上面二者是等价的 xml文件不带命名空间

/bookstore/book[@style='other' and price='11.99']/title

编译运行:

据说80%的API可以完成90%的工作,关于更高级的xpath使用方法建议去参照xpath的RFC介绍,基于本人实际应用场景,这些已经足够了,下次等遇到更复杂的场景也许会再来更新(如果不懒的话)。ps: 也许也不会了,毕竟现在大趋势是使用json格式,xml格式用的地方越来越少了