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