При организации обмена данными между 1С и внешними системами (веб-сайтами, сторонними базами данных или ERP-системами) разработчики часто сталкиваются с необходимостью разбора файлов формата XML. Для последовательного считывания больших XML-файлов в 1С стандартно применяется объект ЧтениеXML. Однако при попытке самостоятельно отслеживать иерархию элементов с помощью «стека имен» нередко возникают неприятные ошибки: путевые структуры сбиваются, а значения попадают не в те реквизиты.
В этой статье мы подробно проанализируем причину сбоя стека имен при парсинге XML, разберем поведение последовательного считывателя при встрече пустых и самозакрывающихся тегов, а также рассмотрим правильные шаблоны написания кода и альтернативные инструменты платформы 1С.
В спецификации XML пустой элемент может быть представлен двумя способами:
<img></img><img/>С точки зрения стандарта XML и парсера 1С обе эти записи эквивалентны. Когда объект ЧтениеXML выполняет последовательный проход по файлу с помощью метода Прочитать(), для самозакрывающегося тега <img/> генерируются два последовательных состояния узла:
Парсер.ТипУзла = ТипУзлаXML.НачалоЭлемента (имя узла — "img").Парсер.ТипУзла = ТипУзлаXML.КонецЭлемента (имя узла — "img").Если алгоритм построен на заполнении стека при ТипУзлаXML.НачалоЭлемента и удалении из стека при ТипУзлаXML.КонецЭлемента, структура стека должна сохранять целостность. Почему же у разработчиков ломается стек тегов?
Проанализируем типичную ошибку в коде, приводящую к рассинхронизации стека. Разберем фрагмент кода, где выполняется попытка считать значение элемента сразу после его открытия:
Пока Парсер.Прочитать() Цикл
Если Парсер.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
СтекИмен.Добавить(Парсер.Имя);
ПолныйПуть = ПостроитьПуть(СтекИмен);
Если ПолныйПуть = "/section/groups/group" Тогда
Группа = ТЗгр.Добавить();
Группа.id = Парсер.ЗначениеАтрибута("id");
// ОШИБКА: Дополнительный вызов чтения прямо внутри условия!
Парсер.Прочитать();
Группа.Наименование = Парсер.Значение;
КонецЕсли;
ИначеЕсли Парсер.ТипУзла = ТипУзлаXML.КонецЭлемента Тогда
СтекИмен.Удалить(СтекИмен.Количество() - 1);
КонецЕсли;
КонецЦикла;
Выясним причину, почему при таком подходе сбивается путь. Когда внутри условия выполняется принудительный вызов Парсер.Прочитать(), считыватель смещает указатель на следующий узел (например, на узел текста или закрывающий тег КонецЭлемента).
На следующем шаге основного цикла Пока Парсер.Прочитать() Цикл считыватель перемещается еще дальше! В результате событие ТипУзлаXML.КонецЭлемента проскакивает мимо ветки ИначеЕсли основного цикла, и закрывающий тег не удаляется из стека. При обработке последующих элементов стек накапливает «фантомные» открытые теги, и условие проверки полного пути перестает срабатывать.
Чтобы стек имен работал корректно, запомним главное правило: внутри основного цикла `Пока Парсер.Прочитать() Цикл` никогда нельзя принудительно вызывать `Парсер.Прочитать()`. Каждая итерация цикла должна обрабатывать ровно одно текущее состояние парсера.
Рассмотрим правильный шаблон разбора XML-файла через ЧтениеXML со стеком имен:
Функция ПостроитьПуть(Стек)
Результат = "";
Для Каждого ЭлементИзСтека Из Стек Цикл
Результат = Результат + "/" + ЭлементИзСтека.Значение;
КонецЦикла;
Возврат Результат;
КонецФункции
Процедура ПрочитатьXMLФайл(ПутьКФайлу)
Парсер = Новый ЧтениеXML;
Парсер.ОткрытьФайл(ПутьКФайлу);
СтекИмен = Новый СписокЗначений;
ТекущийПуть = "";
Пока Парсер.Прочитать() Цикл
ТипУзла = Парсер.ТипУзла;
Если ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
// 1. Фиксируем открытие элемента в стеке
СтекИмен.Добавить(Парсер.Имя);
ТекущийПуть = ПостроитьПуть(СтекИмен);
// Если у элемента есть атрибуты, считываем их сразу
Если ТекущийПуть = "/part/groups/group" Тогда
НоваяСтрока = ТЗгр.Добавить();
НоваяСтрока.id = Парсер.ЗначениеАтрибута("id");
НоваяСтрока.parent = Парсер.ЗначениеАтрибута("parent");
КонецЕсли;
ИначеЕсли ТипУзла = ТипУзлаXML.Текст Тогда
// 2. Текстовый узел обрабатывается на отдельной итерации цикла
Если ТекущийПуть = "/part/groups/group" Тогда
// Записываем текстовое значение в ранее созданную строку
НоваяСтрока = ТЗгр[ТЗгр.Количество() - 1];
НоваяСтрока.Наименование = СокрЛП(Парсер.Значение);
ИначеЕсли ТекущийПуть = "/part/name" Тогда
// Обработка наименования детали
КонецЕсли;
ИначеЕсли ТипУзла = ТипУзлаXML.КонецЭлемента Тогда
// 3. При закрытии тега обязательно удаляем его из стека
Если СтекИмен.Количество() > 0 Тогда
СтекИмен.Удалить(СтекИмен.Количество() - 1);
ТекущийПуть = ПостроитьПуть(СтекИмен);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Парсер.Закрыть();
КонецПроцедуры
При таком подходе самозакрывающийся тег (например, <img/>) отработает строго по шагам: сначала добавится в стек в блоке НачалоЭлемента, а на следующей итерации без проблем удалится в блоке КонецЭлемента. Стек останется идеальным на протяжении всего файла.
Последовательное чтение через ЧтениеXML с ручным управлением стеком — это низкоуровневый подход. Платформа 1С предоставляет более простые и удобные инструменты для работы с XML.
Если файл XML имеет небольшое или среднее количество данных (до 50–100 МБ), предпочтительно загрузить его в объект ДокументDOM. Это позволит обращаться к узлам через XPath-выражения без циклов и стеков.
Проанализируем пример чтения с помощью DOM:
ПарсерXML = Новый ЧтениеXML;
ПарсерXML.ОткрытьФайл(ПутьКФайлу);
ПостроительDOM = Новый ПостроительDOM;
ДокументDOM = ПостроительDOM.Прочитать(ПарсерXML);
// Получаем все узлы group с помощью XPath
ВыражениеXPath = "/part/groups/group";
РезультатXPath = ДокументDOM.ВычислитьВыражениеXPath(ВыражениеXPath);
Узел = РезультатXPath.ПолучитьСледующий();
Пока Узел <> Неопределено Цикл
Идентификатор = Узел.Атрибуты.ПолучитьИменнуюВещь("id").Значение;
Родитель = Узел.Атрибуты.ПолучитьИменнуюВещь("parent").Значение;
Наименование = Узел.ТекстовоеСодержимое;
// Сохраняем данные в справочник или таблицу
Узел = РезультатXPath.ПолучитьСледующий();
КонецЦикла;
ПарсерXML.Закрыть();
Многие разработчики избегают XDTO, считая, что для этого обязательно нужно создавать сложную схему XML (XSD) в конфигураторе. Однако ФабрикаXDTO может автоматически десериализовать фрагменты XML прямо в объекты XDTO или структуры 1С.
Рассмотрим, как прочитать XML с помощью ФабрикаXDTO за пару строк:
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
// Перейти к корневому или нужному узлу
ЧтениеXML.ПереместитьКСодержимому();
// Читаем весь XML или отдельный элемент в объект XDTO
ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
// Обращаемся к свойствам напрямую через точку
Наименование = ОбъектXDTO.name;
Артикул = ОбъектXDTO.art;
// Чтение вложенных коллекций
Для Каждого ЭлементГруппа Из ОбъектXDTO.groups.group Цикл
КодГруппы = ЭлементГруппа.Последовательность().ПолучитьТекст(0);
Родитель = ЭлементГруппа.parent;
КонецЦикла;
ЧтениеXML.Закрыть();
Мы выяснили, что самозакрывающиеся теги <img/> являются стандартом XML и корректно поддерживаются платформой 1С. Главная причина ошибок при их обработке — нарушающие логику работы парсера принудительные вызовы Парсер.Прочитать() внутри цикла. Соблюдайте однородность цикла при работе с ЧтениеXML либо используйте более современные механизмы ДокументDOM и ФабрикаXDTO для упрощения кода интеграции.