Как извлечь атрибуты XML в Дерево значений 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с внешними системами, загрузке прайс-листов, интеграциях с маркетплейсами или разборе каталогов товаров программисты 1С регулярно сталкиваются с необходимостью разбора XML-файлов. Один из наиболее наглядных способов представления иерархических данных в памяти платформы — это объект ДеревоЗначений. Однако при последовательном чтении через стандартный объект ЧтениеXML возникает классическая архитектурная трудность: как правильно обработать атрибуты узлов (например, id или parentId) и не перепутать их с подчиненными элементами (тегами) XML-структуры?

Если записывать атрибуты в ту же коллекцию дочерних строк дерева, где хранятся вложенные теги, структура данных превращается в кашу, которую крайне сложно анализировать. В этой статье мы подробно разберем, как решить данную проблему, и рассмотрим четыре различных подхода: от модернизации базового рекурсивного парсера до использования объектной модели DOM и фабрики XDTO.

Выясняем причину проблемы: анализ исходного алгоритма

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


Пока XMLФайл.ПрочитатьАтрибут() Цикл
    НоваяСтрокаАтрибут = НоваяСтрока.Строки.Добавить();
    НоваяСтрокаАтрибут.Имя = XMLФайл.Имя;
    НоваяСтрокаАтрибут.Значение = СокрЛП(XMLФайл.Значение);
КонецЦикла;

Проанализируем эту ситуацию. При таком подходе в коллекции НоваяСтрока.Строки будут одновременно находиться:

  1. Строки, представляющие собой атрибуты (например, с именем "id" и "parentId").
  2. Строки, представляющие собой полноценные дочерние XML-элементы, которые считываются при рекурсивном вызове процедуры ПрочитатьXMLПоТегам.

В результате иерархия дерева нарушается. Чтобы понять, где реальный дочерний узел, а где метаданные (атрибуты), приходится писать сложные проверки. Это усложняет алгоритм и снижает скорость обработки данных.

---

Решение 1. Выделение атрибутов в отдельную колонку (Соответствие или Структура)

Рассмотрим наиболее элегантное исправление текущего рекурсивного парсера. Чтобы не засорять подчиненные строки дерева, мы добавим в ДеревоЗначений третью колонку с именем Атрибуты. В этой колонке для каждого узла мы будем хранить объект Соответствие, в который запишем пары "ИмяАтрибута - ЗначениеАтрибута".

Важное замечание: Мы рекомендуем использовать именно объект Соответствие, а не Структура. Имена атрибутов в 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)", Наименование, Идентификатор, РодительИдентификатор));
    КонецЕсли;
    
КонецЦикла;

---

Решение 2. Создание типизированных колонок под известную структуру

Если структура импортируемого 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Файл.Закрыть();
    Возврат Дерево;
    
КонецФункции

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

---

Решение 3. Использование объектной модели документа (ПостроительDOM)

Если размер 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-файлов.

---

Решение 4. Чтение данных через механизм XDTO

Механизм 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 и требований к гибкости алгоритма, и структура вашего Дерева значений всегда будет оставаться прозрачной и удобной для дальнейшей обработки.

← На главную