При обмене данными с внешними системами разработчики часто сталкиваются с ситуацией, когда полученный XML-файл не соответствует стандартам W3C. Одной из самых распространенных ошибок является наличие в одном файле нескольких XML-прологов (заголовков <?xml ... ?>) или нескольких корневых элементов. Согласно спецификации, корректный XML-документ должен иметь строго один корневой элемент, в который вложены все остальные данные.
Стандартный объект 1С ЧтениеXML при попытке обработать такой файл выдает ошибку, так как он ожидает увидеть завершение документа после закрытия первого корневого тега. В этой статье мы подробно разберем, как программно преобразовать такой «составной» файл в правильный XML-документ, который платформа 1С сможет прочитать без ошибок.
Рассмотрим ситуацию, когда внешняя система (например, некорректно настроенный веб-сервис или скрипт выгрузки) формирует отчеты один за другим в один и тот же файл, просто дописывая их в конец. В итоге получается структура следующего вида:
<?xml version="1.0" encoding="windows-1251"?>
<Report>
<Record>Данные 1</Record>
</Report>
<?xml version="1.0" encoding="windows-1251"?>
<Report>
<Record>Данные 2</Record>
</Report>
Для 1С это не один документ, а два разных документа, склеенных в один файл. Объект ЧтениеXML прочитает первый блок Report и, встретив заголовок второго блока, прервет работу с ошибкой «строка содержит недопустимый символ» или «обнаружено несколько корневых элементов». Наша задача — превратить это в единую структуру с одним общим корнем.
Если размер файла не превышает нескольких десятков мегабайт, мы можем воспользоваться объектом ТекстовыйДокумент. Этот метод прост в реализации, так как позволяет считать весь текст файла в оперативную память, выполнить замены и записать его обратно.
Рассмотрим алгоритм действий по шагам:
windows-1251 или UTF-8).Проанализируем пример кода, который реализует эту логику:
Процедура ПроверитьИИсправитьФайлXML(ИмяФайлаДанных)
// Создаем объект для работы с текстом
Текст = Новый ТекстовыйДокумент;
// Читаем файл. Важно указать кодировку, в которой пришел файл.
Текст.Прочитать(ИмяФайлаДанных, "windows-1251");
// Получаем все содержимое в одну строковую переменную
СтрокаXML = Текст.ПолучитьТекст();
// Определяем маркеры заголовков и корневых элементов (например, ReportXML)
МаркерЗаголовка = "<?xml version=""1.0"" encoding=""windows-1251""?>";
МаркерНачалаКорня = "<ReportXML>";
МаркерКонцаКорня = "</ReportXML>";
// Проверяем, есть ли дубликаты
ЧислоВхожденийКодировки = СтрЧислоВхождений(СтрокаXML, МаркерЗаголовка);
Если ЧислоВхожденийКодировки > 1 Тогда
// Удаляем все упоминания заголовков, чтобы потом добавить один в начало
СтрокаXML = СтрЗаменить(СтрокаXML, МаркерЗаголовка, "");
// Удаляем все открывающие и закрывающие корневые теги
// Таким образом, мы очищаем текст от промежуточных разделителей
СтрокаXML = СтрЗаменить(СтрокаXML, МаркерНачалаКорня, "");
СтрокаXML = СтрЗаменить(СтрокаXML, МаркерКонцаКорня, "");
// Теперь собираем правильную структуру вручную:
// Один заголовок + один открывающий корень + все данные + один закрывающий корень
ИтоговаяСтрокаXML = МаркерЗаголовка + Символы.ПС
+ МаркерНачалаКорня + Символы.ПС
+ СтрокаXML + Символы.ПС
+ МаркерКонцаКорня;
// Записываем исправленный текст обратно в файл
Текст.УстановитьТекст(ИтоговаяСтрокаXML);
Текст.Записать(ИмяФайлаДанных, "windows-1251");
Сообщить("Файл успешно исправлен. Обнаружено и устранено блоков: " + ЧислоВхожденийКодировки);
КонецЕсли;
КонецПроцедуры
Важный момент: При использовании СтрЗаменить нужно быть уверенным, что искомые строки (например, название тега <ReportXML>) не встречаются внутри самих данных (в значениях полей). Если такие совпадения возможны, следует использовать более сложную логику замены или регулярные выражения.
Метод с ТекстовыйДокумент.ПолучитьТекст() не подходит для файлов объемом в сотни мегабайт или гигабайты. В этом случае 1С может выдать ошибку «Недостаточно памяти». Для таких задач мы должны использовать объекты ЧтениеТекста и ЗаписьТекста, которые позволяют обрабатывать файл построчно без загрузки всего содержимого в RAM.
Разберем, как реализовать потоковую очистку:
Процедура ИсправитьБольшойXML(ИмяФайлаИсточник, ИмяФайлаПриемник)
Чтение = Новый ЧтениеТекста(ИмяФайлаИсточник, "windows-1251");
Запись = Новый ЗаписьТекста(ИмяФайлаПриемник, "windows-1251");
// Записываем заголовок и открывающий тег один раз в начале
Запись.ЗаписатьСтроку("<?xml version=""1.0"" encoding=""windows-1251""?>");
Запись.ЗаписатьСтроку("<Root>");
Строка = Чтение.ПрочитатьСтроку();
Пока Строка <> Неопределено Цикл
// Пропускаем строки, содержащие пролог XML
Если Найти(Строка, "<?xml") > 0 Тогда
Строка = Чтение.ПрочитатьСтроку();
Продолжить;
КонецЕсли;
// Пропускаем повторные открывающие и закрывающие теги корневого элемента
// Здесь мы предполагаем, что корень называется "Report"
Если Найти(Строка, "<Report>") > 0 Или Найти(Строка, "</Report>") > 0 Тогда
Строка = Чтение.ПрочитатьСтроку();
Продолжить;
КонецЕсли;
// Все остальные строки (данные) записываем как есть
Запись.ЗаписатьСтроку(Строка);
Строка = Чтение.ПрочитатьСтроку();
КонецЦикла;
// Закрываем общий корневой тег
Запись.ЗаписатьСтроку("</Root>");
Чтение.Закрыть();
Запись.Закрыть();
КонецПроцедуры
Такой подход позволяет обрабатывать файлы практически любого размера, так как в памяти одновременно находится только одна строка текста.
Проанализируем ситуацию с кодировками. Если ваш XML-файл сохранен в кодировке UTF-8, он может содержать так называемую метку BOM (Byte Order Mark) — невидимые символы в самом начале файла. При «склеивании» нескольких файлов эти метки могут оказаться в середине текста.
Стандартное СтрЗаменить может не увидеть эти символы, но ЧтениеXML обязательно споткнется на них. Чтобы избежать проблем, при записи исправленного файла в 1С рекомендуется явно указывать кодировку КодировкаТекста.UTF8 и следить за тем, чтобы промежуточные заголовки удалялись полностью, включая невидимые спецсимволы. Использование СтрНайти в сочетании с Сред позволяет более гибко вырезать куски текста, если вы не знаете точного содержания заголовка (например, если там меняется атрибут standalone).
Выясним причину появления таких файлов. Часто это происходит из-за того, что внешняя программа не очищает файл перед выгрузкой новой порции данных, а использует режим дозаписи. Если у вас нет возможности повлиять на разработчиков внешней системы, лучшим решением будет создание «препроцессора» в 1С.
Создайте вспомогательную функцию, которая будет вызываться перед основной загрузкой данных. Эта функция должна проверять файл на «валидность». Самый быстрый способ проверки без полного чтения — попробовать открыть файл объектом ЧтениеXML и вызвать метод Следующий(). Если возникнет исключение в блоке Попытка...Исключение, значит, файл поврежден или содержит несколько корней, и его нужно пропустить через нашу процедуру исправления.
Резюме: Исправление XML как текстового файла — это надежный «костыль» для работы с некорректными внешними системами. Главное — правильно определить границы корневых элементов и не забыть про кодировку, чтобы не исказить данные при перезаписи.