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}");
}
}
重要:存取 user["name"] 只有在直接子節點中有 <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> 元素 |
|
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