Ошибки и их решения

Как читать и разбирать XML-файлы в C#

Обновлено: · 5 мин чтения

Прочитать XML в C# можно четырьмя основными способами: через LINQ to XML (XDocument), XmlDocument с XPath, потоковый XmlReader и десериализацию в классы. Ниже у каждого способа свой пример на .NET 8, таблица выбора, работа с атрибутами и пространствами имён, а также разбор ошибок, из-за которых код падает на реальных файлах.

Как читать и разбирать XML-файлы в C#
Содержание
  1. Пример XML-файла для разбора
  2. Какой способ выбрать
  3. Чтение через XDocument и LINQ to XML
    1. Фильтры и сортировка
  4. Чтение атрибутов
  5. XmlDocument и XPath
  6. Потоковое чтение через XmlReader
  7. Десериализация в классы
  8. Пространства имён XML
  9. Загрузка из строки, потока и сети
  10. Безопасность: внешние сущности
  11. Типичные ошибки
  12. Частые вопросы

Пример XML-файла для разбора

Все примеры используют один файл books.xml: каталог книг с атрибутом, вложенными элементами и необязательным полем. Сохраните его рядом с исполняемым файлом программы.

<?xml version="1.0" encoding="utf-8"?>
<catalog>
  <book id="1">
    <title>Язык C# в подробностях</title>
    <author>Иван Петров</author>
    <price currency="RUB">1290.50</price>
    <year>2022</year>
  </book>
  <book id="2">
    <title>Алгоритмы на практике</title>
    <author>Мария Орлова</author>
    <price currency="RUB">990</price>
  </book>
</catalog>

Обратите внимание, что у второй книги нет элемента year. Именно такие необязательные поля чаще всего вызывают NullReferenceException при наивном чтении, и ниже показано, как с ними обращаться.

Какой способ выбрать

СпособКогда подходитПамятьОсобенности
XDocument (LINQ to XML)Файлы до десятков мегабайт, запросы и фильтрыВесь документ в памятиСамый удобный современный API
XmlDocument и XPathГотовый XPath-запрос, старый кодВесь документ в памятиСтарый DOM-интерфейс, но он всё ещё работает
XmlReaderОгромные файлы, потокиПочти не растётТолько вперёд, больше кода
XmlSerializerСтруктура известна заранееОбъекты в памятиПолучаем типизированные классы

Если сомневаетесь, начинайте с XDocument: он покрывает большинство задач и читается лучше остальных вариантов.

Чтение через XDocument и LINQ to XML

Пространство имён System.Xml.Linq позволяет загрузить файл одной строкой и выбирать данные запросами LINQ. Метод Elements возвращает дочерние элементы, Element - первый дочерний элемент с таким именем, а Attribute - атрибут.

using System.Globalization;
using System.Xml.Linq;

XDocument doc = XDocument.Load("books.xml");

var books = doc.Root!.Elements("book").Select(b => new
{
    Id = (int)b.Attribute("id")!,
    Title = (string?)b.Element("title"),
    Author = (string?)b.Element("author"),
    Price = decimal.Parse(b.Element("price")!.Value, CultureInfo.InvariantCulture),
    Year = (int?)b.Element("year")
});

foreach (var b in books)
    Console.WriteLine($"{b.Id}: {b.Title}, {b.Author}, {b.Price} руб., год: {b.Year?.ToString() ?? "не указан"}");

Ключевой приём здесь - явное приведение элемента к типу: (int?)b.Element("year") вернёт null, если элемента нет, и не бросит исключения. Приведение (string?) работает аналогично. Цену мы парсим с CultureInfo.InvariantCulture, потому что в XML десятичный разделитель - точка, а на русской локали decimal.Parse без этого параметра споткнётся на "1290.50".

Фильтры и сортировка

Поскольку это обычный LINQ, условия пишутся как для любой коллекции. Вот выборка книг дороже тысячи рублей по возрастанию года.

var expensive = doc.Descendants("book")
    .Where(b => (decimal)b.Element("price")! > 1000m)
    .OrderBy(b => (int?)b.Element("year") ?? 0)
    .Select(b => (string?)b.Element("title"));

Метод Descendants ищет элементы на любой глубине, а не только среди прямых потомков, что удобно, когда вложенность нестабильна. Но он медленнее, чем Elements, на больших документах.

Чтение атрибутов

Атрибуты читаются методом Attribute либо коллекцией Attributes(). Значение цены лежит в тексте элемента, а валюта - в атрибуте.

XElement price = doc.Root!.Element("book")!.Element("price")!;
string? currency = (string?)price.Attribute("currency"); // RUB

foreach (XAttribute a in price.Attributes())
    Console.WriteLine($"{a.Name} = {a.Value}");

XmlDocument и XPath

Класс XmlDocument появился ещё в первых версиях .NET и до сих пор встречается в старом коде. Его сильная сторона - XPath: выражения вроде //book[price>1000]/title выбирают узлы одной строкой.

using System.Xml;

var doc = new XmlDocument();
doc.Load("books.xml");

XmlNodeList? titles = doc.SelectNodes("//book[price>1000]/title");
foreach (XmlNode node in titles!)
    Console.WriteLine(node.InnerText);

XmlNode? first = doc.SelectSingleNode("/catalog/book[@id='2']");
Console.WriteLine(first?.SelectSingleNode("author")?.InnerText);

XPath доступен и для XDocument: достаточно подключить System.Xml.XPath и вызвать doc.XPathSelectElements(...). Так что выражения можно использовать и в современном коде, не переходя на старый DOM.

Потоковое чтение через XmlReader

Если файл весит сотни мегабайт, загружать его целиком нельзя. XmlReader читает документ последовательно, держа в памяти только текущий узел. Платой за экономию становится ручное управление состоянием.

using System.Xml;

var settings = new XmlReaderSettings { IgnoreWhitespace = true, IgnoreComments = true };

using var reader = XmlReader.Create("books.xml", settings);
while (reader.Read())
{
    if (reader.NodeType == XmlNodeType.Element && reader.Name == "book")
    {
        string? id = reader.GetAttribute("id");
        Console.WriteLine($"Книга {id}");
    }
    else if (reader.NodeType == XmlNodeType.Element && reader.Name == "title")
    {
        Console.WriteLine("  " + reader.ReadElementContentAsString());
    }
}

Хорошая практика для больших файлов - комбинировать подходы: XmlReader перемещается между записями, а каждую запись превращает в XElement через XNode.ReadFrom и разбирает уже удобным LINQ.

static IEnumerable<XElement> StreamBooks(string path)
{
    using var reader = XmlReader.Create(path);
    reader.MoveToContent();
    while (reader.Read())
    {
        if (reader.NodeType == XmlNodeType.Element && reader.Name == "book")
            yield return (XElement)XNode.ReadFrom(reader);
    }
}

Здесь в памяти одновременно находится только одна книга. Один нюанс: после ReadFrom читатель уже стоит на следующем узле, поэтому если книги идут встык без пробелов, часть записей можно пропустить. Надёжнее вызвать reader.ReadToFollowing("book") в цикле.

Десериализация в классы

Когда структура известна заранее, проще описать классы и получить сразу типизированные объекты. Для этого есть XmlSerializer и атрибуты из System.Xml.Serialization. Подробный разбор с вложенными коллекциями приведён в отдельной статье про десериализацию XML в C#, здесь только каркас.

using System.Xml.Serialization;

[XmlRoot("catalog")]
public class Catalog
{
    [XmlElement("book")]
    public List<Book> Books { get; set; } = new();
}

public class Book
{
    [XmlAttribute("id")] public int Id { get; set; }
    [XmlElement("title")] public string Title { get; set; } = "";
    [XmlElement("author")] public string Author { get; set; } = "";
    [XmlElement("price")] public decimal Price { get; set; }
    [XmlElement("year")] public int? Year { get; set; }
}

var serializer = new XmlSerializer(typeof(Catalog));
using var stream = File.OpenRead("books.xml");
var catalog = (Catalog)serializer.Deserialize(stream)!;
Console.WriteLine(catalog.Books.Count);

Этот способ короче всех остальных, но жёстко привязан к схеме: любой неожиданный элемент либо игнорируется, либо приводит к ошибке в зависимости от настроек. Для нерегулярных документов безопаснее LINQ to XML.

Пространства имён XML

Самая частая причина "код не находит элемент, хотя он есть в файле" - пространство имён. Если корневой элемент содержит xmlns, то имя каждого элемента нужно задавать вместе с ним.

XNamespace ns = "http://example.com/catalog";
var doc = XDocument.Load("catalog-ns.xml");

var titles = doc.Descendants(ns + "title").Select(t => t.Value);

// Если пространство имён неважно:
var anyTitles = doc.Descendants().Where(e => e.Name.LocalName == "title");

В XmlDocument для XPath приходится создавать XmlNamespaceManager и использовать префикс в выражении, иначе запрос вернёт пустой результат.

Загрузка из строки, потока и сети

Не обязательно читать с диска. XDocument.Parse принимает строку, Load - путь, поток или TextReader. Для HTTP используйте HttpClient и передавайте поток.

XDocument fromString = XDocument.Parse("<a><b>1</b></a>");

using var http = new HttpClient();
await using Stream s = await http.GetStreamAsync("https://example.com/feed.xml");
XDocument fromWeb = await XDocument.LoadAsync(s, LoadOptions.None, CancellationToken.None);

Не создавайте HttpClient на каждый запрос в реальном приложении, используйте один экземпляр или IHttpClientFactory.

Безопасность: внешние сущности

XML умеет подключать внешние ресурсы через DTD, и разбор документа от недоверенного источника может привести к атакам на чтение файлов (XXE) или раздуванию памяти. В современном .NET XDocument и XmlReader по умолчанию запрещают обработку DTD, но в старом коде на XmlDocument и в .NET Framework 4.5.1 и ниже это нужно было отключать вручную. Проверяйте настройки явно, если принимаете файлы от пользователей.

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
    MaxCharactersFromEntities = 1024
};
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);

Типичные ошибки

  • NullReferenceException при обращении к Element("x").Value, если элемента нет. Приводите к типу: (string?)el.Element("x").
  • XmlException: Data at the root level is invalid. В начале файла стоит BOM или лишний текст либо файл не в той кодировке. Откройте файл как поток и дайте парсеру определить кодировку по объявлению.
  • FormatException при разборе чисел: десятичная точка и русская локаль. Используйте CultureInfo.InvariantCulture.
  • Пустой результат из-за пространства имён.
  • OutOfMemoryException на большом файле: переходите на XmlReader.
  • Недопустимые символы (например, управляющие коды) в тексте: такой файл не является корректным XML и требует предварительной очистки.

Если после чтения нужны данные в другом формате, например JSON, посмотрите статью про преобразование объектов в JSON.

Частые вопросы

Что лучше использовать для чтения XML в C#: XDocument или XmlDocument?

В новом коде выбирайте XDocument. У него лаконичный синтаксис, интеграция с LINQ и более удобная работа с null. XmlDocument оставляйте для поддержки старых проектов.

Как прочитать XML-файл, если он очень большой?

Используйте XmlReader и обрабатывайте записи по одной. Для удобства каждую запись можно превратить в XElement и разобрать LINQ, не загружая остальной документ.

Как получить значение атрибута?

Для XElement - (string?)element.Attribute("name"), для XmlNode - node.Attributes?["name"]?.Value, для XmlReader - reader.GetAttribute("name").

Почему Descendants не находит элемент, который есть в файле?

Почти всегда виновато пространство имён: имя элемента нужно задавать как ns + "title". Проверьте, нет ли в корне атрибута xmlns.

Как прочитать XML из строки?

Метод XDocument.Parse(text) разберёт строку. Для XmlDocument - LoadXml(text).

Можно ли изменять документ и сохранять его обратно?

Да. В XDocument меняйте значения через SetValue, добавляйте узлы через Add, удаляйте через Remove, а затем вызывайте doc.Save("books.xml").