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, если XML из файла) у вас есть полный доступ к содержимому документа.
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
XPath-запросы можно выполнять через методы SelectSingleNode (вернёт первый найденный узел) и SelectNodes (вернёт коллекцию узлов).
Пример: найти пользователя с 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. Модификация XML с помощью DOM
Добавление узлов "на лету"
Добавим новый элемент <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, XPath-запросы к XML с xmlns будут возвращать пусто.
Модификация во время перебора: Если хотите удалять узлы по результатам SelectNodes, сначала сохраните их в массив, а потом итерируйте — иначе коллекция изменится по ходу перебора, и могут возникнуть ошибки.
Отличие текстовых и элементных узлов: Между элементами могут быть текстовые узлы — пробелы и переводы строк, которые XML считает содержимым. Для строгой выборки используйте только XmlElement или фильтруйте по NodeType.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ