XML - XML学习/XML文件解析器(C++)实现
XML概述
XML是一套定义语义标记的规则,这些标记将文档分成许多部件并对这些部件加以标识。它也是元标记语言,用于定义其他与特定领域有关的,语义的,结构化的标记语言的句法语言。XML与HTML一样,都来自Standard Generalized Markup Language(标准通用标记语言,SGML)
SGML是一种用标记来描述文档资料的通用语言,它包含一系列的文档类型定义(Document Type Definition,DTD),DTD中定义了标记的含义,因而SGML的语法是可以拓展的。SGML十分庞大,不容易学习,也不容易使用,在计算机上实现也十分困难。
HTML只实现了SGML中很少的一部分标记,它的标记是固定的,语法也是不可拓展的,但是由于它语法的固定使得它简单易学,随着Web技术的发展而推广到全世界,但是由于HTML过于简单,且没有固定的格式,在发展的过程中遇到了一些问题,此时XML便应运而生。
XML的特点
- 简介有效
- XML是一个精简的SGML,它将SGML的丰富功能与HTML的易用性结合到Web应用中,保留了SGML的可拓展功能,这使得XML从根本上区别于HTML
- 易学易用
- 开放的国际化标准
- XML标准。W3C正式批准的标准,这意味着这个标准是稳定的,完全可用于Web和工具的开发
- XML名域标准。
- DOM(Document Object Model,文档对象模型)标准。为给结构化的数据编写脚本提供标准,这样,开发人员就能够与计算机在基于XML的数据上进行交互
- XSL标准。XSL有两个模块:XSL转换语言和XSL格式化对象。
- XLL标准和XML指针语言(XPointer)标准。
- 高效且可扩充
- XML支持复用文档片断,使用者可以发明和使用自己的标签,也可与他人共享,可延伸性大。
总结:XML使用一个简单而又灵活的标准格式,为基于Web的应用提供一个描述数据和交换数据的有效手段。HTML描述了显示全球数据的通用方法,而XML提供了直接处理全球数据的通用方法。
XML的作用
XML让信息的提供者可以根据需要,自行定义标记及属性名,结构化地描述信息内容
- 使得搜索更加有意义
- 开发灵活的Web应用软件
- XML的应用使得三层Web应用软件的开发更加简单,由于其能够有效地进行消息和数据标准的统一,从而设计,开发出更加灵活的Web应用软件
- 实现不同数据的集成
- 使用于多种应用环境
- 客户端数据处理与计算
- 数据显示多样化
- 局部数据更新
- 与现有Web发布机制相兼容
- 可升级性
- 压缩性能高
- XML压缩性能很好,因为用于描述数据结构的标签可以重复使用。
XML的应用
- 应用于客户需要与不同的数据源进行交互时
- 应用于将大量运算负荷分布在客户端
- 应用于将同一数据以不同的面貌展现给不同的用户
- 应用于网络代理对所取得的信息进行编辑,增减以适应个人用户的需要
解析XML
一个实用的XML文档必须满足两点,分别是 组织良好(Well-formed) 和 有效(Valid)
XML文档
一个组织良好的XML文档,需要满足以下三项基本规则:
- 文档以XML定义
<? xml version = "1.0" ?>
开始 - 有一个包含所有其他内容的根元素
- 所有元素必须合理地嵌套,不允许交叉嵌套
实例XML文件
<?xml version="1.0" encoding="UTF-8"?>
<students>
<student>
<id>202021091138</id>
<name>wzh</name>
</student>
</students>
DOM解析
文档对象模型为XML文档的解析版本定义了一组接口。解析器读入整个文档,然后构建一个驻留内存的树结构,然后代码就可以使用DOM接口来操作这个树结构。
DOM提供了一组丰富的功能,用户可以使用这些功能来解释和操作XML文档,但是DOM解释XML文档也会遇到一些挑战和问题:
- DOM构建整个文档驻留内存的树。如果文档很大时,就会要求有极大的内存,这消耗的内存往往是XML文档的10~100倍左右
- DOM创建表示原始文档中每个东西的对象,包括元素,文本,属性和空格。如果用户只关注原始文档的一小部分,那么创建那些永远不被使用的对象是极其浪费的
- DOM解析器必须在代码取得控制权之前读取整个文档。对于非常大的文档,这会引起显著的延迟
总结:除去以上的问题,DOM API是解析XML文档非常有用的方法
SAX解析
SAX接口的出现是为了解决DOM解析XML文件面临的一些问题,SAX接口有如下特征:
- SAX解析器向代码发送事件。当解析器发现元素开始,元素结束,文本,文档的开始或结束时,它会告诉用户。
- SAX解析器根本不创建任何对象,它只是将事件传递给应用程序。如果希望基于哪些事件创建对象,这将由编程者自己来完成
- SAX解析器在解析开始的时候就开始发送事件。当解析器发现文档开始,元素开始和文本时,代码会收到一个事件。应用程序可以立即开始生成结果;不必一直等到整个文档被解析完毕。
当然,SAX解析器也有一些不足点:
- SAX事件是无状态的。当SAX解析器在XML文档中发现文本时,它就向代码发送一个事件。该事件仅给用户发现的文本,它不告诉用户什么元素包含那个文本。如果想了解这一点,则用户必须自己编写状态管理代码
- SAX事件不是持久的。如果应用程序需要一个数据结构来对XML文档建模,则必须自己编写那样的代码。如果需要从SAX事件访问数据,并且没有把那个数据存储在代码中,那么用户不得不再次解析该文档
以上是解析XML文档主要使用的2种方法,DOM解析器在解析XML文档时,会把文档中的所有元素,按照其出现的层次关系,解析成一个个Node对象(节点),可以遍历和修改节点,SAX解析器逐行扫描文档,一遍扫描一遍解析。相比于DOM,SAX可以在解析文档的任意时刻停止解析解析,是一种速度更快,更高效的方法。
JDOM解析
JDOM是基于Java技术的开发源码项目,它试图遵循80/20原则:用DOM和SAX的20%的功能来满足80%的用户需求。JDOM的底层还是使用SAX和DOM解析器
JDOM的主要特征是它极大地减少了用户必须编写的代码数量,JDOM的应用程序的程度通常是DOM应用程序的1/3,大约是SAX应用程序的一半。JDOM并不做所有的事情,只满足大多数用户要做的解析需求
JAXP解析
JAXP出现是SUN公司为了弥补JAVA在XML标准制定上的空白而制定的一套JAVA XML标准API。它并没有为JAVA解析XML提供任何新功能,但是它为在JAVA获取SAX与DOM解析器提供了更加直接的途径。
它封装了sax\dom两种接口,并在sax\dom的基础之上,作了一套比较简单的api以供开发。
XML文件解析器设计与实现(C++)
造轮子!造轮子!
XML文件解析器实现代码参考来源:https://github.com/oldjun/yazi-xml
XML文件解析器用法
- 从文件加载XML
- 从字符串加载XML
- 访问节点:根据数组下标,节点名称
- 遍历节点:数组,迭代器,for in
- 添加节点
- 删除节点
- 获取,修改节点名称
- 获取,删除节点属性
- 获取,修改节点内容
XML解析器具体实现
XML文件解析器具体实现取决于xml类和Parser类,在xml.h文件中,增加了一个Value数值转化类,方便于XML节点属性的自动转化
xml.h文件
#pragma once
#include <string>
#include <list>
#include <map>
using namespace std;
namespace wzh {
namespace xml {
class Value {
// 数值转化类
public:
Value();
Value(bool value);
Value(int value);
Value(double value);
Value(const char * value);
Value(const string & value);
~Value();
Value & operator = (bool value);
Value & operator = (int value);
Value & operator = (double value);
Value & operator = (const char * value);
Value & operator = (const string & value);
Value & operator = (const Value & value);
bool operator == (const Value & other);
bool operator != (const Value & other);
operator bool();
operator int();
operator double();
operator string();
private:
string m_value;
};
class XML {
public:
XML();
XML(const char * name);
XML(const string & name);
XML(const XML & other);
string name() const; // 获取节点名称
void setName(const string & name); // 设置节点名称
string text() const; // 获取节点内容
void setText(const string & text); // 设置节点内容
Value & attr(const string & key); // 获取节点属性
void setAttr(const string & key, const Value & val); // 设置节点属性
string str() const; // 返回属性序列化结果
void clear(); // 释放内存
void append(const XML & child); // 添加子节点
XML & operator [] (int index); // 以数组下标方式获取子节点
XML & operator [] (const char * name); // 通过节点名称获取子节点(支持C形式)
XML & operator [] (const string & name); // 通过节点名称获取子节点(支持C++形式)
XML & operator = (const XML & other);
void remove(int index); // 以数组下标方式删除子节点
void remove(const char * name); // 通过节点名称删除子节点(支持C形式)
void remove(const string & name); // 通过节点名称删除子节点(支持C++形式)
typedef std::list<XML>::iterator iterator;
iterator begin() {
return m_child->begin();
}
iterator end() {
return m_child->end();
}
iterator erase(iterator it) {
it->clear();
return m_child->erase(it);
}
int size() const {
return m_child->size();
}
bool load(const string & filename);
bool save(const string & filename);
bool parse(const string & str);
private:
// 使用指针加快数据的读取以及存储开销
string* m_name; // 节点名称
string* m_text; // 节点内容
std::map<string, Value>* m_attrs; // 节点属性
std::list<XML>* m_child; // 子节点
};
}
}
xml.cpp文件
#include "xml.h"
#include "Parser.h"
using namespace wzh::xml;
#include <fstream>
#include <sstream>
using namespace std;
Value::Value() {
}
Value::Value(bool value) {
*this = value;
}
Value::Value(int value) {
*this = value;
}
Value::Value(double value) {
*this = value;
}
Value::Value(const char * value) : m_value(value) {
}
Value::Value(const string & value) : m_value(value) {
}
Value::~Value() {
}
Value & Value::operator = (bool value) {
m_value = value ? "true" : "false";
return *this;
}
Value & Value::operator = (int value) {
stringstream ss;
ss << value;
m_value = ss.str();
return *this;
}
Value & Value::operator = (double value) {
stringstream ss;
ss << value;
m_value = ss.str();
return *this;
}
Value & Value::operator = (const char * value) {
m_value = value;
return *this;
}
Value & Value::operator = (const string & value) {
m_value = value;
return *this;
}
Value & Value::operator = (const Value & value) {
m_value = value.m_value;
return *this;
}
bool Value::operator == (const Value & other) {
return m_value == other.m_value;
}
bool Value::operator != (const Value & other) {
return !(m_value == other.m_value);
}
Value::operator bool() {
if (m_value == "true")
return true;
else if (m_value == "false")
return false;
return false;
}
Value::operator int() {
return std::atoi(m_value.c_str());
}
Value::operator double() {
return std::atof(m_value.c_str());
}
Value::operator string() {
return m_value;
}
XML::XML() : m_name(nullptr), m_text(nullptr), m_attrs(nullptr), m_child(nullptr) {
}
XML::XML(const char * name) : m_name(nullptr), m_text(nullptr), m_attrs(nullptr), m_child(nullptr) {
m_name = new string(name);
}
XML::XML(const string & name) : m_name(nullptr), m_text(nullptr), m_attrs(nullptr), m_child(nullptr) {
m_name = new string(name);
}
XML::XML(const XML & other) {
m_name = other.m_name;
m_text = other.m_text;
m_attrs = other.m_attrs;
m_child = other.m_child;
}
string XML::name() const {
if(m_name == nullptr) {
return "";
}
return *m_name;
}
void XML::setName(const string & name) {
if(m_name != nullptr) {
delete m_name;
m_name = nullptr;
}
m_name = new string(name);
}
string XML::text() const {
if(m_text == nullptr) {
return "";
}
return *m_text;
}
void XML::setText(const string & text) {
if(m_text != nullptr) {
delete m_text;
m_text = nullptr;
}
m_text = new string(text);
}
Value & XML::attr(const string & key) {
if(m_attrs == nullptr) {
m_attrs = new std::map<string, Value>();
}
return (*m_attrs)[key];
}
void XML::setAttr(const string & key, const Value & val) {
if(m_attrs == nullptr) {
m_attrs = new std::map<string, Value>();
}
(*m_attrs)[key] = val;
}
string XML::str() const {
if(m_name == nullptr) {
throw std::logic_error("element name is empty");
}
stringstream ss;
ss << "<";
ss << *m_name;
if(m_attrs != nullptr) {
for(auto it = m_attrs->begin(); it != m_attrs->end(); it++) {
ss << " " << it->first << "=\"" << (string)it->second << "\"";
}
}
ss << ">";
if(m_child != nullptr) {
for(auto it = m_child->begin(); it != m_child->end(); it++) {
ss << it->str();
}
}
if(m_text != nullptr) {
ss << *m_text;
}
ss << "</" << *m_name << ">";
return ss.str();
}
void XML::clear() {
if(m_name != nullptr) {
delete m_name;
m_name = nullptr;
}
if(m_text != nullptr) {
delete m_text;
m_text = nullptr;
}
if(m_attrs != nullptr) {
delete m_attrs;
m_attrs = nullptr;
}
if(m_child != nullptr) {
for(auto it = m_child->begin(); it != m_child->end(); it++) {
it -> clear();
}
delete m_child;
m_child = nullptr;
}
}
void XML::append(const XML & child) {
if(m_child == nullptr) {
m_child = new std::list<XML>();
}
m_child->push_back(child);
}
XML & XML::operator [] (int index) {
if(index < 0) {
throw std::logic_error("index less than zero");
}
if(m_child == nullptr) {
m_child = new std::list<XML>();
}
int size = m_child->size();
if(index >= 0 && index < size) {
auto it = m_child->begin();
for(int i=0; i<index; i++)
it++;
return *it;
}
if (index >= size) {
for(int i=size; i<=index; i++)
m_child->push_back(XML());
}
return m_child->back();
}
XML & XML::operator [] (const char * name) {
return (*this)[string(name)];
}
XML & XML::operator [] (const string & name) {
if(m_child == nullptr) {
m_child = new std::list<XML>();
}
for(auto it = m_child->begin(); it != m_child->end(); it++) {
if(it->name() == name) {
return *it;
}
}
m_child->push_back(XML(name));
return m_child->back();
}
XML & XML::operator = (const XML & other) {
clear();
m_name = other.m_name;
m_text = other.m_text;
m_attrs = other.m_attrs;
m_child = other.m_child;
return *this;
}
void XML::remove(int index) {
if(m_child == nullptr) {
return;
}
int size = m_child->size();
if(index < 0 || index >= size) {
throw std::logic_error("index out of range");
return;
}
auto it = m_child->begin();
for(int i=0; i<index; i++) {
it++;
}
it->clear();
m_child->erase(it);
}
void XML::remove(const char * name) {
remove(string(name));
}
void XML::remove(const string & name) {
if(m_child == nullptr) {
return;
}
for(auto it = m_child->begin(); it != m_child->end();) {
if(it->name() == name) {
it->clear();
it = m_child->erase(it);
}
else {
it++;
}
}
}
bool XML::load(const string & filename)
{
Parser p;
if (!p.load_file(filename))
{
return false;
}
*this = p.parse();
return true;
}
bool XML::save(const string & filename)
{
ofstream fout(filename);
if (fout.fail())
{
return false;
}
fout << str();
fout.close();
return true;
}
bool XML::parse(const string & str)
{
Parser p;
if (!p.load_string(str))
{
return false;
}
*this = p.parse();
return true;
}
Parser.h文件
#pragma once
#include "xml.h"
#include <string>
using namespace std;
namespace wzh {
namespace xml{
class Parser {
public:
Parser();
bool load_file(const string & file);
bool load_string(const string & str);
XML parse(); // 解析方法
private:
void skip_white_space(); // 忽略空白符
bool parse_declaration(); // 解析声明
bool parse_comment(); // 解析注释
XML parse_element(); // 解析主函数
string parse_element_name(); // 解析节点名称
string parse_element_text(); // 解析节点文本
string parse_element_attr_key(); // 解析节点属性
string parse_element_attr_val(); // 解析节点属性值
private:
string m_str;
int m_idx;
};
}
}
Parser.cpp文件
#include "Parser.h"
using namespace wzh::xml;
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;
Parser::Parser() : m_str(""), m_idx(0) {
}
bool Parser::load_string(const string & str) {
m_str = str;
m_idx = 0;
return true;
}
bool Parser::load_file(const string & filename) {
ifstream fin(filename);
if(fin.fail()) {
return false;
}
stringstream ss;
ss << fin.rdbuf();
m_str = ss.str();
m_idx = 0;
return true;
}
void Parser::skip_white_space() {
while(m_str[m_idx] == ' ' || m_str[m_idx] == '\r' || m_str[m_idx] == '\n' || m_str[m_idx] == '\t') {
m_idx++;
}
}
XML Parser::parse() {
skip_white_space();
if(m_str.compare(m_idx, 5, "<?xml") == 0) {
if(!parse_declaration()) {
throw std::logic_error("parse declaration error");
}
}
skip_white_space();
while(m_str.compare(m_idx, 4, "<!--") == 0) {
if(!parse_comment()) {
throw std::logic_error("parse comment error");
}
skip_white_space();
}
if (m_str[m_idx] == '<' && (isalpha(m_str[m_idx+1]) || m_str[m_idx+1] == '_')) {
return parse_element();
}
throw std::logic_error("parse element error");
}
bool Parser::parse_declaration() {
if(m_str.compare(m_idx, 5, "<?xml") != 0) {
return false;
}
m_idx += 5;
size_t pos = m_str.find("?>", m_idx);
if(pos == std::string::npos) {
return false;
}
m_idx = pos + 2;
return true;
}
bool Parser::parse_comment() {
if(m_str.compare(m_idx, 4, "<!--") != 0) {
return false;
}
m_idx += 4;
size_t pos = m_str.find("-->", m_idx);
if(pos == std::string::npos) {
return false;
}
m_idx = pos + 3;
return true;
}
XML Parser::parse_element() {
XML elem;
m_idx++;
skip_white_space();
const string & name = parse_element_name();
elem.setName(name);
while(m_str[m_idx] != '\0') {
skip_white_space();
if(m_str[m_idx] == '/') {
if(m_str[m_idx+1] == '>') {
m_idx += 2;
break;
}
else {
throw logic_error("xml empty element is error");
}
}
else if(m_str[m_idx] == '>') {
m_idx++;
string text = parse_element_text();
if(text != "") {
elem.setText(text);
}
}
else if(m_str[m_idx] == '<') {
if(m_str[m_idx+1] == '/') {
// 标签
string end_tag = "</" + name + ">";
size_t pos = m_str.find(end_tag, m_idx);
if (pos == std::string::npos) {
throw std::logic_error("xml element " + name + " end tag not found");
}
m_idx = (pos + end_tag.size());
break;
}
else if (m_str.compare(m_idx, 4, "<!--") == 0) {
// 注释
if (!parse_comment()) {
throw std::logic_error("xml comment is error");
}
}
else {
// 子节点
elem.append(parse_element());
}
}
else {
// 节点属性
string key = parse_element_attr_key();
skip_white_space();
if (m_str[m_idx] != '=')
{
throw std::logic_error("xml element attr is error" + key);
}
m_idx++;
skip_white_space();
string val = parse_element_attr_val();
elem.setAttr(key, val);
}
}
return elem;
}
string Parser::parse_element_name() {
int pos = m_idx;
if (isalpha(m_str[m_idx]) || (m_str[m_idx] == '_')) {
m_idx++;
while (isalnum(m_str[m_idx]) || (m_str[m_idx] == '_') || (m_str[m_idx] == '-') || (m_str[m_idx] == ':') || (m_str[m_idx] == '.')) {
m_idx++;
}
}
return m_str.substr(pos, m_idx - pos);
}
string Parser::parse_element_text() {
int pos = m_idx;
while (m_str[m_idx] != '<') {
m_idx++;
}
return m_str.substr(pos, m_idx - pos);
}
string Parser::parse_element_attr_key() {
int pos = m_idx;
if (isalpha(m_str[m_idx]) || (m_str[m_idx] == '_')) {
m_idx++;
while (isalnum(m_str[m_idx]) || (m_str[m_idx] == '_') || (m_str[m_idx] == '-') || (m_str[m_idx] == ':')) {
m_idx++;
}
}
return m_str.substr(pos, m_idx - pos);
}
string Parser::parse_element_attr_val() {
if (m_str[m_idx] != '"') {
throw std::logic_error("xml element attr value should be in double quotes");
}
m_idx++;
int pos = m_idx;
while (m_str[m_idx] != '"') {
m_idx++;
}
m_idx++;
return m_str.substr(pos, m_idx - pos - 1);
}
Makefile
#指定编译器
CC = g++
#找出当前目录下,所有的源文件(以.cpp结尾)
SRCS := $(shell find ./* -type f | grep '\.cpp' | grep -v 'main\.cpp')
$(warning SRCS is ${SRCS})
#确定cpp源文件对应的目标文件
OBJS := $(patsubst %.cpp, %.o, $(filter %.cpp, $(SRCS)))
$(warning OBJS is ${OBJS})
#编译选项
CFLAGS = -g -O2 -Wall -Werror -Wno-unused -ldl -fPIC -std=c++11
$(warning CFLAGS is ${CFLAGS})
#找出当前目录下所有的头文件
INCLUDE_TEMP = $(shell find ./* -type d)
INCLUDE = $(patsubst %,-I %, $(INCLUDE_TEMP))
$(warning INCLUDE is ${INCLUDE})
SRC_MAIN = main.cpp
OBJ_MAIN = ${SRC_MAIN:%.cpp=%.o}
EXE_MAIN = main
target: ${EXE_MAIN}
$(EXE_MAIN): $(OBJ_MAIN) $(OBJS)
$(CC) -o $@ $^ $(CFLAGS) $(INCLUDE)
clean:
rm -f ${OBJS} ${OBJ_MAIN} ${EXE_MAIN}
%.o: %.cpp
${CC} ${CFLAGS} ${INCLUDE} -c $< -o $@
附上xml测试文件
test1.xml(Maven配置文件)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.item</groupId>
<artifactId>servlet_mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<name>servlet_mybatis</name>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.version>5.7.1</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
test2.xml
<?xml version="1.0" encoding="UTF-8"?>
<students>
<student id="1" class="101">
<name>Jack</name>
<age>21</age>
<gender>male</gender>
</student>
<student id="2" class="102">
<name>Lucy</name>
<age>22</age>
<gender>female</gender>
</student>
<student id="3" class="103">
<name>Lily</name>
<age>23</age>
<gender>female</gender>
</student>
</students>
测试结果如下: