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;。
xmlCharlibxml2中字符使用使用xmlChar类型,其实就是unsigned char,那么字符串就是xmlChar *了,对于char *转换为xmlChar *提供了宏BAD_CAST,不过无视这个宏而使用直接的强制类型转换也没什么不好,需要注意的是应该全局使用UTF-8编码。我不想考虑非UTF-8编码的情况了,不要给自己创造麻烦。
xmlAttrXML中属性使用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_NODEXML内容类型,name就是固定字符串text。
content 如果是XML_TEXT_NODEXML内容类型,content保存了其内容字符串。
以下面例子中生成的XML文档 为例,xmlNode中各成员组织关系如下图:
xmlXPathObjectXPath查询结果结构体,xmlXPathEval函数返回其指针类型xmlXPathObjectPtr。
type 标识了其内部有效的具体成员,类型比较多,可以参考官网文档 。这里介绍几种:
XPATH_NODESETxmlXPathEval返回后总是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"