1. 引言
想象一下,一个巨大的 XML 被某个 SOAP 服务或银行 API 丢给你。里面有成百上千层嵌套、结构不统一,而你只需要找到上个月某笔支付的唯一金额。把它先序列化成对象的方法不适合 —— 太笨重,而且事先不知道该映射到哪个类。你需要一把“放大镜”看 XML 树,和快速在树上跑的能力。
为这类场景,.NET 已经有很久的武器 —— 类 XmlDocument,配合查询语言 XPath。今天你会学到:
- 把 XML 加载到 DOM 树里;
- 用手工遍历和 XPath 快速定位元素;
- 修改 XML 文档内容;
- 添加、删除和修改节点;
- 用 XPath 做复杂选择。
2. 什么是 XmlDocument?
XmlDocument 是来自命名空间 System.Xml 的类。它实现了 DOM(Document Object Model)—— 把整个 XML 文件表示成内存中的对象树。
一点理论和类比
如果把 XmlSerializer 比作把对象拆成一堆 LEGO 积木再拼回来,XmlDocument 更像直接操作一棵真实的树:有根、分支(元素)、叶子(文本节点),你可以在树上走动、加分支、剪叶子、把它们移到别处。
简单加载一个 XML 文档
假设我们有下面这个 XML:
<users>
<user id="1">
<name>奥尔加</name>
<age>28</age>
</user>
<user id="2">
<name>伊戈尔</name>
<age>35</age>
</user>
</users>
把它加载到内存:
using System.Xml;
string xml = @"
<users>
<user id='1'>
<name>奥尔加</name>
<age>28</age>
</user>
<user id='2'>
<name>伊戈尔</name>
<age>35</age>
</user>
</users>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml); // 或者 doc.Load("文件_路径.xml");
调用 LoadXml(或如果是文件则用 Load)后,你就可以完全访问文档内容了。
3. 在 DOM 树上导航
DOM 树的结构
每个加载后的 XML 文档都会变成一棵由不同类型节点组成的树:
| 节点类型 | .NET 中的类 | 示例 |
|---|---|---|
| Document | XmlDocument | |
| Element | XmlElement | |
| Attribute | XmlAttribute | |
| Text | XmlText | 奥尔加, 28 |
| Comment | XmlComment | |
要访问你想要的元素,就得“在树上走”,用属性 ChildNodes、Attributes、ParentNode 等等。
获取根元素
XmlElement root = doc.DocumentElement;
Console.WriteLine(root.Name); // users
遍历子元素
foreach (XmlNode node in root.ChildNodes)
{
if (node is XmlElement user)
{
// user — 这是 <user id="...">...</user>
string id = user.GetAttribute("id");
string name = user["name"].InnerText;
string age = user["age"].InnerText;
Console.WriteLine($"用户 {id}: {name}, 年龄 {age}");
}
}
重要: 只有当直接子节点里确实存在 <name> 元素时,user["name"] 才能使用。
访问属性和文本
var firstUser = root.FirstChild as XmlElement;
string id = firstUser.GetAttribute("id"); // "1"
string name = firstUser["name"].InnerText; // "奥尔加"
4. 修改 XML 文档
修改值
假设奥尔加突然决定“变年轻”:
var olga = root.FirstChild as XmlElement;
olga["age"].InnerText = "22"; // 现在 <age>22</age>
添加新用户
XmlElement newUser = doc.CreateElement("user");
newUser.SetAttribute("id", "3");
XmlElement name = doc.CreateElement("name");
name.InnerText = "瓦西莉莎";
XmlElement age = doc.CreateElement("age");
age.InnerText = "19";
newUser.AppendChild(name);
newUser.AppendChild(age);
root.AppendChild(newUser);
删除元素
删除第二个用户(“伊戈尔”):
XmlNode userToDelete = root.SelectSingleNode("user[@id='2']");
if (userToDelete != null)
root.RemoveChild(userToDelete);
保存修改
doc.Save("users_updated.xml");
// 或者 doc.OuterXml — 获取 XML 字符串
5. XPath — 在 XML 中搜索和选择的语言
当你想按条件查找元素时,在 DOM 树上手动遍历会变得很累 —— 比如查找所有年龄大于 25 的用户。为此有 XPath —— 一个针对 XML 的导航/查询语言。
基本使用 XPath
可以通过 SelectSingleNode(返回第一个匹配节点)和 SelectNodes(返回节点集合)来执行 XPath 查询。
示例:查找 id = 1 的用户
XmlNode user = root.SelectSingleNode("user[@id='1']");
Console.WriteLine(user["name"].InnerText); // 奥尔加
示例:查找所有年龄大于 25 的用户
XmlNodeList nodes = root.SelectNodes("user[age>25]");
foreach (XmlNode u in nodes)
{
Console.WriteLine(u["name"].InnerText); // 将输出 奥尔加 和 伊戈尔(如果伊戈尔还没被删除)
}
XPath — 简要语法
| XPath 表达式 | 作用 |
|---|---|
|
根下的所有 <user> 元素(在 <users> 下) |
|
id=3 的用户 |
|
年龄大于 25 的用户 |
|
所有用户的 <name> 元素 |
|
最后一个 <user> |
|
前两个 <user> |
更多例子和语法:XPath 文档。
再举一个:按嵌套元素选择
假设 XML 更复杂一些:
<library>
<book>
<title>蝇王</title>
<author>
<firstname>威廉</firstname>
<lastname>戈尔丁</lastname>
</author>
</book>
<book>
<title>斯万那边</title>
<author>
<firstname>马塞尔</firstname>
<lastname>普鲁斯特</lastname>
</author>
</book>
</library>
XmlDocument doc = new XmlDocument();
doc.LoadXml(libraryXml);
XmlNodeList authors = doc.SelectNodes("/library/book/author/lastname");
foreach (XmlNode lastName in authors)
Console.WriteLine(lastName.InnerText);
将输出:
戈尔丁
普鲁斯特
6. XPath:过滤、逻辑、计算
逻辑过滤
输出所有年龄在 18 到 30 之间的用户名字:
// [age>=18 and age<=30]
XmlNodeList youngUsers = root.SelectNodes("user[age>=18 and age<=30]");
foreach (XmlNode u in youngUsers)
Console.WriteLine(u["name"].InnerText);
处理属性
属性通过 @ 访问。查找 id 以 "1" 起始的用户:
XmlNodeList nodes = root.SelectNodes("user[starts-with(@id, '1')]");
统计节点数量
在 C# 里直接用 SelectNodes 无法返回 count() 的数字 —— 它返回的是集合。但可以这样做:
int count = root.SelectNodes("user").Count;
嵌套过滤
XmlNodeList nodes = doc.SelectNodes("/library/book[author/lastname='戈尔丁']");
7. XPath 与命名空间
如果你的 XML 使用命名空间(xmlns),情况就复杂一点!这时用 XmlNamespaceManager:
<catalog xmlns="http://books.example.com">
<book>
<title>算法</title>
</book>
</catalog>
doc.LoadXml(xml);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("b", "http://books.example.com");
XmlNodeList books = doc.SelectNodes("/b:catalog/b:book", nsmgr);
foreach (XmlNode book in books)
Console.WriteLine(book.SelectSingleNode("b:title", nsmgr)?.InnerText);
8. 使用 DOM 修改 XML
“在线”添加节点
给第一个用户添加一个新的 <email> 元素:
XmlElement email = doc.CreateElement("email");
email.InnerText = "olga@gmail.com";
olga.AppendChild(email);
修改属性
olga.SetAttribute("id", "99"); // 现在 id="99"
用 XPath 删除节点
XmlNodeList nodesToRemove = root.SelectNodes("user[age<20]");
foreach (XmlNode node in nodesToRemove)
root.RemoveChild(node);
9. 有用的小提示
查找并批量修改结构
假设你收到一个很大的 XML,里面有很多 <record type="..."> 元素。你只想保留那些属性 type 为 "customer" 的,并给每个添加一个子元素 <status>active</status>。
XmlNodeList customerNodes = root.SelectNodes("record[@type='customer']");
foreach (XmlElement record in customerNodes)
{
XmlElement status = doc.CreateElement("status");
status.InnerText = "active";
record.AppendChild(status);
}
XML 节点树 (DOM)
users
├─ user (id="1")
│ ├─ name ("奥尔加")
│ └─ age ("28")
├─ user (id="2")
│ ├─ name ("伊戈尔")
│ └─ age ("35")
按 XPath 选择示例
| XPath 查询 | 会返回 |
|---|---|
|
第一个用户 (id="1") |
|
最后一个用户 (id="2") |
|
所有年龄大于 30 的用户 (伊戈尔) |
|
id="2" 的用户 |
实际用途
- 与“老旧”API 的集成。 在很多国企和银行里,SOAP/XML 仍然很常见。能快速在大 XML 响应中找到并修改东西,能帮你省下几十个小时。
- 数据迁移。 从一个系统迁到另一个时,经常需要解析 XML 做复杂筛选、转换和批量修改。
- Excel 导入导出。 在很多 B2B 产品里,输入仍然是 XML。这里 XPath 是提取目标数据而不建立大量数据模型的快捷方式。
10. 错误、细节和常见坑
NullReferenceException: 如果尝试访问不存在的元素,例如 el["something"].InnerText,而这个子元素根本不存在,就会报错。记得总是检查 null。
XPath 与上下文: 记住没有起始 / 的表达式是在当前节点的后代中查找,而带 / 的是从文档根开始。不同的起点会导致找不到本来存在的数据。
命名空间处理: 如果不使用 XmlNamespaceManager,对带有 xmlns 的 XML 做 XPath 查询通常会得不到结果。
遍历时修改: 如果你想在遍历 SelectNodes 的结果时删除节点,先把节点保存到数组里再迭代 —— 否则集合会在遍历过程中变化,可能抛异常或漏删。
文本节点与元素节点的区别: 元素之间可能有文本节点(空格和换行),XML 会把它们当作内容。做严格选择时,使用 XmlElement 或按 NodeType 过滤会更安全。
GO TO FULL VERSION