1. Giới thiệu
Hãy tưởng tượng một XML khổng lồ được gửi từ một dịch vụ SOAP hoặc banking API. Nó có hàng trăm phần tử lồng nhau, cấu trúc không đồng đều, và bạn chỉ cần tìm đúng một giá trị tổng tiền thanh toán của tháng trước. Cách làm bằng serialization ở đây không phù hợp — quá nặng và không biết trước phải nhìn vào đâu. Cần một "kính lúp" cho cây XML và kỹ năng đi nhanh trên các nhánh của nó.
Chính vì những trường hợp này trong .NET đã có một công cụ mạnh mẽ từ lâu — lớp XmlDocument, kết hợp với ngôn ngữ truy vấn XPath. Hôm nay bạn sẽ học:
- Load XML vào DOM-tree;
- Đi lại và tìm các phần tử cần bằng tay và bằng XPath;
- Chỉnh sửa nội dung của XML document;
- Thêm, xóa và thay đổi node;
- Sử dụng XPath cho các truy vấn phức tạp.
2. XmlDocument là gì?
XmlDocument — lớp từ namespace System.Xml. Đây là một hiện thực của mô hình DOM (Document Object Model) — đại diện toàn bộ file XML dưới dạng cây đối tượng trong bộ nhớ.
Ít lý thuyết và phép so sánh
Nếu XmlSerializer giống như bộ LEGO, khi bạn biến object thành các miếng và ngược lại, thì XmlDocument giống như làm việc trực tiếp với một cái cây: có root, cành (elements), lá (text nodes), và bạn có thể đi trên cây đó, thêm cành, cắt lá và ghép chúng đi nơi khác tùy ý.
Load một XML đơn giản
Giả sử ta có XML như sau:
<users>
<user id="1">
<name>Olga</name>
<age>28</age>
</user>
<user id="2">
<name>Igor</name>
<age>35</age>
</user>
</users>
Load nó vào bộ nhớ:
using System.Xml;
string xml = @"
<users>
<user id='1'>
<name>Olga</name>
<age>28</age>
</user>
<user id='2'>
<name>Igor</name>
<age>35</age>
</user>
</users>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml); // hoặc doc.Load("duong_dan_file.xml");
Sau khi gọi LoadXml (hoặc Load nếu XML từ file) bạn có quyền truy cập đầy đủ vào nội dung document.
3. Dạo quanh DOM-tree
Cấu trúc DOM-tree
Mỗi document XML sau khi load trở thành một cây, gồm các node kiểu khác nhau:
| Loại node | Class trong .NET | Ví dụ |
|---|---|---|
| Document | XmlDocument | |
| Element | XmlElement | |
| Attribute | XmlAttribute | |
| Text | XmlText | Olga, 28 |
| Comment | XmlComment | |
Để truy cập phần tử cần thiết — bạn sẽ "đi trên cây" sử dụng các property như ChildNodes, Attributes, ParentNode v.v.
Lấy element root
XmlElement root = doc.DocumentElement;
Console.WriteLine(root.Name); // users
Duyệt các phần tử con
foreach (XmlNode node in root.ChildNodes)
{
if (node is XmlElement user)
{
// user — là <user id="...">...</user>
string id = user.GetAttribute("id");
string name = user["name"].InnerText;
string age = user["age"].InnerText;
Console.WriteLine($"User {id}: {name}, age {age}");
}
}
QUAN TRỌNG: Truy cập user["name"] chỉ hợp lệ nếu trong con trực tiếp có element <name>.
Truy cập attribute và text
var firstUser = root.FirstChild as XmlElement;
string id = firstUser.GetAttribute("id"); // "1"
string name = firstUser["name"].InnerText; // "Olga"
4. Chỉnh sửa XML document
Thay đổi giá trị
Giả sử Olga bất ngờ muốn "trẻ hóa":
var olga = root.FirstChild as XmlElement;
olga["age"].InnerText = "22"; // bây giờ <age>22</age>
Thêm người dùng mới
XmlElement newUser = doc.CreateElement("user");
newUser.SetAttribute("id", "3");
XmlElement name = doc.CreateElement("name");
name.InnerText = "Vasilisa";
XmlElement age = doc.CreateElement("age");
age.InnerText = "19";
newUser.AppendChild(name);
newUser.AppendChild(age);
root.AppendChild(newUser);
Xóa phần tử
Xóa user thứ hai ("Igor"):
XmlNode userToDelete = root.SelectSingleNode("user[@id='2']");
if (userToDelete != null)
root.RemoveChild(userToDelete);
Lưu thay đổi
doc.Save("users_updated.xml");
// Hoặc doc.OuterXml — để lấy chuỗi XML
5. XPath — ngôn ngữ tìm kiếm và chọn lọc trong XML
Làm việc với DOM có thể mệt nếu bạn cần tìm phần tử "theo điều kiện" — ví dụ, tất cả user lớn hơn 25 tuổi. Đó là lý do có XPath — ngôn ngữ điều hướng trên cây XML.
Sử dụng cơ bản XPath
Các truy vấn XPath có thể thực hiện qua phương thức SelectSingleNode (trả node đầu tiên) và SelectNodes (trả collection).
Ví dụ: tìm user có id = 1
XmlNode user = root.SelectSingleNode("user[@id='1']");
Console.WriteLine(user["name"].InnerText); // Olga
Ví dụ: tìm tất cả user lớn hơn 25 tuổi
XmlNodeList nodes = root.SelectNodes("user[age>25]");
foreach (XmlNode u in nodes)
{
Console.WriteLine(u["name"].InnerText); // In ra Olga và Igor (nếu Igor chưa bị xóa)
}
XPath — cú pháp ngắn gọn
| Biểu thức XPath | Sẽ làm gì |
|---|---|
|
Tất cả phần tử <user> trong root <users> |
|
User có id=3 |
|
Users lớn hơn 25 tuổi |
|
Tất cả element <name> của mọi user |
|
User cuối cùng |
|
Hai user đầu tiên |
Nhiều ví dụ hơn: Tài liệu về XPath.
Thêm ví dụ: chọn lọc theo phần tử lồng nhau
Giả sử XML phức tạp hơn:
<library>
<book>
<title>Chúa ruồi</title>
<author>
<firstname>William</firstname>
<lastname>Golding</lastname>
</author>
</book>
<book>
<title>Trong hướng Svan</title>
<author>
<firstname>Marcel</firstname>
<lastname>Proust</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);
Kết quả in ra sẽ là:
Golding
Proust
6. XPath: lọc, logic, tính toán
Lọc logic
In tên tất cả users có tuổi giữa 18 và 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);
Làm việc với attribute
Attribute được truy vấn bằng @. Tìm users có id bắt đầu bằng "1":
XmlNodeList nodes = root.SelectNodes("user[starts-with(@id, '1')]");
Đếm số node
Trực tiếp trong method SelectNodes không trả số bằng hàm count() — nó trả collection, không phải số. Nhưng bạn có thể làm như sau:
int count = root.SelectNodes("user").Count;
Lọc lồng nhau
XmlNodeList nodes = doc.SelectNodes("/library/book[author/lastname='Golding']");
7. XPath và namespace
Nếu XML của bạn dùng namespace (xmlns), mọi thứ trở nên thú vị hơn! Dùng XmlNamespaceManager cho các trường hợp đó:
<catalog xmlns="http://books.example.com">
<book>
<title>Thuật toán</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. Chỉnh sửa XML bằng DOM
Thêm node "on the fly"
Thêm element <email> cho user đầu tiên:
XmlElement email = doc.CreateElement("email");
email.InnerText = "olga@gmail.com";
olga.AppendChild(email);
Thay đổi attribute
olga.SetAttribute("id", "99"); // Bây giờ id="99"
Xóa node bằng XPath
XmlNodeList nodesToRemove = root.SelectNodes("user[age<20]");
foreach (XmlNode node in nodesToRemove)
root.RemoveChild(node);
9. Những mẹo hữu ích
Tìm và thay đổi cấu trúc hàng loạt
Giả sử bạn nhận một XML lớn với các phần tử khác nhau <record type="...">. Cần giữ lại chỉ những record có attribute type là "customer", và thêm cho mỗi cái một child <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);
}
Cây node XML (DOM)
users
├─ user (id="1")
│ ├─ name ("Olga")
│ └─ age ("28")
├─ user (id="2")
│ ├─ name ("Igor")
│ └─ age ("35")
Ví dụ chọn bằng XPath
| XPath-query | Sẽ trả |
|---|---|
|
User đầu tiên (id="1") |
|
User cuối cùng (id="2") |
|
Tất cả > 30 tuổi (Igor) |
|
User có id="2" |
Ứng dụng thực tế
- Tích hợp với API "cũ". Ở nhiều cơ quan nhà nước và ngân hàng SOAP/XML vẫn còn dùng nhiều. Khả năng nhanh tìm và sửa trong một XML lớn có thể cứu được hàng chục giờ làm việc.
- Di chuyển dữ liệu. Khi chuyển hệ thống thường phải parse XML và làm các chọn lọc, chuyển đổi, và sửa hàng loạt.
- Import-export vào Excel. Trong nhiều sản phẩm B2B input vẫn là XML. Ở đó XPath là cách nhanh để lấy data cần mà không cần dựng model lớn.
10. Lỗi, lưu ý và bẫy hay gặp
NullReferenceException: Nếu bạn cố truy cập element không tồn tại, ví dụ el["something"].InnerText, mà phần tử con đó hoàn toàn không có. Luôn kiểm tra null.
XPath và vị trí: Nhớ rằng biểu thức không có dấu / bắt đầu tìm trong descendants của node hiện tại, còn có / thì từ root của document. Các điểm tham chiếu khác nhau có thể dẫn đến không có kết quả mặc dù dữ liệu tồn tại.
Làm việc với namespace: Nếu không dùng XmlNamespaceManager, các truy vấn XPath tới XML có xmlns sẽ trả rỗng.
Chỉnh sửa trong khi duyệt: Nếu muốn xóa node dựa trên kết quả SelectNodes, hãy lưu chúng vào mảng trước, sau đó iterate — nếu không collection sẽ thay đổi khi duyệt và có thể gây lỗi.
Phân biệt text và element nodes: Giữa các element có thể có text nodes — khoảng trắng và xuống dòng, mà XML coi là nội dung. Để chọn chính xác dùng XmlElement hoặc lọc theo NodeType.
GO TO FULL VERSION