Создание XML-документа: LinqXml10
Задания группы LinqXml посвящены технологии LINQ to XML
интерфейсу LINQ, предназначенному для обработки документов XML.
Интерфейс LINQ to XML включает, помимо дополнительных методов
расширения, набор классов, связанных с различными компонентами XML.
Этот набор образует объектную модель документа XML (XML Document
Object Model XML DOM), которую мы в дальнейшем для краткости
будем обозначать X-DOM. Основные классы, входящие в X-DOM, а также
понятия, используемые при работе с XML-документами, кратко описаны в
преамбуле к группе LinqXml. Многие классы, входящие в X-DOM,
имеют свойства, возвращающие различные последовательности;
некоторые методы классов (в частности, их конструкторы) могут
принимать последовательности в качестве своих параметров. Во всех
ситуациях связанных с обработкой последовательностей, можно
использовать базовые запросы LINQ,
входящие в интерфейс LINQ to Objects и изученные нами при выполнении
заданий из групп LinqBegin и LinqObj.
Знакомство с возможностями LINQ to XML естественно начать с
создания XML-документов. Этой теме посвящена первая подгруппа
группы LinqXml. Рассмотрим последнее из заданий, входящих в
эту подгруппу.
LinqXml10°. Даны имена существующего текстового файла и создаваемого XML-документа. Создать XML-документ с корневым элементом root, элементами первого уровня line и инструкциями обработки (инструкции обработки являются дочерними узлами корневого элемента). Если строка текстового файла начинается с текста «data:», то она (без текста «data:») добавляется в XML-документ в качестве данных к очередной инструкции обработки с именем instr, в противном случае строка добавляется в качестве дочернего текстового узла в очередной элемент line.
После создания с помощью программного модуля PT4Load
проекта-заготовки для данного задания, автоматического запуска
среды Visual Studio и загрузки в нее созданного проекта на экране
будет отображен файл LinqXml10.cs. Приведем содержимое этого
файла:
// File: "LinqXml10"
using PT4;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
namespace PT4Tasks
{
public class MyTask : PT
{
// When solving tasks of the LinqXml group, the following
// additional methods defined in the taskbook are available:
// (*) Show() and Show(cmt) (extension methods) - debug output
// of a sequence, cmt - string comment;
// (*) Show(e => r) and Show(cmt, e => r) (extension methods) -
// debug output of r values, obtained from elements e
// of a sequence, cmt - string comment.
public static void Solve()
{
Task("LinqXml10");
}
}
}
Подобно ранее рассмотренным заготовкам, создаваемым для заданий
групп LinqBegin и LinqObj, этот файл содержит набор директив using и
описание класса MyTask с функцией Solve, в которую требуется ввести
решение задачи. В комментарии,
предшествующем функции Solve, кратко описываются варианты метода
Show, предназначенного для отладочной печати
последовательностей.
Заметим, что список директив using содержит директиву
подключения пространства имен System.Xml.Linq, с которым связаны все
классы модели X-DOM.
В созданной заготовке отсутствуют дополнительные методы для
ввода-вывода последовательностей (подобные методам GetEnumerableInt и
GetEnumerableString и методу расширения Put, используемым при
выполнении заданий группы LinqBegin). Это связано с тем, что в
большинстве заданий требуется обработать XML-документ, хранящийся
во внешнем файле, и записать в этот же файл результат обработки. Для
операций файлового чтения-записи XML-документов предусмотрены
специальные методы класса XDocument, которые и следует использовать
при выполнении заданий. Исключение составляют задания на создание
XML-документов, исходные данные для которых содержатся в «обычных»
текстовых файлах. При выполнении подобных заданий, как и заданий
группы LinqObj, следует использовать метод File.ReadLines,
обеспечивающий чтение всех строк исходного текстового файла в
строковую последовательность IEnumerable<string>. Для ввода имен файлов и дополнительных
строковых данных следует, как обычно, использовать функцию задачника
GetString; для вывода данных базовых типов (подобный вывод требуется
только в заданиях подгруппы, связанной с анализом содержимого XML-документа) следует использовать функцию Put, также
определенную в задачнике.
После запуска созданной программы-заготовки мы увидим на экране
окно задачника, содержащее формулировку задания, а также пример
исходных данных и правильных результатов:
Хотя файловые данные на приведенном рисунке отображаются в
сокращенном виде, указанные фрагменты демонстрируют все особенности
этих данных. В исходном текстовом файле содержатся строки,
представляющие собой наборы слов, причем некоторые строки
начинаются с текста «data:» (на рисунке этот текст содержит первая
строка); результирующий файл содержит XML-документ в кодировке
«us-ascii», включающий корневой элемент root , дочерними узлами
которого (т. е. узлами первого уровня, поскольку корневой элемент
считается узлом нулевого уровня) являются элементы line и инструкции
обработки instr .
Элемент XML-документа обязательно имеет имя; он может быть
представлен в виде парных тегов вида <имя>...</имя> (где на месте
многоточия могут располагаться дочерние узлы этого элемента) или в виде
комбинированного тега <имя /> . Элемент может также содержать
атрибуты.
Инструкция обработки является особым компонентом XML-документа,
который заключается в скобки вида <? ?> (заметим, что
объявление XML-документа, с которого он начинается, также оформляется
подобным образом). Первое слово в этих скобках считается именем
инструкции обработки (точнее, именем ее программы-получателя), а
последующий текст данными, связанными с инструкцией. Инструкции
обработки называются так потому, что они определяют не содержимое
XML-документа, а дополнительные данные, предназначенные для
программ, обрабатывающих этот документ. Разумеется, в образцах XML-документов,
генерируемых задачником, используются инструкции
обработки, не связанные ни с какими программами и имеющие лишь
формальные признаки «настоящих» инструкций.
Данные для инструкций instr должны извлекаться из строк исходного
файла, начинающихся с текста «data:»; прочие строки файла должны
использоваться в качестве содержимого элементов line . Таким образом,
при выполнении задания нам потребуется сформировать на основе
строковой последовательности, полученной из исходного файла,
последовательность дочерних узлов корневого элемента root для
созданного XML-документа.
Как уже было отмечено, исходную строковую последовательность
проще всего получить с помощью метода File.ReadLines. Начиная с версии 1.3 задачника PT for LINQ, все
текстовые файлы, связанные с заданиями, содержат только символы из набора ASCII, поэтому
при вызове метода ReadLine не нужно указывать
дополнительный параметр, определяющий кодировку символов (символы будут правильно обрабатываться
с применением кодировки UTF-8, которая выбирается по умолчанию):
var a = File.ReadLines(GetString());
Для создания как самого XML-документа, так и его компонентов,
следует использовать конструкторы соответствующих классов. При
выполнении данного задания нам потребуются следующие классы,
входящие в X-DOM: XDocument (XML-документ), XDeclaration
(объявление документа), XElement (элемент) и XProcessingInstruction
(инструкция обработки).
Важной особенностью конструкторов классов XDocument и XElement
является то, что они позволяют сразу указывать содержимое создаваемого
документа (соответственно, элемента) в виде списка параметров, среди
которых могут быть и параметры-последовательности (в случае
параметров-последовательностей в создаваемый документ или элемент
добавляются все элементы этих последовательностей). Указанной
особенностью обладают и некоторые другие методы классов X-DOM.
Благодаря этой особенности документ XML вместе с его содержимым
может быть построен в одном выражении, причем при формировании
содержимого можно использовать последовательности, получаемые с
помощью методов LINQ. Подобный способ формирования XML-документа
называется функциональным конструированием. Его
достоинствами является краткость и наглядность получаемого кода, по
виду которого можно легко определить структуру определяемого
XML-документа или любого его фрагмента.
В качестве примера функционального конструирования XML-документа
приведем фрагмент, который по последовательности строк a
(полученной ранее из исходного текстового файла) формирует документ с
корневым элементом root и элементами первого уровня line с текстовыми
значениями, взятыми из последовательности a в том же порядке:
XDocument d = new XDocument(
new XDeclaration(null, "us-ascii", null),
new XElement("root",
a.Select(e => new XElement("line", e))));
В этом фрагменте вначале вызывается конструктор класса
XDocument; в списке его параметров указывается конструктор, создающий
объявление документа (в нем достаточно явным образом определить
второй параметр, определяющий кодировку документа), и конструктор,
создающий элемент нулевого уровня root . В конструкторе элемента root
вначале указывается его имя, а затем, в последующих параметрах, его
содержимое. В данном случае содержимое представляет собой
последовательность элементов (т. е. объектов типа XElement),
полученную из исходной строковой последовательности с помощью
метода проецирования Select. Лямбда-выражение метода Select
содержит вызов конструктора для объекта XElement; в нем в
качестве имени указывается строковая константа «line», а в качестве
содержимого строка e .
В приведенном фрагменте не обрабатываются особым образом строки
исходной последовательности, начинающиеся с текста «data:» (которые
надо преобразовывать не в элементы, а в инструкции обработки). Этот
недочет мы исправим позже.
Осталось сохранить полученный XML-документ под требуемым
именем. Для этого достаточно использовать метод Save класса XDocument,
указав имя в качестве параметра:
d.Save(GetString());
При вызове этого метода не требуется дополнительно указывать
используемую кодировку, поскольку она была ранее указана в объявлении
XML-документа. Заметим также, что метод Save по умолчанию форматирует
XML-документ при его сохранении в файле, выводя каждый узел на
отдельной строке (или на нескольких строках, если узел является
элементом, содержащим набор дочерних элементов) и добавляя в начало
каждой строки необходимые отступы.
Объединяя указанные выше операторы, получаем первый (пока еще
не вполне правильный) вариант решения:
public static void Solve()
{
Task("LinqXml10");
var a = File.ReadLines(GetString());
XDocument d = new XDocument(
new XDeclaration(null, "us-ascii", null),
new XElement("root",
a.Select(e => new XElement("line", e))));
d.Save(GetString());
}
При запуске программы в окне будет выведено сообщение об
ошибочном решении (для уменьшения размеров окна в нем
скрыт раздел с формулировкой задания):
Для получения XML-документа, соответствующего условиям задачи,
надо при создании документа d предусмотреть специальную обработку
строк исходного файла, начинающихся с текста «data:», создавая для них
вместо объектов XElement объекты XProcessingInstruction. Подобную
обработку можно реализовать, включив в лямбда-выражение тернарную
операцию:
e => e.StartsWith("data:") ?
new XProcessingInstruction("instr", e.Substring(5)) :
new XElement("line", e)
При создании инструкции обработки в качестве ее данных
указывается подстрока строки e без начального текста «data:» (число 5
определяет индекс начального символа подстроки; напомним, что
индексация символов начинается от 0).
Однако при попытке откомпилировать полученную программу будет
выведено сообщение об ошибке компиляции. Ошибка заключается в том,
что по приведенному лямбда-выражению нельзя определить тип
элементов возвращаемой последовательности (часть элементов будет
иметь тип XProcessingInstruction, а часть тип XElement). Чтобы
исправить данную ошибку, следует дополнить тернарное выражение
операцией приведения к некоторому общему типу, который и будет
считаться типом элементов возвращаемой последовательности. В качестве
такого общего типа можно указать любой тип, являющийся предком как
класса XProcessingInstruction, так и класса XElement. Ближайшим общим
предком этих классов является класс XNode, являющийся абстрактным
базовым классом для всех компонентов-узлов XML-документа (класс
XDocument также является потомком класса XNode). Используя операцию
приведения типа as, получаем следующий вариант лямбда-выражения
(добавленный фрагмент выделен полужирным шрифтом):
e => e.StartsWith("data:") ?
new XProcessingInstruction("instr", e.Substring(5)) :
new XElement("line", e) as XNode
Вместо типа XNode, мы могли бы использовать и другие общие
классы-предки классов XProcessingInstruction и XElement, например,
XObject (общий абстрактный базовый класс для узлов и атрибутов XML-документа)
или object (общий предок всех классов .NET).
Подчеркнем, что приведение типа не изменяет
фактический тип элементов последовательности, оно лишь обеспечивает
единообразную интерпретацию всех этих элементов как XML-узлов, давая тем самым
компилятору возможность определить тип получаемой
последовательности (в нашем случае IEnumerable<XNode>).
Приведем окончательный вариант решения:
public static void Solve()
{
Task("LinqXml10");
var a = File.ReadLines(GetString());
XDocument d = new XDocument(
new XDeclaration(null, "us-ascii", null),
new XElement("root",
a.Select(e => e.StartsWith("data:") ?
new XProcessingInstruction("instr", e.Substring(5)) :
new XElement("line", e) as XNode)));
d.Save(GetString());
}
После пяти тестовых испытаний программы мы получим сообщение о
том, что задание выполнено.
|