При работе с внешними системами, загрузке прайс-листов, интеграциях с маркетплейсами или разборе каталогов товаров программисты 1С регулярно сталкиваются с необходимостью разбора XML-файлов. Один из наиболее наглядных способов представления иерархических данных в памяти платформы — это объект ДеревоЗначений. Однако при последовательном чтении через стандартный объект ЧтениеXML возникает классическая архитектурная трудность: как правильно обработать атрибуты узлов (например, id или parentId) и не перепутать их с подчиненными элементами (тегами) XML-структуры?
Если записывать атрибуты в ту же коллекцию дочерних строк дерева, где хранятся вложенные теги, структура данных превращается в кашу, которую крайне сложно анализировать. В этой статье мы подробно разберем, как решить данную проблему, и рассмотрим четыре различных подхода: от модернизации базового рекурсивного парсера до использования объектной модели DOM и фабрики XDTO.
Рассмотрим код, с которого начал автор темы. В его реализации используется последовательное чтение узлов с помощью рекурсивной процедуры. При обнаружении начала элемента программа пытается прочитать его атрибуты и добавляет их как новые строки в ту же подчиненную коллекцию строк:
Пока XMLФайл.ПрочитатьАтрибут() Цикл
НоваяСтрокаАтрибут = НоваяСтрока.Строки.Добавить();
НоваяСтрокаАтрибут.Имя = XMLФайл.Имя;
НоваяСтрокаАтрибут.Значение = СокрЛП(XMLФайл.Значение);
КонецЦикла;
Проанализируем эту ситуацию. При таком подходе в коллекции НоваяСтрока.Строки будут одновременно находиться:
ПрочитатьXMLПоТегам.В результате иерархия дерева нарушается. Чтобы понять, где реальный дочерний узел, а где метаданные (атрибуты), приходится писать сложные проверки. Это усложняет алгоритм и снижает скорость обработки данных.
---
Рассмотрим наиболее элегантное исправление текущего рекурсивного парсера. Чтобы не засорять подчиненные строки дерева, мы добавим в ДеревоЗначений третью колонку с именем Атрибуты. В этой колонке для каждого узла мы будем хранить объект Соответствие, в который запишем пары "ИмяАтрибута - ЗначениеАтрибута".
Важное замечание: Мы рекомендуем использовать именно объект Соответствие, а не Структура. Имена атрибутов в XML могут содержать символы, недопустимые для ключей структуры 1С (например, дефисы, двоеточия или начинаться с цифры). Ключом соответствия может быть абсолютно любая строка.
Разберем по шагам модернизированный код:
Функция ЧтениеXMLВДерево(Путь)
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Имя");
Дерево.Колонки.Добавить("Значение");
Дерево.Колонки.Добавить("Атрибуты"); // Колонка для хранения Соответствия атрибутов
XMLФайл = Новый ЧтениеXML;
XMLФайл.ОткрытьФайл(Путь);
ПрочитатьXMLПоТегам(XMLФайл, Дерево.Строки);
XMLФайл.Закрыть();
Возврат Дерево;
КонецФункции
Процедура ПрочитатьXMLПоТегам(XMLФайл, ТекущийНаборСтрок)
Пока XMLФайл.Прочитать() Цикл
Если XMLФайл.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
НоваяСтрока = ТекущийНаборСтрок.Добавить();
НоваяСтрока.Имя = XMLФайл.Имя;
НоваяСтрока.Значение = "";
// Заполняем атрибуты текущего элемента в Соответствие
СоответствиеАтрибутов = Новый Соответствие;
Пока XMLФайл.ПрочитатьАтрибут() Цикл
СоответствиеАтрибутов.Вставить(XMLФайл.Имя, СокрЛП(XMLФайл.Значение));
КонецЦикла;
Если СоответствиеАтрибутов.Количество() > 0 Тогда
НоваяСтрока.Атрибуты = СоответствиеАтрибутов;
КонецЕсли;
// Рекурсивный переход к чтению дочерних элементов
ПрочитатьXMLПоТегам(XMLФайл, НоваяСтрока.Строки);
ИначеЕсли XMLФайл.ТипУзла = ТипУзлаXML.КонецЭлемента Тогда
Возврат;
ИначеЕсли XMLФайл.ТипУзла = ТипУзлаXML.Текст Тогда
// Записываем текстовое значение тега в родительскую строку
ТекущийНаборСтрок.Родитель.Значение = СокрЛП(XMLФайл.Значение);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Теперь проанализируем, как удобно работать с полученным результатом. При обходе дерева значений мы можем легко получить доступ к атрибутам любого узла напрямую по ключу:
Для Каждого СтрокаКатегории Из ДеревоКатегорий.Строки Цикл
Наименование = СтрокаКатегории.Значение;
Если СтрокаКатегории.Атрибуты <> Неопределено Тогда
Идентификатор = СтрокаКатегории.Атрибуты.Получить("id");
РодительИдентификатор = СтрокаКатегории.Атрибуты.Получить("parentId");
Сообщить(Шаблон("Категория: %1 (ID: %2, ParentID: %3)", Наименование, Идентификатор, РодительИдентификатор));
КонецЕсли;
КонецЦикла;
---
Если структура импортируемого XML-файла заранее известна и стабильна (например, это стандартный выгрузочный файл категорий товаров), то использование универсального рекурсивного обхода часто избыточно. Гораздо надежнее и производительнее сразу создать ДеревоЗначений с конкретными типизированными колонками.
Рассмотрим пример реализации парсера под фиксированную структуру тега category:
Функция ПрочитатьКатегорииВТипизированноеДерево(Путь)
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Идентификатор", Новый ОписаниеТипов("Число"));
Дерево.Колонки.Добавить("РодительИдентификатор", Новый ОписаниеТипов("Число"));
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
XMLФайл = Новый ЧтениеXML;
XMLФайл.ОткрытьФайл(Путь);
// Переменная для хранения текущей строки дерева на разных уровнях иерархии
ТаблицаСоответствияРодителей = Новый Соответствие;
Пока XMLФайл.Прочитать() Цикл
Если XMLФайл.ТипУзла = ТипУзлаXML.НачалоЭлемента И XMLФайл.Имя = "category" Тогда
// Читаем атрибуты тега
Ид = 0;
РодительИд = 0;
Пока XMLФайл.ПрочитатьАтрибут() Цикл
Если XMLФайл.Имя = "id" Тогда
Ид = Число(XMLФайл.Значение);
ИначеЕсли XMLФайл.Имя = "parentId" Тогда
РодительИд = Число(XMLФайл.Значение);
КонецЕсли;
КонецЦикла;
// Читаем наименование категории (текст внутри тега)
Наименование = "";
Если XMLФайл.Прочитать() И XMLФайл.ТипУзла = ТипУзлаXML.Текст Тогда
Наименование = СокрЛП(XMLФайл.Значение);
КонецЕсли;
// Определяем, куда добавить строку (в корень или к родителю)
СтрокаРодитель = ТаблицаСоответствияРодителей.Получить(РодительИд);
Если СтрокаРодитель = Неопределено Тогда
НоваяСтрока = Дерево.Строки.Добавить();
Иначе
НоваяСтрока = СтрокаРодитель.Строки.Добавить();
EndIf;
НоваяСтрока.Идентификатор = Ид;
НоваяСтрока.РодительИдентификатор = РодительИд;
НоваяСтрока.Наименование = Наименование;
// Запоминаем строку, чтобы в дальнейшем добавлять в нее подчиненные элементы
ТаблицаСоответствияРодителей.Вставить(Ид, НоваяСтрока);
КонецЕсли;
КонецЦикла;
XMLФайл.Закрыть();
Возврат Дерево;
КонецФункции
Этот метод работает быстрее рекурсивного обхода, поскольку здесь отсутствуют накладные расходы на глубокие вызовы процедур и создание промежуточных коллекций под каждый узел.
---
Если размер XML-файла невелик (до 30–50 МБ), великолепным решением будет использование объектной модели DOM. С помощью объекта ПостроительDOM мы можем загрузить весь документ в оперативную память за одно действие. После этого мы можем производить поиск по тегам, извлекать атрибуты и строить иерархию без ручного пошагового разбора.
Посмотрим на пример практической реализации:
Функция ПрочитатьXMLЧерезDOM(Путь)
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Ид");
Дерево.Колонки.Добавить("РодительИд");
Дерево.Колонки.Добавить("Наименование");
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(Путь);
// Загружаем XML в дерево объектов DOM
Построитель = Новый ПостроительDOM;
ДокументDOM = Построитель.Прочитать(ЧтениеXML);
ЧтениеXML.Закрыть();
// Выбираем из документа все элементы с именем "category"
СписокКатегорий = ДокументDOM.ПолучитьЭлементыПоИмени("category");
Для Каждого ЭлементDOM Из СписокКатегорий Цикл
НоваяСтрока = Дерево.Строки.Добавить();
// Чтение атрибутов DOM-элемента напрямую
АтрибутИд = ЭлементDOM.Атрибуты.ПолучитьИменованныйЭлемент("id");
Если АтрибутИд <> Неопределено Тогда
НоваяСтрока.Ид = Число(АтрибутИд.Значение);
КонецЕсли;
АтрибутРодительИд = ЭлементDOM.Атрибуты.ПолучитьИменованныйЭлемент("parentId");
Если АтрибутРодительИд <> Неопределено Тогда
НоваяСтрока.РодительИд = Число(АтрибутРодительИд.Значение);
КонецЕсли;
// Текстовое содержимое элемента
НоваяСтрока.Наименование = СокрЛП(ЭлементDOM.ТекстовоеСодержимое);
КонецЦикла;
Возврат Дерево;
КонецФункции
Использование DOM делает код чистым, избавляет от необходимости отслеживать состояния парсера, закрывать теги и писать рекурсию. Единственный минус — высокое потребление оперативной памяти при разборе гигабайтных XML-файлов.
---
Механизм XDTO (XML Data Transfer Objects) — это промышленный стандарт платформы 1С для работы со сложными структурированными XML-данными. Он автоматически преобразует элементы и атрибуты XML в понятные программные объекты 1С, свойства которых доступны через обычную точку.
Если у вас нет готовой XSD-схемы, фабрика XDTO может прочитать файл динамически, используя схему по умолчанию:
Функция ПрочитатьXMLЧерезXDTO(Путь)
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(Путь);
// Фабрика XDTO автоматически преобразует файл в дерево объектов
ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
ЧтениеXML.Закрыть();
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Ид");
Дерево.Колонки.Добавить("РодительИд");
Дерево.Колонки.Добавить("Наименование");
// Если "category" - это множественное свойство, XDTO вернет СписокXDTO
Если ТипЗнч(ОбъектXDTO.category) = Тип("СписокXDTO") Тогда
Для Каждого КатегорияXDTO Из ОбъектXDTO.category Цикл
НоваяСтрока = Дерево.Строки.Добавить();
// Обращение к атрибутам происходит мгновенно через точку!
НоваяСтрока.Ид = Число(КатегорияXDTO.id);
НоваяСтрока.РодительИд = Число(КатегорияXDTO.parentId);
// Текстовое содержимое узла в XDTO
НоваяСтрока.Наименование = СокрЛП(КатегорияXDTO.Последовательность().ПолучитьТекст(0));
КонецЦикла;
КонецЕсли;
Возврат Дерево;
КонецФункции
---
Подводя итоги, сопоставим все четыре рассмотренных подхода, чтобы вы могли сделать оптимальный выбор для своей задачи:
Соответствие, иерархия не нарушается. Поддерживает работу с файлами любого объема без риска переполнения памяти.Применяйте наиболее подходящий метод в зависимости от объема ваших файлов XML и требований к гибкости алгоритма, и структура вашего Дерева значений всегда будет оставаться прозрачной и удобной для дальнейшей обработки.