Привет, Хабр!

Обработка XML-данных остаётся актуальной задачей несмотря на появление более крутых технологий для работы с данными. XML — это все еще гибкий и мощный инструмент для структурирования данных, который используется в самых разнообразных приложениях.

В статье рассмотрим как работать с XML в C#.

Работа с XML в C#

Начнем с базы.

XmlDocument — это класс, который позволяет работать с XML в доменной модели. Можно загружать, изменять и сохранять XML-документы. Читать XML можно через методыLoad и LoadXml, которые считывают данные из файлов или строк. Создание новых узлов и атрибутов происходит с помощью методов CreateElement и CreateAttribute, после чего элементы добавляются в документ с помощью AppendChild.

Cоздадим XML-документ, добавим в него элементы и атрибуты, а затем сохраним его:

using System;
using System.Xml;

class Program
{
    static void Main()
    {
        // новый XML-документ
        XmlDocument doc = new XmlDocument();

        // корневой элемент
        XmlElement root = doc.CreateElement("users");
        doc.AppendChild(root);

        // новый элемент
        XmlElement user = doc.CreateElement("user");
        root.AppendChild(user);

        // атрибут для элемента user
        XmlAttribute attr = doc.CreateAttribute("id");
        attr.Value = "1";
        user.Attributes.Append(attr);

        // вложенные элементы в элемент user
        XmlElement firstName = doc.CreateElement("firstName");
        firstName.InnerText = "John";
        user.AppendChild(firstName);

        XmlElement lastName = doc.CreateElement("lastName");
        lastName.InnerText = "Doe";
        user.AppendChild(lastName);

        // сейвим XML-документ в файл
        doc.Save("users.xml");
    }
}

Но есть способы получше.

XmlReader и XmlWriter

XmlReader и XmlWriter предоставляют более производительные альтернативы для чтения и записи XML по сравнению с DOM, т.к они работают в потоковом режиме. XmlReader читает XML поэлементно.

Пример XmlReader:

using System;
using System.Xml;

class Program
{
    static void Main()
    {
        // создаем XmlReader для чтения файла
        using (XmlReader reader = XmlReader.Create("example.xml"))
        {
            while (reader.Read()) // чтение некст элемента
            {
                if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
                {
                    Console.WriteLine(reader.ReadElementContentAsString()); // читаем содержимое элемента <name>
                }
            }
        }
    }
}

Здесь юзаем XmlReader для поэлементного чтения XML-файла. Фильтруем элементы по типу узла XmlNodeType.Element и имени name, чтобы извлечь информацию только из тех элементов, которые нас интересуют.

Пример XmlWriter:

using System;
using System.Xml;

class Program
{
    static void Main()
    {
        // создаем XmlWriter
        using (XmlWriter writer = XmlWriter.Create("output.xml"))
        {
            writer.WriteStartDocument(); // начало документа
            writer.WriteStartElement("users"); // начало корневого элемента <users>

            writer.WriteStartElement("user"); // начало элемента <user>
            writer.WriteElementString("name", "Ivan"); //добавление <name>
            writer.WriteEndElement(); // закрытие элемента <user>

            writer.WriteEndElement(); // закрытие корневого элемента <users>
            writer.WriteEndDocument(); // закрытие документа
        }
    }
}

Используем XmlWriter для создания нового XML-файла с простой структурой. Стартуем с создания корневого элемента <users>, добавляем в него дочерний элемент <user> с вложенным элементом <name>, и последовательно закрываем все открытые элементы и документ.

XDocument

Также существует XDocument и связанные с ним классы XElement, XAttribute.

XElement — это отдельный элемент в XML-документе и он позволяет управлять содержимым элемента, включая его вложенные элементы, текст и атрибуты.

XAttribute служит для работы с атрибутами XML-элементов. Атрибуты представляют собой пары имя-значение, которые прикрепляются к элементам.

Cоздадим простой XML-документ, который содержит инфу о нескольких юзерах, каждый из которых имеет уникальный идентификатор и имя:

using System;
using System.Xml.Linq;

class Program
{
    static void Main()
    {
        // новый XML-документа
        XDocument xmlDoc = new XDocument(
            new XDeclaration("1.0", "utf-8", null),
            new XElement("users", // корневой элемент
                new XElement("user", // дочерний элемент
                    new XAttribute("id", "1"), // атрибут элемента user
                    new XElement("name", "Ivan")
                ),
                new XElement("user",
                    new XAttribute("id", "2"),
                    new XElement("name", "Kolya")
                )
            )
        );

        Console.WriteLine(xmlDoc.ToString());

        // сохранение XML-документа в файл
        xmlDoc.Save("users.xml");
    }
}

XDocument создаёт новый XML-документ.

XElement используется для создания элементов users и user. Элемент users служит корневым элементом, а user — дочерним элементом, представляющим пользователя.

XAttribute применяется для добавления атрибутов к элементу user, в данном случае это идентификатор пользователя.

LINQ

Можно интегрировать LINQ с XML и сделать код более читабельным.

Например, так можно сделать чтение и запрос данных:

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

class Program
{
    static void Main()
    {
        XDocument doc = XDocument.Load("books.xml");

        var books = from book in doc.Descendants("book")
                    where (int)book.Attribute("id") == 1
                    select new
                    {
                        Title = book.Element("title").Value,
                        Author = book.Element("author").Value
                    };

        foreach (var book in books)
        {
            Console.WriteLine($"Title: {book.Title}, Author: {book.Author}");
        }
    }
}

А так сделать агрегацию данных из XML:

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

class Program
{
    static void Main()
    {
        XDocument doc = XDocument.Load("books.xml");

        var bookCount = doc.Descendants("book").Count(); // cчитаем количество книг
        var maxId = doc.Descendants("book").Max(book => (int)book.Attribute("id")); // Находим максимальный id

        Console.WriteLine($"Total books: {bookCount}, Max ID: {maxId}");
    }
}

Десериализация XML и обработка исключений

Для начала десериализации нужно создать класс, структура которого соответствует структуре XML документа. Этот класс должен содержать поля или свойства, соответствующие элементам XML.

Создаем экземпляр XmlSerializer, указав тип объекта, который нужно десериализовать:

XmlSerializer serializer = new XmlSerializer(typeof(MyClass));

Также используемXmlSerializer для чтения XML из файла или потока и его преобразования в объект. Чаще всего используется StreamReader или StringReader:

using (StreamReader reader = new StreamReader("path_to_file.xml"))
{
    MyClass myObject = (MyClass)serializer.Deserialize(reader);
}

При десериализации могут возникать различные исключения, например, InvalidOperationException при несоответствии XML схемы ожидаемому классу или XmlException при синтаксических ошибках в XML. Обрабатывать эти исключения можно с помощью блоков try-catch:

try
{
    using (StreamReader reader = new StreamReader("path_to_file.xml"))
    {
        MyClass myObject = (MyClass)serializer.Deserialize(reader);
    }
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Invalid XML format: {ex.Message}");
}
catch (XmlException ex)
{
    Console.WriteLine($"XML Parsing Error at line {ex.LineNumber}: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"General error: {ex.Message}");
}

Рассмотрим класс Person и XML файл, который содержит данные о человеке:

<Person>
    <Name>Ivan</Name>
    <Age>30</Age>
</Person>

Соответствующий класс Person в C# будет выглядеть так:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Десериализация этого XML в объект Person с XmlSerializer выглядит так:

string xmlData = @"<Person><Name>John Doe</Name><Age>30</Age></Person>";
using (StringReader stringReader = new StringReader(xmlData))
{
    XmlSerializer serializer = new XmlSerializer(typeof(Person));
    Person person = (Person)serializer.Deserialize(stringReader);
    Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}

XML схемы и валидация в C#

XML схемы — это формальное описание структуры XML документа, в которым описаны все ограничения на содержимое и структуру элементов и атрибутов. Так можно автоматом проверять, что XML документы соответствуют заданным стандартам.

XML схема обычно определяет элементы и атрибуты, которые могут появляться в документе, их типы данных, и другие ограничения. Например, схема для описания книг в библиотеке может выглядеть так:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="library">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="book" maxOccurs="unbounded">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="title" type="xs:string"/>
                            <xs:element name="author" type="xs:string"/>
                            <xs:element name="isbn" type="xs:string"/>
                            <xs:element name="price" type="xs:decimal"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

Для валидации XML документа в соответствии с XSD схемой, можно использовать классы XmlReader и XmlReaderSettings из пространства имен System.Xml.Schema. Например:

using System;
using System.Xml;
using System.Xml.Schema;

class Program
{
    static void Main()
    {
        XmlReaderSettings settings = new XmlReaderSettings();
        settings.Schemas.Add(null, "library.xsd");
        settings.ValidationType = ValidationType.Schema;

        XmlReader reader = XmlReader.Create("library.xml", settings);
        XmlDocument document = new XmlDocument();
        document.Load(reader);
        ValidationEventHandler eventHandler = new ValidationEventHandler(ValidationEventHandler);
        document.Validate(eventHandler);
    }

    static void ValidationEventHandler(object sender, ValidationEventArgs e)
    {
        if (e.Severity == XmlSeverityType.Error)
        {
            Console.WriteLine("Error: {0}", e.Message);
        }
        else if (e.Severity == XmlSeverityType.Warning)
        {
            Console.WriteLine("Warning: {0}", e.Message);
        }
    }
}

Таким образом можно обеспечить, что XML документы точно соответствуют определенным структурам и правилам.


Современные приложения иногда потребляют очень много памяти. Приглашаем вас на бесплатный урок, на котором будут рассмотрены основные приемы эффективной работы и экономии памяти в современных .net приложениях, и как помогают в этом ArrayPool, Span.

Комментарии (16)


  1. SpiderEkb
    20.05.2024 06:19

    Если правильно понимаю, то то, что является "потоковым" Xml.Reader работает по принципу XML-SAX. А есть ли в нем автоматическая обработка сложных структур, когда один и тот же тег может находиться внутри разных тегов. Например:

            <ТипСубъекта>
              <Идентификатор>2</Идентификатор>
              <Наименование>Физическое лицо</Наименование>
            </ТипСубъекта>
    
            <ТипДокумента>
               <Идентификатор>746140005</Идентификатор>
               <Наименование>PASSPORT</Наименование>
            </ТипДокумента>

    Т.е. теги Идентификатор и Наименование являются универсальными и относятся к тому тегу, внутри которого находятся (ТипСубъекта или ТипДокумента), являясь контекстно-зависимыми.

    Есть возможность такие вещи автоматически распознавать (т.е. не reader.Name == "Идентификатор", но reader.Name == "ТипДокумента.Идентификатор"), или надо руками отслеживать внутри какого тега сейчас находимся?


    1. mayorovp
      20.05.2024 06:19

      Блин, да посмотрите вы на код из статьи внимательнее! Никакой это не SAX, это, ну, Reader.

      А есть ли в нем автоматическая обработка сложных структур, когда один и тот же тег может находиться внутри разных тегов.

      Нет там никакой обработки структур вообще, только валидация структуры документа. Все тэги должны обрабатываться пользовательским кодом.

      Есть возможность такие вещи автоматически распознавать (т.е. не reader.Name == "Идентификатор", но reader.Name == "ТипДокумента.Идентификатор"), или надо руками отслеживать внутри какого тега сейчас находимся?

      Второе, потому что reader.Name - это имя тэга и ничего кроме имени тэга. Однако, если использовать валидацию по схеме, то можно получить ссылку на текущий элемент и его тип в схеме.


      1. SpiderEkb
        20.05.2024 06:19

        Блин, да посмотрите вы на код из статьи внимательнее! Никакой это не SAX, это, ну, Reader.

        SAX (англ. «Simple API for XML») — способ последовательного чтения/записи XML-файлов.

        Нет там никакой обработки структур вообще, только валидация структуры документа. Все тэги должны обрабатываться пользовательским кодом.

        Ну, собственно, о том и спрашивал. Т.е. никаких там наворотов поверх потокового чтения нет. Все максимально просто, быстро, минимум использования памяти, но много отдается на волю разработчика.

        Это действительно удобно когда нужно просто извлечь из XML какие-то данные и потом с ними что-то делать.

        Работал с таким (причем, XML были достаточно объемные - несколько десятков Мб). Правда, в той реализации, что у меня была под рукой, задавался callback handler, вызываемый "изнутри" с параметрами "код события" + "данные". Коды собятия там были типа "начало тега" (с именем тега в данных), "содержимое тега", "конец тега" и т.п. И да, для описанной выше ситуации приходилось выставлять флаги "внутри тега ТипСубъекта" или "внутри тега ТипДокумента" чтобы правильно обрабатывать теги Идентификатор и ТипДокумента.

        Спросил потому что думал вдруг придумали что-то более умное, типа "полного имени тега" (какое-нибудь свойство fullname) для вложенных тегов.


        1. mayorovp
          20.05.2024 06:19
          +3

          SAX (англ. «Simple API for XML») — способ последовательного чтения/записи XML-файлов

          Это не любой способ, а конкретный способ с конкретным подходом к построению API. XmlReader не реализует этот подход.

          Работал с таким (причем, XML были достаточно объемные - несколько десятков Мб). Правда, в той реализации, что у меня была под рукой, задавался callback handler, вызываемый "изнутри" с параметрами "код события" + "данные"

          Этот способ и называется SAX...

          Спросил потому что думал вдруг придумали что-то более умное, типа "полного имени тега" (какое-нибудь свойство fullname) для вложенных тегов.

          Ну, кое-что придумано, только в другом направлении. В отличии от SAX, XmlReader не требует единого места обработки абсолютно всех элементов. Никто не мешает делать вложенные циклы, и вообще структурировать программу по всем правилам структурного программирования:

          Примерный код:

          if (reader.Name == "ТипСубъекта") ReadSubject(reader);
          
          // ...
          
          void ReadSubject(XmlReader reader)
          {
              while(reader.Read())
              {
                  if (reader.NodeType == XmlNodeType.Element && reader.Name == "Идентификатор")
                      subjectTypeId = reader.ReadElementContentAsString();
          
                  else if (reader.NodeType == XmlNodeType.Element && reader.Name == "Наименование")
                      subjectTypeName = reader.ReadElementContentAsString();
          
                  else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "ТипСубъекта")
                      return;
              }
              throw ...;
          }
          

          Кстати, для SAX я видел другой подход, не через флаги. Там создавался делегирующий обработчик со стеком вложенных обработчиков, каждый из которых отвечал за конкретный элемент. То есть у вас есть обработчик ТипСубъекта, который получает события только для дочерних элементов ТипСубъекта, обработчик ТипДокумента, который получает события только для дочерних элементов ТипДокумента и корневой обработчик, который получает события уровнем выше и кладёт в стек первые два при необходимости.


          1. SpiderEkb
            20.05.2024 06:19

            Кстати, для SAX я видел другой подход, не через флаги. Там создавался делегирующий обработчик со стеком вложенных обработчиков, каждый из которых отвечал за конкретный элемент. То есть у вас есть обработчик ТипСубъекта, который получает события только для дочерних элементов ТипСубъекта, обработчик ТипДокумента, который получает события только для дочерних элементов ТипДокумента и корневой обработчик, который получает события уровнем выше и кладёт в стек первые два при необходимости.

            Подобный подход тоже использовал.

            Но в SAX все равно передается что-то одно - обработчик верхнего уровня. Ну а дальше он уже должен решать по текущему состоянию - куда отдавать дальнейшую обработку элемента. Т.е. там все равно какие-то флаги нужны.

            В моем понимании SAX - это общий принцип. Не построение всего дерева документа в памяти (как в DOM), а потоковое чтение и действия на каждый прочитанный элемент. В том SAX с которым я работал те циклы, что указаны в reader просто спрятаны внутрь. Не берусь судить что удобнее - надо пробовать под конкретную задачу.

            А для простейших одноуровневых XML типа

            <Person>
                <Name>Ivan</Name>
                <Age>30</Age>
            </Person>

            В используемом мной языке вообще есть XML-INTO - там просто создаешь структуру с именами полей, совпадающими с именами тегов, дергаешь XML-INTO и сразу получаешь заполненную структуру.
            Минус - с русскими тегами не работает :-(


            1. mayorovp
              20.05.2024 06:19

              В моем понимании SAX - это общий принцип. Не построение всего дерева документа в памяти (как в DOM), а потоковое чтение и действия на каждый прочитанный элемент.

              Потоковое чтение называется "потоковое чтение", ему не требуется специальное название.

              В том SAX с которым я работал те циклы, что указаны в reader просто спрятаны внутрь.

              То-то и оно, что они не "просто спрятаны", а фундаментально спрятаны. Архитектура SAX и XmlReader принципиально различаются, как и способы их использования, возникающие при этом проблемы и способы их решения.

              В используемом мной языке вообще есть XML-INTO - там просто создаешь структуру с именами полей, совпадающими с именами тегов, дергаешь XML-INTO и сразу получаешь заполненную структуру. Минус - с русскими тегами не работает :-(

              Это называется "десериализация", в C# ей занимается XmlSerializer. С русскими тэгами без проблем работает. Можно комбинировать с потоковым чтением при необходимости (верхний уровень читается потоково, а нижний десериализуется).


              1. SpiderEkb
                20.05.2024 06:19

                Я привел ссылку на определение SAX как способ потокового чтения XML файлов. Именно способ, на конкретную реализацию, которых может быть много.

                Событийная реализация просто наиболее общепринятая. Никто не мешает реализовать эту же модель аналогично последовательному чтению записей БД с проверкой содержимого полей очередной прочитанной записи.

                Впрочем, это все вопрос терминологии.


                1. mayorovp
                  20.05.2024 06:19

                  Если походить по страницам по вашей ссылке, то можно найти и такое: "Росси́я, или Росси́йская Федера́ция (сокр. РФ), — государство в Восточной Европе и Северной Азии.". Означает ли это, что любое государство в Восточной Европе - это Россия?

                  SAX - это имя собственное, и оно означает один конкретный набор интерфейсов в Java и ничего более.

                  К слову, гляньте один из источников по вашей же ссылке: http://www.saxproject.org/


    1. rukhi7
      20.05.2024 06:19

      Есть возможность такие вещи автоматически распознавать (т.е. не reader.Name == "Идентификатор", но reader.Name == "ТипДокумента.Идентификатор")

      тогда вам не ридер нужен, а язык запросов, например XPath (XML Path Language) 

      Ридер просто загружает XML из файла в память, есть два основных способа, загрузить и отдать пользователю полностью дерево XML объектов (узлов, элементов, ... как хотите называйте)

      или отдавать XML объекты по мере чтения.

      Поскольку поиск все равно должен читать XML, сначала, а потом еще проверить условие запроса, и вернуть пользователю только то что соответствует вашему запросу, поиск также в основе своей использует два этих метода чтения.

      Через XPath можно запросить, то есть найти, все узлы которые имеют Идентификатор как дочерний узел, можно найти все узлы путь к которым заканчивается на "ТипДокумента.Идентификатор"

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


      1. SpiderEkb
        20.05.2024 06:19

        Не совсем так. Мне вполне хватило чтобы ридер знал "полное имя тега". Т.е. для указанного выше случая (как уже написал выше) можно было смотреть как свойство name - например, "Идентификатор", так и свойство fullname (или qualifiedname - как угодно), например, "ТипДокумента.Идентификатор". Ну или свойство path - "ТипДокумента"

        С точки зрения реализации это не является чем-то невозможным - ридер идет по документу последовательно и вполне способен "накапливать" последовательность имен тегов, внутри которой находится в текущий момент.

        И да, все это легко реализуется руками, но было бы чуть удобнее, если бы оно уже было "из коробки". Но раз нет - говорить не о чем.


        1. rukhi7
          20.05.2024 06:19
          +1

          И да, все это легко реализуется руками, но было бы чуть удобнее, если бы оно уже было "из коробки". Но раз нет - говорить не о чем.

          XML парсер существует (даже с С++ интерфейсом) и успешно (хотя не всегда конечно) используется уже более 20 лет, и интерфейс к нему существует столько же. Вы считаете что за 20 лет никто бы не догадался сделать что-то лучше, если оно действительно лучше?

          Потом что значит "чуть удобнее"? Так можно дойти до того, что было бы даже намного удобнее если бы всю работу уже кто-то сделал за вас, только возникает тогда вопрос, а вы то зачем тогда нужны как технический специалист, коробки переставлять? Никогда не понимал такого отношения!


      1. tuxi
        20.05.2024 06:19
        +1

        XPath это уже про DOM модель парсера. На паре гигибайт данных это такое себе удовольствие.

        SAX парсер безальтернативен на больших данных, и да, там придется флажки ставить "нода открылась, нода закрылась".


        1. mayorovp
          20.05.2024 06:19

          XmlReader - вполне себе альтернатива SAX парсеру. Но да, XPath он тоже не умеет.

          Кстати, на Java тот же Saxon умудряется ограниченно комбинировать XPath и потоковый разбор, однако я не вижу способа дёшево "достать" из него этот механизм.


          1. SpiderEkb
            20.05.2024 06:19

            Фактически - тот же SAX, просто в другой реализации.

            Два подхода - DOM с построением всего дерева в памяти и SAX потоковым чтением. Как все это реализовать к конкретике - тут уже возможны варианты.


            1. tuxi
              20.05.2024 06:19

              Именно так, это просто SAX с красивой оберткой. Полноценный XPath возможен только после полного построения дерева элементов. А это однозначно DOM парсер.

              И еще комментарий: использовать DOM без четкого понимания (контракта с внешней системой) объемов данных - это стрелять себе в ноги крупной дробью. SAX при первоначальной бОльшей трудности реализации, делает систему гораздо более устойчивой и прогнозируемой. Надо только способ получения потока данных реализовывать не полным чтением в память.


  1. CorwinH
    20.05.2024 06:19
    +4

    По-моему, тема не раскрыта. В чём разница между XmlDocument и XDocument, между XmlReader и SAX-парсерами и т.п.