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
/**
* xmlAttr:
*
* An attribute on an XML node.
*/
typedef struct _xmlAttr xmlAttr;
typedef xmlAttr *xmlAttrPtr;
struct _xmlAttr {
void *_private; /* application data */
xmlElementType type; /* XML_ATTRIBUTE_NODE, must be second ! */
const xmlChar *name; /* the name of the property */
struct _xmlNode *children; /* the value of the property */
struct _xmlNode *last; /* NULL */
struct _xmlNode *parent; /* child->parent link */
struct _xmlAttr *next; /* next sibling link */
struct _xmlAttr *prev; /* previous sibling link */
struct _xmlDoc *doc; /* the containing document */
xmlNs *ns; /* pointer to the associated namespace */
xmlAttributeType atype; /* the attribute type if validating */
void *psvi; /* for type/PSVI informations */
};
  • type
    固定为XML_ATTRIBUTE_NODE。
  • name
    属性的key
  • children
    xmlNode结构,typeXML_TEXT_NODEcontent成员保存了属性的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
/**
* xmlNode:
*
* A node in an XML tree.
*/
typedef struct _xmlNode xmlNode;
typedef xmlNode *xmlNodePtr;
struct _xmlNode {
void *_private; /* application data */
xmlElementType type; /* type number, must be second ! */
const xmlChar *name; /* the name of the node, or the entity */
struct _xmlNode *children; /* parent->childs link */
struct _xmlNode *last; /* last child link */
struct _xmlNode *parent; /* child->parent link */
struct _xmlNode *next; /* next sibling link */
struct _xmlNode *prev; /* previous sibling link */
struct _xmlDoc *doc; /* the containing document */

/* End of common part */
xmlNs *ns; /* pointer to the associated namespace */
xmlChar *content; /* the content */
struct _xmlAttr *properties;/* properties list */
xmlNs *nsDef; /* namespace definitions on this node */
void *psvi; /* for type/PSVI informations */
unsigned short line; /* line number */
unsigned short extra; /* extra data for XPath/XSLT */
};
  • type
    xmlNode的节点类型,枚举类型,值比较多,可以参考官网文档。这里主要关注三种类型
    • XML_ELEMENT_NODE
      对应XML结构中的元素。
    • XML_TEXT_NODE
      对应XML中的内容。
    • XML_ATTRIBUTE_NODE
      如果是该类型,说明该xmlNode实际是xmlAttr
  • children
    xmlNode的子节点。
  • next、prev
    用于链接同一个父节点的其他xmlNode
  • name
    如果是XML_ELEMENT_NODE元素类型,name就是元素名字。如果是XML_TEXT_NODEXML内容类型,name就是固定字符串text。
  • content
    如果是XML_TEXT_NODEXML内容类型,content保存了其内容字符串。

以下面例子中生成的XML文档为例,xmlNode中各成员组织关系如下图:

xmlNode成员组织关系

xmlXPathObject

XPath查询结果结构体,xmlXPathEval函数返回其指针类型xmlXPathObjectPtr

  • type
    标识了其内部有效的具体成员,类型比较多,可以参考官网文档。这里介绍几种:
    • XPATH_NODESET
      xmlXPathEval返回后总是XPATH_NODESET类型,标识其成员nodesetval中保存了符合查询条件的xmlNode节点指针数组,如果查询的是属性,xmlAttr指针也会保存于该xmlNode节点指针数组中。这个数组仅保存符合条件的节点的指针,指向了xmlDoc中的具体数据结构,而不是重新分配的内存,因此不要尝试修改其内容。如果没有匹配的节点,返回值的nodesetval会为NULL,需要判断该情况。
    • XPATH_BOOLEANXPATH_NUMBERXPATH_STRING
      由于xmlXPathEval并不会返回该类型的结果,因此意义不大。可以通过对XPATH_NODESET类型的结果调用函数xmlXPathConvertStringnodesetval中的第一个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
/*
* A node-set (an unordered collection of nodes without duplicates).
*/
typedef struct _xmlNodeSet xmlNodeSet;
typedef xmlNodeSet *xmlNodeSetPtr;
struct _xmlNodeSet {
int nodeNr; /* number of nodes in the set */
int nodeMax; /* size of the array as allocated */
xmlNodePtr *nodeTab; /* array of nodes in no particular order */
/* @@ with_ns to check wether namespace nodes should be looked at @@ */
};

/*
* An expression is evaluated to yield an object, which
* has one of the following four basic types:
* - node-set
* - boolean
* - number
* - string
*
* @@ XPointer will add more types !
*/

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 /* An XSLT value tree, non modifiable */
} 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主要为treexpathparser三部分。

PS:生产环境需要更细致的错误检查。libxml2使用时需要注意及时释放不再访问的资源以防内存泄漏。

生成XML

这里以libvirt可以接受的network格式为例,生成XML格式字符串。调用的主要函数为:

  • xmlNewDoc
    创建一个xmlDoc结构并返回指针。
  • xmlNewNode
    创建一个xmlNode结构并返回指针,typeXML_ELEMENT_NODE
  • xmlDocSetRootElement
    将一个xmlNode设置为xmlDoc的根节点。
  • xmlNewText
    创建一个xmlNode结构,typeXML_TEXT_NODEcontent为参数字符串。
  • 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;

/* new doc */
doc = xmlNewDoc(NULL);
root = xmlNewNode(NULL, BAD_CAST("network"));
xmlDocSetRootElement(doc, root);

/* 加入name */
node = xmlNewNode(NULL, BAD_CAST("name"));
node2 = xmlNewText(BAD_CAST("s-engine-net"));
xmlAddChild(node, node2);
xmlAddChild(root, node);

/* 加入bridge */
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);

/* 加入ip */
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);

/* 例子:查找attr */
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) {
/*
* attr->children是一个xmlNode,文本类型,这个函数取该xmlNode的content
* 后面会有更清晰的例子
* */
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) {
/* 例子,XML格式化造成的多余content */
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");

/* 查找attr的结果 */
if (bridge_name) {
printf("find bridge attr name: %s\n", bridge_name);
free(bridge_name);
}

xmlFreeDoc(doc);
return;
}

/* xmlNodeListGetString,可以看到返回了"c1"和"c2"两个字符串拼接的结果 */
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]");

/* 这个用于libvirt搜索domain第一个网卡MAC地址 */
/*
search_doc(context, "/domain[1]/devices[1]/interface[1]/mac[1]/@address[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"