Обмен данными через формат XML остается одним из самых распространенных способов интеграции 1С с внешними системами. Однако при чтении файлов с вложенной структурой (например, когда документ содержит сведения о контрагенте в атрибутах, а внутри него расположен список товаров) начинающие разработчики часто совершают логические ошибки. В этой статье мы подробно разберем, как правильно организовать чтение таких файлов, исправим типичные ошибки построения циклов и рассмотрим альтернативные, более современные и удобные способы работы с XML в платформе 1С:Предприятие.
Для начала проанализируем структуру XML-файла, которую пытается прочитать автор темы:
<doc Контрагент="Вася Пупкин" Водитель="Иванов И.В." АдресДоставки="623780, Свердловская обл, ">
<tovar Номенклатура="Гвозди "Количество="42"/>
<tovar Номенклатура="Гвозди" Количество="20"/>
<tovar Номенклатура="Шурупы" Количество="6"/>
<tovar Номенклатура="Болты" Количество="6"/>
</doc>
При детальном рассмотрении первой строки с товаром мы сразу обнаруживаем критическую синтаксическую ошибку (так называемый «битый XML»):
<tovar Номенклатура="Гвозди "Количество="42"/>
Обратите внимание на кавычку после слова «Гвозди». Она закрывает значение атрибута Номенклатура, после чего сразу идет слово Количество без пробела-разделителя, либо парсер воспримет это как некорректное объявление нового атрибута. При попытке прочитать такой файл стандартный механизм 1С ЧтениеXML немедленно вызовет исключение «Ошибка разбора XML» (XML parsing error) и аварийно прекратит работу.
Как это исправить: Данная проблема должна решаться на стороне системы-отправителя, которая формирует файл. Символы кавычек внутри значений атрибутов должны быть экранированы (например, заменены на специальную сущность ") или просто удалены:
<tovar Номенклатура="Гвозди "" Количество="42"/> или <tovar Номенклатура="Гвозди" Количество="42"/>.
---
В первом сообщении автор пытается читать файл последовательно, используя объект ЧтениеXML, и совершает ошибку, создавая внутренний цикл для чтения товаров:
Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента И ЧтениеXML.Имя = "doc" Тогда
// Чтение атрибутов документа...
Пока ЧтениеXML.Имя = "tovar" Цикл
// Чтение товаров...
КонецЦикла;
КонецЕсли;
Проанализируем, почему этот код не работает. Объект ЧтениеXML работает по принципу последовательного однопроходного сканера (StAX-парсер). Метод Прочитать() сдвигает указатель на следующий узел XML-документа.
Когда парсер заходит в условие ЧтениеXML.Имя = "doc", текущим узлом является сам элемент <doc>. Сразу после этого программа заходит во внутренний цикл Пока ЧтениеXML.Имя = "tovar". Но в этот момент имя текущего узла все еще "doc"! Условие цикла сразу же возвращает значение Ложь, и этот цикл завершается, даже не успев начаться. Если же принудительно вызвать ЧтениеXML.Прочитать() внутри этого цикла, мы собьем шаг основного цикла обхода файла.
Правило работы с ЧтениеXML: Обход документа всегда должен выполняться в одном-единственном цикле Пока ЧтениеXML.Прочитать() Цикл. Внутри этого цикла мы просто проверяем тип узла и его имя с помощью условных операторов Если ... ИначеЕсли, как справедливо заметили участники форума в сообщениях 3 и 4.
---
В сообщении 5 автор радуется, что избавился от внутреннего цикла и код начал «читать» все узлы. Однако его финальный алгоритм содержит скрытую архитектурную ошибку:
Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента И ЧтениеXML.Имя = "doc" Тогда
ст = Результат.Добавить();
ст.Контрагент = ЧтениеXML.ЗначениеАтрибута("Контрагент");
// ...
ИначеЕсли ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента И ЧтениеXML.Имя = "tovar" Тогда
строк = Результат.Добавить();
строк.Номенклатура = ЧтениеXML.ЗначениеАтрибута("Номенклатура");
// ...
КонецЕсли;
Если Результат — это плоская таблица значений, то этот код запишет данные следующим образом:
doc создается первая строка таблицы. В ней заполняются поля Контрагент, Водитель и Адрес. Поля Номенклатура и Количество остаются пустыми.tovar для каждого товара создается новая отдельная строка таблицы. В ней заполняются поля товара, но поля контрагента и водителя остаются пустыми!В результате мы получаем разорванные данные. Если в XML-файле будет несколько документов doc, мы вообще не сможем определить, какие товары к какому контрагенту относились. Связь между родителем и потомком полностью теряется.
---
Чтобы решить проблему потери связи при последовательном чтении, мы должны сохранять контекст «шапки» документа во временных переменных. При нахождении каждого нового элемента tovar мы создаем строку таблицы и записываем в нее как данные самого товара, так и сохраненные данные «шапки».
Рассмотрим по шагам правильную реализацию этого подхода:
// Создаем таблицу для накопления результатов
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Водитель", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Адрес", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ПутьКФайлу = "C:\temp\data.xml";
ЧтениеXML = Новый ЧтениеXML;
Попытка
ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
Исключение
Сообщить("Не удалось открыть файл: " + ОписаниеОшибки());
Возврат;
КонецПопытки;
// Инициализируем переменные для хранения контекста текущего документа
ТекущийКонтрагент = "";
ТекущийВодитель = "";
ТекущийАдрес = "";
// Основной цикл последовательного чтения файла
Пока ЧтениеXML.Прочитать() Цикл
// Проверяем, что парсер находится на начале элемента
Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
Если ЧтениеXML.Имя = "doc" Тогда
// Запоминаем контекст шапки документа во временные переменные
ТекущийКонтрагент = ЧтениеXML.ЗначениеАтрибута("Контрагент");
ТекущийВодитель = ЧтениеXML.ЗначениеАтрибута("Водитель");
ТекущийАдрес = ЧтениеXML.ЗначениеАтрибута("АдресДоставки");
ИначеЕсли ЧтениеXML.Имя = "tovar" Тогда
// Создаем строку таблицы и заполняем ее данными товара и контекстом документа
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Контрагент = ТекущийКонтрагент;
НоваяСтрока.Водитель = ТекущийВодитель;
НоваяСтрока.Адрес = ТекущийАдрес;
НоваяСтрока.Номенклатура = ЧтениеXML.ЗначениеАтрибута("Номенклатура");
// Важно преобразовывать строковые значения атрибутов к нужным типам данных
ЗначениеКоличества = ЧтениеXML.ЗначениеАтрибута("Количество");
НоваяСтрока.Количество = ?(ЗначениеЗаполнено(ЗначениеКоличества), Число(ЗначениеКоличества), 0);
КонецЕсли;
КонецЕсли;
КонецЦикла;
ЧтениеXML.Закрыть();
Этот метод работает максимально быстро и потребляет минимальное количество оперативной памяти. Мы рекомендуем использовать его для разбора огромных файлов (размером в десятки и сотни мегабайт).
---
Для файлов небольшого и среднего размера (до 10–20 мегабайт) гораздо удобнее использовать объектную модель документов — DOM (Document Object Model). Этот подход считывает весь файл в оперативную память и представляет его в виде иерархического дерева объектов 1С. Нам больше не нужно вручную контролировать указатель чтения и сохранять контекст в переменных.
Проанализируем, как изящно решается наша задача с помощью механизма ПостроительDOM:
ПутьКФайлу = "C:\temp\data.xml";
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
// Загружаем XML-документ в дерево DOM
ПостроительDOM = Новый ПостроительDOM;
ДокументDOM = ПостроительDOM.Прочитать(ЧтениеXML);
ЧтениеXML.Закрыть();
// Получаем список всех узлов "doc"
ВсеДокументы = ДокументDOM.ПолучитьЭлементыПоИмени("doc");
Для Каждого УзелDoc Из ВсеДокументы Цикл
// Извлекаем атрибуты шапки документа напрямую из узла
Контрагент = УзелDoc.Атрибуты.ПолучитьИменованныйЭлемент("Контрагент").Значение;
Водитель = УзелDoc.Атрибуты.ПолучитьИменованныйЭлемент("Водитель").Значение;
Адрес = УзелDoc.Атрибуты.ПолучитьИменованныйЭлемент("АдресДоставки").Значение;
// Получаем вложенные элементы "tovar", принадлежащие ТОЛЬКО текущему узлу "doc"
ВсеТоварыДокумента = УзелDoc.ПолучитьЭлементыПоИмени("tovar");
Для Каждого УзелTovar Из ВсеТоварыДокумента Цикл
Номенклатура = УзелTovar.Атрибуты.ПолучитьИменованныйЭлемент("Номенклатура").Значение;
Количество = Число(УзелTovar.Атрибуты.ПолучитьИменованныйЭлемент("Количество").Значение);
// Здесь мы можем сразу создавать документы в базе данных или заполнять ТаблицуЗначений
// Данные гарантированно связаны между собой!
Сообщить(СтрШаблон("Контрагент: %1, Товар: %2, Кол-во: %3", Контрагент, Номенклатура, Количество));
КонецЦикла;
КонецЦикла;
Использование DOM избавляет разработчика от необходимости писать сложные условия и следить за открытием и закрытием тегов. Код получается чистым, понятным и легко сопровождаемым.
---
Наиболее современным, технологичным и правильным с точки зрения стандартов 1С является чтение XML через XDTO (XML Data Transfer Objects). Если для вашего XML-файла есть схема (XSD) или структура файла простая и стабильная, вы можете прочитать файл буквально одной строкой кода, преобразовав его в полноценный объект 1С с типизированными свойствами.
Рассмотрим пример чтения, если в нашей конфигурации создана схема пакета XDTO, описывающая структуру файла:
ПутьКФайлу = "C:\temp\data.xml";
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
// Читаем XML напрямую в объект XDTO
Попытка
// ФабрикаXDTO автоматически преобразует типы данных (строки в строки, числа в числа)
ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
Исключение
Сообщить("Ошибка чтения XDTO: " + ОписаниеОшибки());
ЧтениеXML.Закрыть();
Возврат;
КонецПопытки;
ЧтениеXML.Закрыть();
// Теперь мы работаем с данными как с обычным объектом 1С
// Свойства "Контрагент", "Водитель" и список "tovar" доступны через точку
Сообщить("Контрагент: " + ОбъектXDTO.Контрагент);
Сообщить("Водитель: " + ОбъектXDTO.Водитель);
Для Каждого СтрокаТовара Из ОбъектXDTO.tovar Цикл
// Количество автоматически имеет тип Число, если это было указано в схеме XSD
Сообщить(СтрШаблон("Товар: %1, Количество: %2", СтрокаТовара.Номенклатура, СтрокаТовара.Количество));
КонецЦикла;
Преимущества XDTO:
Число() или Дата()).При работе с вложенными структурами XML всегда сопоставляйте объем файлов и требования к производительности. Для больших объемов используйте последовательное ЧтениеXML, не забывая сохранять переменные контекста родительских узлов. Для средних и небольших файлов отдавайте предпочтение ПостроителюDOM или технологии XDTO, чтобы писать лаконичный, надежный и современный код.