Чтение и обход XML

61

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

// Это используется для хранения ссылки на один из элементов в дереве XML.
XElement firstEmployee;

XDocument xDocument = new XDocument(
              new XDeclaration("1.0", "UTF-8", "yes"),
              new XDocumentType("Employees", null, "Employees.dtd", null),
              new XProcessingInstruction("EmployeeCataloger", "out-of-print"),
                // Обратите внимание, что в следующей строке сохраняется 
                // ссылка на первый элемент
              new XElement("Employees", firstEmployee =
                new XElement("Employee",
                  new XAttribute("type", "Programmer"),
                  new XElement("FirstName", "Alex"),
                  new XElement("LastName", "Erohin")),
                new XElement("Employee",
                  new XAttribute("type", "Editor"),
                  new XElement("FirstName", "Elena"),
                  new XElement("LastName", "Volkova"))));

Console.WriteLine(xDocument);

Для начала обратите внимание на сохранение ссылки на первый сконструированный элемент Employee. Это делается для того, чтобы иметь базовый элемент, с которого можно было выполнять обход. Хотя в данном примере переменная firstEmployee не используется, она понадобится в последующих примерах. Следующее, что необходимо отметить — это аргумент метода Console.WriteLine. В данном случае выводится сам документ. В последующих примерах этот аргумент будет изменяться, демонстрируя, как обходить дерево XML. Ниже представлен вывод предыдущего примера:

Базовый пример для всех последующих примеров

Свойства обхода

Рассмотрение начинается с первичных свойств обхода. Когда указаны направления (вверх, вниз и т.д.), они указываются относительно элемента, на котором вызван метод. В последующих примерах сохраняется ссылка на первый элемент Employee, который и служит базовым элементом для обхода.

Обход вперед с помощью XNode.NextNode

Обход вперед по дереву XML выполняется с помощью свойства NextNode.Ниже приведен пример:

...
Console.WriteLine(firstEmployee.NextNode);

Поскольку базовым служит первый элемент Employee — firstEmployee, — обход вперед должен привести ко второму элементу Employee. Вот результат:

Обход вперед от объекта XElement через свойство NextNode

Результат доказывает работоспособность кода. Если обратиться к свойству PreviousNode элемента, то оно вернет null, поскольку этот узел является первым в списке узлов родителя. Давайте проверим это утверждение.

Обход назад с помощью XNode.PreviousNode

Для обхода XML-дерева назад используется свойство PreviousNode. Поскольку firstEmployee не имеет предыдущего узла, предпримем небольшой трюк, сначала сделав доступ к свойству NextNode, получив второй узел Employee, как это делалось в предыдущем примере, и из него получим PreviousNode. Это приведет обратно к узлу firstEmployee. Если вы когда-либо слышали выражение "шаг вперед, два шага назад", то всего одно дополнительное обращение к свойству PreviousNode позволит сделать это:

...
Console.WriteLine(firstEmployee.NextNode.PreviousNode);

Если код работает, как ожидается, то должна быть получена XML-разметка первого элемента Employee:

Обход назад от объекта XElement через свойство PreviousNode

Вверх к документу с помощью XObject.Document

Получить XML-документ из объекта XElement можно, обратившись к свойству Document элемента:

...
Console.WriteLine(firstEmployee.Document);

Код дает тот же документ, что и в выводе изначального примера:

Вверх с помощью XObject.Parent

Когда нужно подняться по дереву на один уровень выше, не удивительно, что в этом поможет свойство Parent. Изменение узла, переданного методу WriteLine, как показано ниже, изменит вывод:

...
Console.WriteLine(firstEmployee.Parent);

Вывод соответствующим образом изменится:

Обход вверх от объекта XElement через свойство Parent

Однако не давайте ввести себя в заблуждение. Это не весь документ. Здесь отсутствует описание типа документа и инструкция обработки.

Методы обхода

Чтобы продемонстрировать методы обхода, поскольку они возвращают последовательности из множества узлов, придется заменить единственный вызов метода Console.WriteLine циклом foreach, чтобы вывести потенциально многочисленные узлы. Это приведет к прежнему вызову Console.WriteLine, который выглядит в основном так:

 foreach (XNode node in firstEmployee.Nodes())
     Console.WriteLine(node);

Единственное, что будет изменяться от примера к примеру — вызываемый метод на узле firstEmployee в цикле foreach.

Вниз с помощью XContainer.Nodes()

Спуск по дереву XML осуществляется простым вызовом метода Nodes, который возвращает последовательность объектов XNode данного объекта. Вспомните, что последовательность — это IEnumerable<T>, т.е. IEnumerable определенного типа. Пример приведен ниже:

...
foreach (XNode node in firstEmployee.Nodes())
     Console.WriteLine(node);

Вот вывод:

Обход вниз от объекта XElement через свойство Nodes

He забудьте, что этот метод возвращает все дочерние узлы, а не только элементы. Поэтому любой узел из списка дочерних узлов первого участника будет включен. Сюда могут относиться комментарии (XComment), текст (XText), инструкции обработки (XProcessinglnstruction), типы документа (XDocumentType) или элементы (XElement). Также обратите внимание, что сюда не входят атрибуты, потому что атрибут узлом не является.

Ниже представлен лучший пример использования метода Nodes. Он подобен базовому примеру, но с некоторыми дополнительными узлами:

XElement firstEmployee;

            XDocument xDocument = new XDocument(
              new XDeclaration("1.0", "UTF-8", "yes"),
              new XDocumentType("Employees", null, "Employees.dtd", null),
              new XProcessingInstruction("EmployeeCataloger", "out-of-print"),
                // Обратите внимание, что в следующей строке сохраняется 
                // ссылка на первый элемент
              new XElement("Employees", firstEmployee =
                new XElement("Employee",
                  new XComment("Это программист"),
                  new XProcessingInstruction("ProgrammerHandler", "new"),
                  new XAttribute("type", "Programmer"),
                  new XElement("FirstName", "Alex"),
                  new XElement("LastName", "Erohin")),
                new XElement("Employee",
                  new XAttribute("type", "Editor"),
                  new XElement("FirstName", "Elena"),
                  new XElement("LastName", "Volkova"))));

            foreach (XNode node in firstEmployee.Nodes())
                Console.WriteLine(node);

Этот пример отличается от предыдущего тем, что здесь к первому элементу Employee добавлен комментарий и инструкция обработки. Нажатие <Ctrl+F5> приведет к отображению следующего вывода:

 Обход вниз от объекта XElement через свойство Nodes с дополнительными типами узлов

Теперь можно видеть комментарий и инструкцию обработки. Но что, если нужны только узлы определенного типа, например, только элементы? Ранее была описана операция OfType. Ее можно использовать для возврата только тех узлов, которые относятся к определенному типу, такому как XElement. Используя тот же базовый код, что и в предыдущем пример, давайте обеспечим возврат только элементов, просто изменив строку foreach:

...
foreach (XNode node in firstEmployee.Nodes().OfType<XElement>())
                Console.WriteLine(node);

Как видите, объекты XComment и XProcessingInstruction по-прежнему создаются. Но поскольку вызвана операция OfType, код выдает следующий результат:

Использование операции OfType для возврата только элементов

Видите, насколько интеллектуально работают новые средства языка C# с LINQ? Разве не здорово, что можно использовать эту стандартную операцию запроса для подобного ограничения последовательности узлов XML? Таким образом, если необходимо получить только комментарии из первого элемента Employee, можно воспользоваться операцией OfType:

foreach (XNode node in firstEmployee.Nodes().OfType<XComment>())
                Console.WriteLine(node);
 Использование операции OfType для возврата только комментариев

А можно ли с помощью операции OfТуpe извлечь только атрибуты? Нет, нельзя. Это непростой вопрос. Вспомните, что в отличие от W3C XML DOM API, в LINQ to XML атрибуты не являются узлами дерева XML. Они представляют собой последовательность пар "имя-значение", привязанных к элементу. Чтобы получить атрибуты узла Employee, придется изменить код, как показано ниже:

...
foreach (XAttribute attr in firstEmployee.Attributes())
                Console.WriteLine(attr);
Возврат атрибутов

Вниз с помощью XContainer.Elements()

Поскольку API-интерфейс LINQ to XML настолько сосредоточен на элементах, и именно с ними приходится работать больше всего, был предусмотрен быстрый способ получения только элементов из всех дочерних узлов посредством метода Elements. Это эквивалент вызова метода OfType<XElement> на последовательности, возвращенной методом Nodes:

foreach (XNode node in firstEmployee.Elements())
                Console.WriteLine(node);

Этот код выдает в точности тот же результат, что и код в первом примере вызова операции OfType. Метод Elements также имеет перегруженную версию, которая позволяет передавать имя искомого элемента:

foreach (XNode node in firstEmployee.Elements("FirstName"))
                Console.WriteLine(node);

Этот код выдаст следующий результат:

Обращение к именованным дочерним элементам с использованием метода Elements

Вниз с помощью XContainer.Element()

Метод Element позволяет получить дочерний элемент, соответствующий указанному имени. Вместо возврата последовательности, требующей затем применения цикла foreach, возвращается единственный элемент, как показано ниже:

Console.WriteLine(firstEmployee.Element("LastName"));
Обращение к первому дочернему элементы с указанным именем

Рекурсивно вверх с помощью XNode.Ancestors()

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

Для большей наглядности добавляется ряд дочерних узлов к элементу FirstName первого участника. Кроме того, вместо перечисления предков первого элемента Employee, используется метод Element для спуска двумя уровнями ниже к вновь добавленному элементу NickName. Это предоставит больше предков для демонстрационных целей. Необходимый код показан ниже:

XElement firstEmployee;

            XDocument xDocument = new XDocument(
              new XDeclaration("1.0", "UTF-8", "yes"),
              new XDocumentType("Employees", null, "Employees.dtd", null),
              new XProcessingInstruction("EmployeeCataloger", "out-of-print"),
                // Обратите внимание, что в следующей строке сохраняется 
                // ссылка на первый элемент
              new XElement("Employees", firstEmployee =
                new XElement("Employee",
                  new XComment("Это программист"),
                  new XProcessingInstruction("ProgrammerHandler", "new"),
                  new XAttribute("type", "Programmer"),
                  new XElement("FirstName", 
                      new XText("Alex"),
                      new XElement("NickName","alexsave")),
                  new XElement("LastName", "Erohin")),
                new XElement("Employee",
                  new XAttribute("type", "Editor"),
                  new XElement("FirstName", "Elena"),
                  new XElement("LastName", "Volkova"))));

            foreach (XElement element in firstEmployee
                .Element("FirstName")
                .Element("NickName")
                .Ancestors())
            {
                Console.WriteLine(element.Name);
            }

Обратите внимание на добавление некоторых дочерних узлов к элементу FirstName первого участника. Это приведет к тому, что элемент FirstName первого участника получит содержимое, включающее объект XText, эквивалентный строке "Alex", и дочерний элемент NickName. Элемент NickName элемента FirstName первого участника извлекается, чтобы получить всех его предков.

Вдобавок, для перечисления последовательности, возвращенной методом Ancestors, используется переменная типа XElement, а не XNode. Это позволит добраться до свойства Name элемента. Вместо отображения XML элемента, как это делалось в предыдущих примерах, отображается только имя каждого элемента в последовательности предков. Так сделано во избежание путаницы при отображении XML каждого предка, потому что каждый из них включал бы предыдущий, что затруднило бы восприятие результата. С учетом всего сказанного, вот что получается после запуска примера:

Обход вверх от объекта XElement методом Ancestors

Рекурсивно вверх с помощью XElement.AncestorsAndSelf()

Этот метод работает точно так же, как и Ancestors, но с тем отличием, что в возвращенную последовательность предков он включает сам элемент, на котором вызван. Ниже представлен тот же пример, что и предыдущий, но с вызовом метода AncestorsAndSelf:

...
foreach (XElement element in firstEmployee
       .Element("FirstName")
       .Element("NickName")
       .AncestorsAndSelf())
{
     Console.WriteLine(element.Name);
}

Результат похож на предыдущий, но также включает имя элемента NickName в начале:

Обход вверх от объекта XElement методом AncestorsAndSelf

Рекурсивно вниз с помощью XContainer.Descendants()

В дополнение к рекурсивному обходу вверх можно также рекурсивно обходить вниз с помощью метода Descendants. Этот метод возвращает только элементы. Существует эквивалентный метод DescendantNodes для возврата всех узлов-потомков. Ниже представлен тот же код, что и предыдущий, но с вызовом метода Descendants на элементе первого участника:

...
foreach (XElement element in firstEmployee.Descendants())
{
       Console.WriteLine(element.Name);
}

Результат выглядит следующим образом:

Обход вниз от объекта XElement методом Descendants

Как видите, пример выполняет обход до конца по каждой ветви дерева XML.

Рекурсивно вниз с помощью XElement.DescendantsAndSelf()

Точно так же, как метод Ancestors имеет вариацию AncestorsAndSelf, у метода Descendants есть свой подобный аналог. Метод DescendantsAndSelf работает похоже на Descendants, но с тем отличием, что включает в возвращенную последовательность сам элемент, на котором вызван. Ниже представлен пример, подобный предыдущему, но с вызовом DescendantsAndSelf вместо Descendants:

...
foreach (XElement element in firstEmployee.DescendantsAndSelf())
{
       Console.WriteLine(element.Name);
}

Будет ли вывод включать также имя элемента firstEmployee? Давайте посмотрим:

Обход вниз от объекта XElement методом DescendantsAndSelf

Конечно, будет.

Вперед с помощью XNode.NodesAfterSelf()

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

XElement firstEmployee;

            XDocument xDocument = new XDocument(
              new XDeclaration("1.0", "UTF-8", "yes"),
              new XDocumentType("Employees", null, "Employees.dtd", null),
              new XProcessingInstruction("EmployeeCataloger", "out-of-print"),
                // Обратите внимание, что в следующей строке сохраняется 
                // ссылка на первый элемент
              new XElement("Employees", 
                new XComment("Начало списка"),
                firstEmployee = new XElement("Employee",
                  new XComment("Это программист"),
                  new XProcessingInstruction("ProgrammerHandler", "new"),
                  new XAttribute("type", "Programmer"),
                  new XElement("FirstName", 
                      new XText("Alex"),
                      new XElement("NickName","alexsave")),
                  new XElement("LastName", "Erohin")),
                new XElement("Employee",
                  new XAttribute("type", "Editor"),
                  new XElement("FirstName", "Elena"),
                  new XElement("LastName", "Volkova")),
                new XComment("Конец списка")));

            foreach (XNode node in firstEmployee.NodesAfterSelf())
            {
                Console.WriteLine(node);
            }

Это вызовет перечисление всех соседних узлов первого узла Employee. Вот результат:

Обход вперед от текущего узла с использованием метода NodesAfterSelf

Как видите, последний комментарий включен в вывод потому, что он является узлом. Не позволяйте этому выводу ввести вас в заблуждение. Метод NodesAfterSelf возвращает только два узла: элемент Employee, чей атрибут type равен Editor, и комментарий "Конец списка". Остальные узлы — FirstName и LastName — отображаются просто потому, что вызывается метод ToString узла Employee.

Имейте в виду, что этот метод возвращает узлы, а не элементы. Чтобы ограничить тип возвращаемых узлов, можно воспользоваться операцией OfType, которая демонстрировалась в предшествующих примерах. Но если интересуют элементы, то для этого предусмотрен метод ElementsAfterSelf.

Вперед с помощью XNode.ElementsAfterSelf()

В этом примере используются некоторые модификации документа XML, сделанные в предыдущем примере, которые касаются добавления двух комментариев. Чтобы получить последовательность только соседних элементов после указанного узла, необходимо вызвать метод ElementsAfterSelf:

foreach (XNode node in firstEmployee.ElementsAfterSelf())
{
     Console.WriteLine(node);
}

Этот код выдаст следующий результат:

Обход вперед от текущего узла с использованием метода ElementsAfterSelf

Обратите внимание, что на этот раз комментарий исключен, поскольку не является элементом. Опять-таки, элементы FirstName и LastName отображаются лишь потому, что являются содержимым элемента Employee, который был извлечен, и на этом элементе был вызван метод ToString.

Назад с помощью XNode.NodesBeforeSelf()

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

Метод NodesBeforeSelf работает подобно NodesAfterSelf, но извлекает соседние узлы, расположенные перед текущим. Поскольку начальной ссылкой на документ является первый узел Employee, получается ссылка на второй узел Employee с использованием свойства NextNode первого узла Employee, так что остаются узлы для возврата, как показано ниже:

foreach (XNode node in firstEmployee.NextNode.NodesBeforeSelf())
{
       Console.WriteLine(node);
}
 Обход назад от текущего узла

Как интересно! Возврат двух узлов — комментария и первого Employee — ожидался в обратном порядке. Казалось, что метод начнет с узла, на который произведена ссылка, и построит последовательность через свойство PreviousNode. Возможно, так и случилось, но затем была вызвана операция Reverse или InDocumentOrder. Не позволяйте узлам FirstName и LastName запутать вас. Метод NodesBeforeSelf не возвращал их. Они присутствуют здесь только потому, что методом Console.WriteLine был вызван метод ToString на первом узле Employee, поэтому они были отображены.

Назад с помощью XNode.ElementsBeforeSelf()

В этом примере используются те же модификации документа XML, что были проведены ранее, касающиеся добавления двух комментариев. Подобно тому, как у метода NodesAfterSelf есть сопровождающий метод ElementsAfterSelf для возврата только элементов, так и у метода NodesBeforeSelf имеется дополняющий его метод ElementsBeforeSelf, который возвращает только соседние элементы, предшествующие ссылаемому узлу:

...
foreach (XNode node in firstEmployee.NextNode.ElementsBeforeSelf())
{
        Console.WriteLine(node);
}

Обратите внимание, что здесь опять получается ссылка на второй узел Employee через свойство NextNode. Войдет ли комментарий в вывод?

Обход назад от текущего узла

Конечно, нет, т.к. он не является элементом.

Пройди тесты
Лучший чат для C# программистов