1. Wprowadzenie
Wyobraź sobie gigantyczny XML, który przysłał jakiś serwis SOAP albo bankowe API. W nim setki zagnieżdżonych elementów, niejednorodne struktury, a Tobie trzeba znaleźć tylko jedną jedyną kwotę płatności za poprzedni miesiąc. Podejście z serializacją tu nie pasuje — za bardzo klopotliwe i z góry nie wiadomo, gdzie patrzeć. Potrzebna "lupa" do drzewa XML i umiejętność szybkiego poruszania się po jego gałęziach.
Właśnie do takich przypadków w .NET od wielu lat jest potężne narzędzie — klasa XmlDocument, uzupełniona językiem zapytań XPath. Dziś nauczysz się:
- Wczytywać XML do drzewa DOM;
- Poruszać się i znajdować potrzebne elementy ręcznie i za pomocą XPath;
- Modyfikować zawartość dokumentu XML;
- Dodawać, usuwać i zmieniać węzły;
- Używać XPath do złożonych selekcji.
2. Czym jest XmlDocument?
XmlDocument — klasa z przestrzeni nazw System.Xml. To implementacja DOM (Document Object Model) — reprezentacji całego pliku XML jako drzewa obiektów w pamięci.
Trochę teorii i analogii
Jeśli XmlSerializer przypomina konstruktor LEGO — kiedy zamieniasz obiekt na zestaw klocków i z powrotem — to XmlDocument jest jak praca z prawdziwym drzewem: ma korzeń, gałęzie (elementy), liście (węzły tekstowe), i możesz chodzić po tym drzewie, dodawać gałęzie, ścinać liście i przesadzać je gdzie chcesz.
Proste wczytanie dokumentu XML
Załóżmy, że mamy taki XML:
<users>
<user id="1">
<name>Olga</name>
<age>28</age>
</user>
<user id="2">
<name>Igor</name>
<age>35</age>
</user>
</users>
Wczytamy go do pamięci:
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); // albo doc.Load("ścieżka_do_pliku.xml");
Po wywołaniu LoadXml (albo Load, jeśli XML z pliku) masz pełny dostęp do zawartości dokumentu.
3. Nawigacja po drzewie DOM
Struktura drzewa DOM
Każdy dokument XML po wczytaniu staje się drzewem, składającym się z węzłów różnych typów:
| Typ węzła | Klasa w .NET | Przykład |
|---|---|---|
| Document | XmlDocument | |
| Element | XmlElement | |
| Attribute | XmlAttribute | |
| Text | XmlText | Olga, 28 |
| Comment | XmlComment | |
Aby odwołać się do potrzebnych elementów — trzeba "chodzić po drzewie", używając właściwości ChildNodes, Attributes, ParentNode itd.
Pobieramy element root
XmlElement root = doc.DocumentElement;
Console.WriteLine(root.Name); // users
Iterujemy dzieci
foreach (XmlNode node in root.ChildNodes)
{
if (node is XmlElement user)
{
// user — to <user id="...">...</user>
string id = user.GetAttribute("id");
string name = user["name"].InnerText;
string age = user["age"].InnerText;
Console.WriteLine($"Użytkownik {id}: {name}, wiek {age}");
}
}
WAŻNE: Dostęp user["name"] jest możliwy tylko jeśli wśród bezpośrednich potomków jest element <name>.
Dostęp do atrybutów i tekstu
var firstUser = root.FirstChild as XmlElement;
string id = firstUser.GetAttribute("id"); // "1"
string name = firstUser["name"].InnerText; // "Olga"
4. Modyfikacja dokumentu XML
Zmiana wartości
Załóżmy, że Olga niespodziewanie postanowiła się "odmłodzić":
var olga = root.FirstChild as XmlElement;
olga["age"].InnerText = "22"; // teraz <age>22</age>
Dodanie nowego użytkownika
XmlElement newUser = doc.CreateElement("user");
newUser.SetAttribute("id", "3");
XmlElement name = doc.CreateElement("name");
name.InnerText = "Wasilisa";
XmlElement age = doc.CreateElement("age");
age.InnerText = "19";
newUser.AppendChild(name);
newUser.AppendChild(age);
root.AppendChild(newUser);
Usuwanie elementu
Usuniemy drugiego użytkownika ("Igor"):
XmlNode userToDelete = root.SelectSingleNode("user[@id='2']");
if (userToDelete != null)
root.RemoveChild(userToDelete);
Zapisywanie zmian
doc.Save("users_updated.xml");
// Albo doc.OuterXml — aby dostać string z XML
5. XPath — język wyszukiwania i selekcji w XML
Praca z drzewem DOM staje się męcząca, jeśli trzeba szukać elementów "wg warunku" — np. wszystkich użytkowników starszych niż 25 lat. Do tego właśnie służy XPath — język nawigacji po drzewie XML.
Podstawowe użycie XPath
Zapytania XPath można wykonywać przez metody SelectSingleNode (zwróci pierwszy znaleziony węzeł) i SelectNodes (zwróci kolekcję węzłów).
Przykład: znaleźć użytkownika z id = 1
XmlNode user = root.SelectSingleNode("user[@id='1']");
Console.WriteLine(user["name"].InnerText); // Olga
Przykład: znaleźć wszystkich użytkowników starszych niż 25 lat
XmlNodeList nodes = root.SelectNodes("user[age>25]");
foreach (XmlNode u in nodes)
{
Console.WriteLine(u["name"].InnerText); // Wypisze Olga i Igor (jeśli Igor jeszcze nie został usunięty)
}
XPath — krótka składnia
| Wyrażenie XPath | Co zrobi |
|---|---|
|
Wszystkie elementy <user> w korzeniu <users> |
|
Użytkownik z id=3 |
|
Użytkownicy starsi niż 25 lat |
|
Wszystkie elementy <name> wszystkich użytkowników |
|
Ostatni <user> |
|
Pierwsze dwa <user> |
Więcej przykładów i składni: Dokumentacja XPath.
Jeszcze przykład: wybór po zagnieżdżonych elementach
Załóżmy, że XML jest bardziej złożony:
<library>
<book>
<title>Władca much</title>
<author>
<firstname>William</firstname>
<lastname>Golding</lastname>
</author>
</book>
<book>
<title>W stronę Swanna</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);
Co zostanie wypisane:
Golding
Proust
6. XPath: filtrowanie, logika, obliczenia
Filtry logiczne
Wypisz imiona wszystkich użytkowników, których wiek jest między 18 i 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);
Praca z atrybutami
Atrybuty wybiera się przez @. Znajdź użytkowników, których id zaczyna się od "1":
XmlNodeList nodes = root.SelectNodes("user[starts-with(@id, '1')]");
Liczenie węzłów
Bezpośrednio w C# metoda SelectNodes nie zwraca liczby przez funkcję count() — zwraca kolekcję, a nie liczbę. Ale można zrobić tak:
int count = root.SelectNodes("user").Count;
Zagnieżdżone filtry
XmlNodeList nodes = doc.SelectNodes("/library/book[author/lastname='Golding']");
7. XPath i przestrzenie nazw
Jeżeli Twój XML używa przestrzeni nazw (xmlns), to robi się o wiele ciekawiej! W takich przypadkach używaj XmlNamespaceManager:
<catalog xmlns="http://books.example.com">
<book>
<title>Algorytmy</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. Modyfikacja XML za pomocą DOM
Dodawanie węzłów "w locie"
Dodajmy nowy element <email> do pierwszego użytkownika:
XmlElement email = doc.CreateElement("email");
email.InnerText = "olga@gmail.com";
olga.AppendChild(email);
Zmiana atrybutów
olga.SetAttribute("id", "99"); // Teraz id="99"
Usuwanie węzłów za pomocą XPath
XmlNodeList nodesToRemove = root.SelectNodes("user[age<20]");
foreach (XmlNode node in nodesToRemove)
root.RemoveChild(node);
9. Przydatne niuanse
Wyszukiwanie i masowe zmiany struktury
Załóżmy, że dostajesz duży XML z różnymi elementami <record type="...">. Trzeba zostawić tylko te, które mają w atrybucie type wartość "customer", i dodać każdemu podrzędny element <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);
}
Drzewo węzłów XML (DOM)
users
├─ user (id="1")
│ ├─ name ("Olga")
│ └─ age ("28")
├─ user (id="2")
│ ├─ name ("Igor")
│ └─ age ("35")
Przykład wyboru przez XPath
| Zapytanie XPath | Co zwróci |
|---|---|
|
Pierwszy użytkownik (id="1") |
|
Ostatni użytkownik (id="2") |
|
Wszyscy powyżej 30 lat (Igor) |
|
Użytkownik z id="2" |
Praktyczne zastosowania
- Integracje ze "starymi" API. W wielu instytucjach rządowych i bankach SOAP/XML nadal rządzi. Możliwość szybkiego znalezienia i zmiany czegoś w dużej odpowiedzi XML może zaoszczędzić dziesiątki godzin.
- Migracja danych. Przy przejściu z jednego systemu na inny często trzeba parsować XML i robić złożone selekcje, transformacje i masowe zmiany.
- Import-eksport do Excel. W wielu produktach B2B dane nadal przychodzą w XML. Tam XPath to szybki sposób, by wydobyć to, co potrzebne, bez budowania dużego modelu danych.
10. Błędy, niuanse i typowe pułapki
NullReferenceException: Jeśli spróbujesz odwołać się do nieistniejącego elementu, np. el["something"].InnerText, a takiego potomka wcale nie ma. Zawsze sprawdzaj na null.
XPath i zagnieżdżenie: Pamiętaj, że wyrażenie bez początkowego / szuka wśród potomków bieżącego węzła, a z / — od korzenia dokumentu. Różne punkty odniesienia mogą dać brak wyniku, chociaż dane są.
Praca z przestrzeniami nazw: Jeśli nie użyjesz XmlNamespaceManager, XPath-zapytania do XML z xmlns będą zwracać pusto.
Modyfikacja podczas iteracji: Jeżeli chcesz usuwać węzły na podstawie wyników SelectNodes, najpierw zapisz je w tablicy, a potem iteruj — inaczej kolekcja zmieni się w trakcie pętli i mogą pojawić się błędy.
Różnica między węzłami tekstowymi a elementami: Między elementami mogą być węzły tekstowe — spacje i nowe linie, które XML traktuje jak zawartość. Dla ścisłej selekcji używaj tylko XmlElement albo filtruj po NodeType.
GO TO FULL VERSION