CodeGym /课程 /C# SELF /进阶 XML 操作: XmlDocument

进阶 XML 操作: XmlDocumentXPath

C# SELF
第 48 级 , 课程 2
可用

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
<users>...</users>
Element XmlElement
<user>, <name>
Attribute XmlAttribute
id="1"
Text XmlText 奥尔加, 28
Comment XmlComment
<!-- 注释 -->

要访问你想要的元素,就得“在树上走”,用属性 ChildNodesAttributesParentNode 等等。

获取根元素

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 表达式 作用
/users/user
根下的所有 <user> 元素(在 <users> 下)
user[@id='3']
id=3 的用户
user[age>25]
年龄大于 25 的用户
user/name
所有用户的 <name> 元素
user[last()]
最后一个 <user>
user[position()<3]
前两个 <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:过滤、逻辑、计算

逻辑过滤

输出所有年龄在 1830 之间的用户名字:

// [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 查询 会返回
/users/user[1]
第一个用户 (id="1")
/users/user[last()]
最后一个用户 (id="2")
/users/user[age>30]
所有年龄大于 30 的用户 (伊戈尔)
/users/user[@id='2']
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 过滤会更安全。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION