libxml2是一个用于XML解析的开发工具包,提供C语言接口。这里简单记录使用libxml2进行XML数据生成、解析及使用XPath语法进行节点选取的基本操作。
本文操作系统环境为 CentOS Linux release 7.5.1804,libxml2版本为libxml2-2.9.1-6。
简介 XML(可扩展标记语言)的定义及其结构和结构名称可以参考维基百科 。这里简单关注几个与后文xmlNode
结构有关的概念:
标记(tag) XML既然是一种标记语言,肯定会有标记的概念,这里可以不严谨的理解为<
符号开始与>
符号结束的一段文本,比如
<section>
是开始标记
</section>
是结束标记
<section/>
是空元素标记
内容(content) XML中标记之外的部分就是内容。在libxml2中内容与元素一样都对应一个xmlNode
,只是其type为XML_TEXT_NODE
,其content
成员保存了内容字符串,而且其作为外层元素xmlNode
的子节点存在。
元素(element) 元素是一个逻辑概念,一种是开始标记与对应的结束标记及其中间的所有内容和其他子元素,另一种是一个空元素标记作为一个元素存在。在libxml2中一个元素对应一个xmlNode
,其type为XML_ELEMENT_NODE
。
属性(attribute) 属性是元素的内部结构,以键值对存在。在libxml2中对应一个xmlAttr
结构。xmlNode
中的属性由其成员properties
链接管理。
libxml2项目官网为http://xmlsoft.org/index.html 。
PS:我能说github上拉的源码缩进简直反人类么。
可以使用命令yum install libxml2-devel
安装rpm包及依赖。/usr/bin/xml2-config
命令指示了编译需要的相关参数,比如为了查找头文件需要编译参数-I/usr/include/libxml2
,链接需要参数-lxml2
。
数据结构 xmlDoc
对于XML数据文档的整体使用xmlDoc
结构体类型表示,这个结构体的内部成员可以暂时无视,使用api进行操作是正确的访问方式。更多时候使用xmlDocPtr
类型代表xmlDoc
的指针,定义为typedef xmlDoc *xmlDocPtr;
。
xmlChar
libxml2中字符使用使用xmlChar
类型,其实就是unsigned char
,那么字符串就是xmlChar *
了,对于char *
转换为xmlChar *
提供了宏BAD_CAST
,不过无视这个宏而使用直接的强制类型转换也没什么不好,需要注意的是应该全局使用UTF-8编码。我不想考虑非UTF-8编码的情况了,不要给自己创造麻烦。
xmlAttr
XML中属性使用xmlAttr
结构表示,该结构由元素xmlNode
成员properties
引用。xmlAttr
结构的前面部分与xmlNode
一致,在XPath返回的结果中,xmlAttr
指针会被强制转换为xmlNode
指针,可以使用type字段区分具体的类型。
xmlAttr 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _xmlAttr xmlAttr ;typedef xmlAttr *xmlAttrPtr;struct _xmlAttr { void *_private; xmlElementType type; const xmlChar *name; struct _xmlNode *children ; struct _xmlNode *last ; struct _xmlNode *parent ; struct _xmlAttr *next ; struct _xmlAttr *prev ; struct _xmlDoc *doc ; xmlNs *ns; xmlAttributeType atype; void *psvi; };
type 固定为XML_ATTRIBUTE_NODE。
name 属性的key
childrenxmlNode
结构,type
为XML_TEXT_NODE
,content
成员保存了属性的value。
next、prev 用于链接同一个元素节点的其他属性。
xmlNode
每个元素(element)和内容(content)使用xmlNode
结构体类型表示,同样有一个xmlNodePtr
代表其指针。
xmlNode 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef struct _xmlNode xmlNode ;typedef xmlNode *xmlNodePtr;struct _xmlNode { void *_private; xmlElementType type; const xmlChar *name; struct _xmlNode *children ; struct _xmlNode *last ; struct _xmlNode *parent ; struct _xmlNode *next ; struct _xmlNode *prev ; struct _xmlDoc *doc ; xmlNs *ns; xmlChar *content; struct _xmlAttr *properties ; xmlNs *nsDef; void *psvi; unsigned short line; unsigned short extra; };
typexmlNode
的节点类型,枚举类型,值比较多,可以参考官网文档 。这里主要关注三种类型
XML_ELEMENT_NODE
对应XML结构中的元素。
XML_TEXT_NODE
对应XML中的内容。
XML_ATTRIBUTE_NODE
如果是该类型,说明该xmlNode
实际是xmlAttr
。
childrenxmlNode
的子节点。
next、prev 用于链接同一个父节点的其他xmlNode
。
name 如果是XML_ELEMENT_NODE
元素类型,name就是元素名字。如果是XML_TEXT_NODE
XML内容类型,name就是固定字符串text。
content 如果是XML_TEXT_NODE
XML内容类型,content保存了其内容字符串。
以下面例子中生成的XML文档 为例,xmlNode
中各成员组织关系如下图:
xmlXPathObject
XPath查询结果结构体,xmlXPathEval
函数返回其指针类型xmlXPathObjectPtr
。
type 标识了其内部有效的具体成员,类型比较多,可以参考官网文档 。这里介绍几种:
XPATH_NODESET
xmlXPathEval
返回后总是XPATH_NODESET
类型,标识其成员nodesetval
中保存了符合查询条件的xmlNode
节点指针数组,如果查询的是属性,xmlAttr
指针也会保存于该xmlNode
节点指针数组中。这个数组仅保存符合条件的节点的指针,指向了xmlDoc
中的具体数据结构,而不是重新分配的内存,因此不要尝试修改其内容。如果没有匹配的节点,返回值的nodesetval
会为NULL
,需要判断该情况。
XPATH_BOOLEAN
、XPATH_NUMBER
、XPATH_STRING
由于xmlXPathEval
并不会返回该类型的结果,因此意义不大。可以通过对XPATH_NODESET
类型的结果调用函数xmlXPathConvertString
将nodesetval
中的第一个xmlNode
的内容字符串存储于stringval
成员,其他类型同理。
nodesetval 该结构体的主要成员,内部数组成员nodeTab
保存了查找到的符合条件的每个节点指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 typedef struct _xmlNodeSet xmlNodeSet ;typedef xmlNodeSet *xmlNodeSetPtr;struct _xmlNodeSet { int nodeNr; int nodeMax; xmlNodePtr *nodeTab; }; typedef enum { XPATH_UNDEFINED = 0 , XPATH_NODESET = 1 , XPATH_BOOLEAN = 2 , XPATH_NUMBER = 3 , XPATH_STRING = 4 , XPATH_POINT = 5 , XPATH_RANGE = 6 , XPATH_LOCATIONSET = 7 , XPATH_USERS = 8 , XPATH_XSLT_TREE = 9 } xmlXPathObjectType; typedef struct _xmlXPathObject xmlXPathObject ;typedef xmlXPathObject *xmlXPathObjectPtr;struct _xmlXPathObject { xmlXPathObjectType type; xmlNodeSetPtr nodesetval; int boolval; double floatval; xmlChar *stringval; void *user; int index; void *user2; int index2; };
使用例子 以下记录了几种简单使用例子。更多使用方式可以参考官网tutorial 以及API ,本文使用的api主要为tree 、xpath 、parser 三部分。
PS:生产环境需要更细致的错误检查。libxml2使用时需要注意及时释放不再访问的资源以防内存泄漏。
生成XML 这里以libvirt可以接受的network格式为例,生成XML格式字符串。调用的主要函数为:
xmlNewDoc
创建一个xmlDoc结构并返回指针。
xmlNewNode
创建一个xmlNode
结构并返回指针,type
为XML_ELEMENT_NODE
。
xmlDocSetRootElement
将一个xmlNode
设置为xmlDoc
的根节点。
xmlNewText
创建一个xmlNode
结构,type
为XML_TEXT_NODE
,content
为参数字符串。
xmlAddChild
将一个xmlNode
做为另一个的子元素插入。
xmlNewProp
创建一个以参数作为键值对的xmlAttr
并插入到一个xmlNode
中作为属性。
xmlDocDumpFormatMemory
将xmlDoc
以格式化XML文本的形式输出内存字符串。
xmlSaveFormatFile
将xmlDoc
以格式化XML文本的形式输出到文件。
create.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <stdio.h> #include <stdlib.h> #include <libxml/parser.h> void create_doc (char *file) { xmlDocPtr doc; xmlNodePtr root; xmlNodePtr node; xmlNodePtr node2; xmlChar *buf; int len; doc = xmlNewDoc(NULL ); root = xmlNewNode(NULL , BAD_CAST("network" )); xmlDocSetRootElement(doc, root); node = xmlNewNode(NULL , BAD_CAST("name" )); node2 = xmlNewText(BAD_CAST("s-engine-net" )); xmlAddChild(node, node2); xmlAddChild(root, node); node = xmlNewNode(NULL , BAD_CAST("bridge" )); xmlNewProp(node, BAD_CAST("name" ), BAD_CAST("s-engine-br0" )); xmlNewProp(node, BAD_CAST("stp" ), BAD_CAST("on" )); xmlNewProp(node, BAD_CAST("delay" ), BAD_CAST("0" )); xmlAddChild(root, node); node = xmlNewNode(NULL , BAD_CAST("ip" )); xmlNewProp(node, BAD_CAST("address" ), BAD_CAST("172.16.0.1" )); xmlNewProp(node, BAD_CAST("netmask" ), BAD_CAST("255.255.0.0" )); xmlAddChild(root, node); node2 = xmlNewNode(NULL , BAD_CAST("dhcp" )); xmlAddChild(node, node2); node = xmlNewNode(NULL , BAD_CAST("range" )); xmlNewProp(node, BAD_CAST("start" ), BAD_CAST("172.16.100.100" )); xmlNewProp(node, BAD_CAST("end" ), BAD_CAST("172.16.100.254" )); xmlAddChild(node2, node); xmlDocDumpFormatMemory(doc, &buf, &len, 1 ); if (len > 0 ) { printf ("%s\n" , buf); free (buf); } if (xmlSaveFormatFile(file, doc, 1 ) == -1 ) { fprintf (stderr , "failed to save file \"%s\"\n." , file); } xmlFreeDoc(doc); } int main (int argc, char **argv) { if (argc < 2 ) { fprintf (stderr , "Usage: %s filename\n" , argv[0 ]); return -1 ; } create_doc(argv[1 ]); return 0 ; }
编译
gcc -Wall -g -I /usr/include/libxml2 -lxml2 create.c -o create
执行
./create tt
输出及文件内容均为
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0"?> <network > <name > s-engine-net</name > <bridge name ="s-engine-br0" stp ="on" delay ="0" /> <ip address ="172.16.0.1" netmask ="255.255.0.0" > <dhcp > <range start ="172.16.100.100" end ="172.16.100.254" /> </dhcp > </ip > </network >
解析XML 这里演示如何递归解析刚刚生成的XML文件,包括元素、属性和内容的解析。
parse.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 #include <stdio.h> #include <stdlib.h> #include <libxml/parser.h> xmlChar *bridge_name = NULL ; void parse_node (xmlNodePtr node, int space) { xmlNodePtr cur; xmlAttr *attr; if (node->type == XML_ELEMENT_NODE) { int i; printf ("\n" ); for (i = 0 ; i < space; i++) { printf (" " ); } printf ("%s" , node->name); if (xmlStrcmp(node->name, BAD_CAST("bridge" )) == 0 ) { bridge_name = xmlGetProp(node, BAD_CAST("name" )); } cur = node->children; if (cur != NULL && cur->type == XML_TEXT_NODE && cur->next == NULL && cur->prev == NULL ) printf (": \"%s\"" , cur->content); i = 0 ; attr = node->properties; while (attr != NULL ) { xmlChar *str = xmlNodeListGetString(node->doc, attr->children, 1 ); if (i == 0 ) { i = 1 ; printf ("(%s=\"%s\"" , attr->name, str); } else { printf (" %s=\"%s\"" , attr->name, str); } free (str); attr = attr->next; } if (i != 0 ) { printf (")" ); } cur = node->children; while (cur != NULL ) { parse_node(cur, space + 2 ); cur = cur->next; } } else if (node->type == XML_TEXT_NODE) { if (node->next != NULL || node->prev != NULL ) { int i; printf ("\n" ); for (i = 0 ; i < space; i++) { printf (" " ); } printf ("# USELESS CONTENT #" ); } } } void parse_doc (char *file) { xmlDocPtr doc; xmlNodePtr root; doc = xmlParseFile(file); if (doc == NULL ) { fprintf (stderr ,"failed to parse file \"%s\".\n" , file); return ; } root = xmlDocGetRootElement(doc); if (root == NULL ) { fprintf (stderr ,"empty document\n" ); xmlFreeDoc(doc); return ; } parse_node(root, 0 ); printf ("\n\n" ); if (bridge_name) { printf ("find bridge attr name: %s\n" , bridge_name); free (bridge_name); } xmlFreeDoc(doc); return ; } void test () { xmlDocPtr doc; xmlNodePtr node; xmlChar *str = BAD_CAST("<e1>c1<e2/>c2</e1>" ); doc = xmlParseDoc(str); node = xmlDocGetRootElement(doc); str = xmlNodeListGetString(doc, node->children, 1 ); printf ("test example: \"%s\"\n" , str); free (str); xmlFreeDoc(doc); } int main (int argc, char **argv) { if (argc < 2 ) { fprintf (stderr , "Usage: %s filename\n" , argv[0 ]); return -1 ; } parse_doc(argv[1 ]); test(); return 0 ; }
编译
gcc -Wall -g -I /usr/include/libxml2 -lxml2 parse.c -o parse
执行
./parse tt
输出1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 network # USELESS CONTENT # name: "s-engine-net"(k="v") # USELESS CONTENT # bridge(name="s-engine-br0" stp="on" delay="0") # USELESS CONTENT # ip(address="172.16.0.1" netmask="255.255.0.0") # USELESS CONTENT # dhcp # USELESS CONTENT # range(start="172.16.100.100" end="172.16.100.254") # USELESS CONTENT # # USELESS CONTENT # # USELESS CONTENT # find bridge attr name: s-engine-br0 test example: "c1c2"
XPath查找 XPath具体语法可以参考维基百科 。查找的到节点集合的结构关系参考前文xmlXPathObject结构体
这里主要使用的就是两个XPath相关函数:
xmlXPathNewContext
由xmlDoc
指针生成XPath的上下文。
xmlXPathEval
在XPath上下文中执行指定字符串的查询。
search.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <stdio.h> #include <stdlib.h> #include <libxml/parser.h> #include <libxml/xpath.h> static void search_doc (xmlXPathContextPtr context, char *search_string) { xmlChar *str = BAD_CAST(search_string); xmlXPathObjectPtr result; int i; printf ("search string: \"%s\"\n" , search_string); result = xmlXPathEval(str, context); if (result == NULL || result->nodesetval == NULL ) { printf (" found nothing\n" ); goto err_ret; } for (i = 0 ; i < result->nodesetval->nodeNr; i++) { xmlNodePtr node = result->nodesetval->nodeTab[i]; if (node->type == XML_ELEMENT_NODE) { xmlChar *s = xmlNodeListGetString(node->doc, node->children, 1 ); printf (" [%d] element: \"%s\" \"%s\"\n" , i, node->name, s); free (s); } else if (node->type == XML_ATTRIBUTE_NODE) { xmlChar *s = xmlNodeListGetString(node->doc, node->children, 1 ); printf (" [%d] attribute: \"%s\" \"%s\"\n" , i, node->name, s); free (s); } else { printf (" [%d] type %d: \"%s\"\n" , i, node->type, node->name); } } err_ret: if (result != NULL ) xmlXPathFreeObject(result); } int main (int argc, char **argv) { xmlDocPtr doc; xmlXPathContextPtr context; if (argc < 2 ) { printf ("Usage: %s docname\n" , argv[0 ]); return (0 ); } doc = xmlParseFile(argv[1 ]); if (doc == NULL ) { fprintf (stderr , "failed to parse file \"%s\".\n" , argv[1 ]); return -1 ; } context = xmlXPathNewContext(doc); search_doc(context, "/network[1]/ip[1]/@address[1] | /network[1]/name[1]" ); xmlXPathFreeContext(context); xmlFreeDoc(doc); xmlCleanupParser(); return (0 ); }
编译
gcc -Wall -g -I /usr/include/libxml2 -lxml2 search.c -o search
执行
./search tt
输出
1 2 3 search string: "/network[1]/ip[1]/@address[1] | /network[1]/name[1]" [0] element: "name" "s-engine-net" [1] attribute: "address" "172.16.0.1"