При разработке решений на платформе «1С:Предприятие» часто возникает задача по обработке файлов, которые пользователь интерактивно выбирает на клиенте, а затем передает на сервер для дальнейшей обработки — для этого подойдёт набор инструментов разработчика для отладки серверных алгоритмов. В частности, для XML-файлов критически важны вопросы производительности и потребления памяти, особенно если речь идет о больших объемах данных и различных версиях платформы.
В этой статье мы подробно разберем, как эффективно и правильно прочитать содержимое XML-файла из временного хранилища на сервере, учтем особенности разных версий платформы (до и после 8.3.9) и поговорим об оптимизации использования оперативной памяти — в этом поможет универсальное средство для выгрузки и загрузки объектов XML.
Начнем с анализа первоначального кода, предложенного автором в сообщении 1 — ускоряет проверку подобных алгоритмов инструмент для пошаговой отладки кода 1С без конфигуратора. Пользователь выбирает XML-файл на клиенте, который помещается во временное хранилище. Затем на сервере делается попытка получить его содержимое в виде строки:
ДвоичныеДанныеКонтейнера = ПолучитьИзВременногоХранилища(ПараметрыАдресФайла);
Если ДвоичныеДанныеКонтейнера = Неопределено Тогда
ТекстОшибки = "Не удалось прочитать файл с контейнером.";
Иначе
Попытка
ЧтениеДанных = Новый ЧтениеДанных(ДвоичныеДанныеКонтейнера);
ЧтениеДанных.КодировкаТекста = КодировкаТекста.UTF8;
ЧтениеТекста = Новый ЧтениеТекста(ЧтениеДанных.ИсходныйПоток, КодировкаТекста.UTF8);
СтрокаXML = ЧтениеТекста.Прочитать;
ЧтениеТекста.Закрыть;
ЧтениеДанных.Закрыть;
Исключение
ТекстОшибки = "Не удалось получить строку XML из файла.";
КонецПопытки;
КонецЕсли;
Проанализируем этот код:
ДвоичныеДанныеКонтейнера = ПолучитьИзВременногоХранилища(ПараметрыАдресФайла); успешно получает бинарное представление файла. Важно понимать, что объект ДвоичныеДанные содержит весь файл в оперативной памяти сервера.ЧтениеДанных = Новый ЧтениеДанных(ДвоичныеДанныеКонтейнера); создает обертку для работы с этими двоичными данными как с потоком. В данном случае это не сильно меняет ситуацию с памятью, так как исходные двоичные данные уже в памяти.СтрокаXML = ЧтениеТекста.Прочитать; считывает весь поток (содержимое XML-файла) в одну большую строку в оперативной памяти.Вывод: Данный код является правильным с точки зрения выполнения задачи — получения строки XML. Однако он не оптимален для файлов большого размера. Фактически, весь XML-файл загружается в оперативную память как минимум дважды: сначала как ДвоичныеДанные, а затем как полноценная строка СтрокаXML. Для файлов в десятки или сотни мегабайт это может привести к значительному потреблению ОЗУ и замедлению работы сервера, а в худшем случае — к исчерпанию памяти.
Начиная с версии 8.3.9, платформа «1С:Предприятие» предоставляет более гибкие и эффективные инструменты для работы с данными, позволяющие избежать полного копирования содержимого больших файлов в оперативную память. Ключевым здесь является работа с потоками данных (Stream).
Вместо того чтобы сначала загружать весь файл как ДвоичныеДанные, а затем преобразовывать его в строку, мы можем работать с потоком напрямую. Объект ДвоичныеДанные имеет метод ОткрытьПотокДляЧтения(), который возвращает объект типа Поток. Этот поток можно передать в конструкторы других объектов, таких как ЧтениеXML или ЧтениеТекста, позволяя им читать данные порциями или последовательно, без предварительной полной загрузки в память.
ДвоичныеДанные.ОткрытьПотокДляЧтения(), чтобы получить объект-поток, который предоставляет доступ к данным без их полного копирования.ЧтениеXML — на базе этого механизма построена подсистема для настройки регулярного обмена данными через XML. Он работает по принципу SAX-парсера (Simple API for XML), то есть читает файл последовательно, узел за узлом, не строя полную объектную модель документа (DOM) в памяти. Это значительно экономит ресурсы для больших XML-файлов.Рассмотрим пример кода для серверной процедуры:
&НаСервере
Функция ПрочитатьXMLИзХранилищаОптимально(АдресФайлаВХранилище)
Перем ТекстОшибки;
// 1. Получаем двоичные данные файла из временного хранилища.
// Объект ДвоичныеДанные сам по себе содержит файл в памяти.
// Однако, мы будем работать с ним через поток, минимизируя дальнейшие копирования.
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресФайлаВХранилище);
Если ДвоичныеДанныеФайла = Неопределено Тогда
ТекстОшибки = "Не удалось получить двоичные данные файла из временного хранилища.";
Возврат Неопределено;
КонецЕсли;
Попытка
// 2. Открываем поток для чтения из двоичных данных.
// Это не копирует весь файл в память как строку, а предоставляет доступ к потоку байтов.
// Поток позволяет ЧтениеXML читать данные порциями.
ПотокДанных = ДвоичныеДанныеФайла.ОткрытьПотокДляЧтения();
// 3. Используем ЧтениеXML для последовательного доступа к содержимому файла.
// ЧтениеXML умеет автоматически определять кодировку из заголовка XML (например, <?xml version="1.0" encoding="UTF-8"?>).
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.Открыть(ПотокДанных);
// Пример обработки XML-документа:
// Мы можем обходить узлы XML-файла последовательно, не загружая весь документ в память.
Пока ЧтениеXML.Прочитать() Цикл
// В этом цикле мы можем реагировать на различные типы узлов XML.
Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
// Например, если мы ищем определенный элемент, чтобы прочитать его как ОбъектXDTO:
Если ЧтениеXML.Имя = "НужныйЭлемент" Тогда
// ФабрикаXDTO.ПрочитатьXML(ЧтениеXML) прочитает текущий элемент
// и все его содержимое как один ОбъектXDTO.
// Если НужныйЭлемент большой, то этот ОбъектXDTO все равно займет много памяти.
// Однако, это лучше, чем читать весь файл как строку, если нас интересует только часть.
ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
// Здесь вы можете обработать ОбъектXDTO, например, сохранить его в базу данных.
// После обработки можно продолжить чтение файла, если есть другие НужныйЭлемент.
Сообщить("Найден и обработан элемент: " + ЧтениеXML.Имя);
КонецЕсли;
КонецЕсли;
// Если требуется только получение всей XML-структуры как строки (что менее оптимально,
// но иногда необходимо), то можно было бы использовать ЧтениеТекста(ПотокДанных),
// как показано ниже, но это сведет на нет преимущества последовательного чтения для XML-структуры.
КонецЦикла;
ЧтениеXML.Закрыть();
ПотокДанных.Закрыть(); // Обязательно закрываем поток после использования
// Если, несмотря на рекомендации, вам все же нужна вся строка XML целиком,
// то для этого можно использовать ЧтениеТекста.
// При этом также используем поток для чтения, чтобы избежать двойного копирования.
ПотокДляЧтенияТекста = ДвоичныеДанныеФайла.ОткрытьПотокДляЧтения();
ЧтениеТекста = Новый ЧтениеТекста(ПотокДляЧтенияТекста); // Автоматическое определение кодировки
СтрокаXML = ЧтениеТекста.Прочитать();
ЧтениеТекста.Закрыть();
ПотокДляЧтенияТекста.Закрыть();
Возврат СтрокаXML; // Или результат обработки, например, список объектов
Исключение
ТекстОшибки = "Не удалось обработать XML-файл: " + ОписаниеОшибки();
Возврат Неопределено;
КонецПопытки;
КонецФункции
Обратите внимание, что объект ФабрикаXDTO.ПрочитатьXML(Поток) или ФабрикаXDTO.ПрочитатьXML(ЧтениеXML) строит полную объектную модель XDTO в памяти для того элемента, который он читает (DOM-подобный подход). Если вы читаете корневой элемент большого XML-файла с помощью ФабрикаXDTO, вы все равно загрузите большую часть файла в память в виде объектов XDTO. Однако, при использовании ЧтениеXML для навигации по файлу, вы можете выборочно читать небольшие интересующие вас части как XDTO, что будет гораздо эффективнее, чем загружать весь документ.
Для обеспечения интерактивного выбора файла пользователем и его последующей обработки на сервере с поддержкой всех видов клиентов (тонкий, веб) используем стандартные асинхронные методы платформы:
НачатьПомещениеФайлаНаСервер() позволяет пользователю выбрать файл. Он асинхронно помещает выбранный файл во временное хранилище на сервере, не читая его предварительно в память клиента или браузера.Посмотрим на пример клиентской и серверной части в совокупности:
// *** Клиентская часть ***
&НаКлиенте
Процедура ВыбратьИОбработатьXMLФайл()
// Создаем описание оповещения для обработки результата помещения файла на сервер.
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеПомещенияФайла", ЭтотОбъект);
// Запускаем интерактивное помещение файла на сервер.
// Параметры: ОписаниеОповещения, ИмяФайла (пусто для выбора), ЗаголовокДиалога,
// ПоказыватьПрогресс, Идентификатор, РасширениеПоУмолчанию, Фильтр
НачатьПомещениеФайлаНаСервер(ОписаниеОповещения, , "Выберите XML файл", Истина, , "xml", "XML файл (*.xml)|*.xml");
КонецПроцедуры
&НаКлиенте
Процедура ПослеПомещенияФайла(РезультатПомещения, АдресФайлаВХранилище, ДополнительныеПараметры) Экспорт
Если РезультатПомещения Тогда
// Файл успешно помещен во временное хранилище на сервере.
// АдресФайлаВХранилище - это ссылка (строка) на этот файл.
// Теперь вызываем серверную процедуру для обработки файла, передавая только адрес.
СтрокаXMLИлиДанные = ПрочитатьXMLИзХранилищаОптимально(АдресФайлаВХранилище);
Если СтрокаXMLИлиДанные <> Неопределено Тогда
Сообщить("Файл успешно прочитан.");
// Здесь вы можете использовать возвращенные данные
// Например, вывести часть строки или обработать полученный ОбъектXDTO
Сообщить("Начало XML-строки: " + Лев(СтрокаXMLИлиДанные, 200));
Иначе
Сообщить("Ошибка при чтении файла на сервере.");
КонецЕсли;
// Обязательно очищаем временное хранилище, когда файл больше не нужен.
// Это предотвращает раздувание кэша на сервере.
УдалитьИзВременногоХранилища(АдресФайлаВХранилище);
Иначе
Сообщить("Файл не был выбран или произошла ошибка при помещении на сервер.");
КонецЕсли;
КонецПроцедуры
// *** Серверная часть (та же, что и выше) ***
// &НаСервере
// Функция ПрочитатьXMLИзХранилищаОптимально(АдресФайлаВХранилище)
// ... (код приведенный ранее) ...
// КонецФункции
Такой подход максимально эффективен, поскольку передача адреса из временного хранилища требует минимального сетевого трафика и серверных ресурсов по сравнению с передачей всего файла как строки или массива байтов. Для более детального знакомства с этой темой можно изучить статью об особенностях работы с файлами в управляемых формах.
До версии 8.3.9 объект ДвоичныеДанные не имел метода ОткрытьПотокДляЧтения(). Это означает, что прямой работы с потоками из двоичных данных не было. В этом случае для экономии памяти при чтении больших XML-файлов приходилось использовать промежуточное сохранение файла на диске сервера.
Для старых версий платформы мы используем следующий алгоритм:
ДвоичныеДанные из временного хранилища. Эти данные, к сожалению, полностью загружаются в память.ПолучитьИмяВременногоФайла().ДвоичныеДанные.Записать(ИмяФайла), чтобы сохранить содержимое файла на диск сервера. После этого можно освободить ссылку на объект ДвоичныеДанные, чтобы система могла высвободить часть памяти.ЧтениеXML.Открыть(ИмяФайла). Объект ЧтениеXML, как мы уже говорили, читает файл последовательно, что значительно снижает потребление памяти по сравнению с полной загрузкой в строку.УдалитьФайлы(ИмяФайла) и очищаем временное хранилище.Рассмотрим пример кода для серверной процедуры в старых версиях:
&НаСервере
Функция ПрочитатьXMLИзХранилищаСтарыйПодход(АдресФайлаВХранилище)
Перем ТекстОшибки;
Перем ИмяВременногоФайла;
// 1. Получаем двоичные данные файла из временного хранилища.
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресФайлаВХранилище);
Если ДвоичныеДанныеФайла = Неопределено Тогда
ТекстОшибки = "Не удалось получить двоичные данные файла из временного хранилища.";
Возврат Неопределено;
КонецЕсли;
Попытка
// 2. Создаем уникальное имя для временного файла на диске сервера.
// ".xml" - это расширение файла, которое поможет системе правильно его ассоциировать.
ИмяВременногоФайла = ПолучитьИмяВременногоФайла(".xml");
// 3. Записываем двоичные данные файла на диск сервера.
// На этом этапе файл физически появляется на сервере.
ДвоичныеДанныеФайла.Записать(ИмяВременногоФайла);
// После записи можно обнулить ссылку на ДвоичныеДанныеФайла,
// чтобы освободить память, если файл очень большой.
ДвоичныеДанныеФайла = Неопределено;
// 4. Открываем файл с помощью ЧтениеXML для последовательного чтения с диска.
// Это позволяет читать XML поэлементно, не загружая весь файл в ОЗУ.
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.Открыть(ИмяВременногоФайла);
// Пример обработки XML-документа:
// Аналогично современному подходу, мы можем обходить узлы XML-файла.
Пока ЧтениеXML.Прочитать() Цикл
Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
Если ЧтениеXML.Имя = "НужныйОбъект" Тогда
ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML);
// Обработка ОбъектXDTO...
Сообщить("Найден и обработан элемент: " + ЧтениеXML.Имя);
КонецЕсли;
КонецЕсли;
КонецЦикла;
ЧтениеXML.Закрыть(); // Закрываем объект ЧтениеXML
// Если все же требуется получить весь XML-файл как строку (неоптимально для больших файлов!)
// Для старых версий это единственный способ, если нет потоков из ДвоичныеДанные.
ЧтениеТекстаФайла = Новый ЧтениеТекста(ИмяВременногоФайла);
СтрокаXML = ЧтениеТекстаФайла.Прочитать();
ЧтениеТекстаФайла.Закрыть();
Возврат СтрокаXML; // Или обработанные данные
Исключение
ТекстОшибки = "Ошибка при обработке файла: " + ОписаниеОшибки();
Возврат Неопределено;
КонецПопытки;
Наконец // Этот блок гарантированно выполнится даже при ошибке
// 5. Обязательно удаляем временный файл с диска сервера.
// Проверяем существование файла, чтобы избежать ошибок.
Если ИмяВременногоФайла <> "" И ФайлСуществует(ИмяВременногоФайла) Тогда
УдалитьФайлы(ИмяВременногоФайла);
Сообщить("Временный файл удален: " + ИмяВременногоФайла);
КонецЕсли;
// Также очищаем временное хранилище на сервере, если данные больше не нужны.
Если АдресФайлаВХранилище <> "" Тогда
УдалитьИзВременногоХранилища(АдресФайлаВХранилище);
Сообщить("Данные удалены из временного хранилища: " + АдресФайлаВХранилище);
КонецЕсли;
КонецПопытки;
КонецФункции
Несмотря на то, что этот подход требует записи файла на диск сервера, он позволяет использовать последовательное чтение XML (SAX-подобный подход), что значительно снижает пиковое потребление памяти по сравнению с полной загрузкой файла в строку. Блок Наконец критически важен для гарантированного удаления временного файла и очистки временного хранилища, предотвращая захламление диска и памяти сервера.
Рассмотрим дополнительные аспекты, которые помогут сделать работу с файлами максимально эффективной и надежной.
Временное хранилище в 1С — это удобный механизм для передачи данных между клиентом и сервером или между различными сеансами. Однако важно понимать, как оно работает и как правильно управлять его содержимым:
УдалитьИзВременногоХранилища(Адрес). Это освободит память и дисковое пространство, предотвратив раздувание сеансового кэша и возможные проблемы с производительностью или доступностью ресурсов.ДвоичныеДанные, после их обработки или записи на диск рекомендуется обнулять ссылки на них (например, ДвоичныеДанныеФайла = Неопределено;). Это подскажет сборщику мусора, что объект больше не используется и его память может быть освобождена.При работе с текстовыми файлами, особенно XML, кодировка является частой причиной проблем. Однако для XML-файлов существуют стандартные механизмы, которые облегчают эту задачу:
ЧтениеXML: Объект ЧтениеXML обладает интеллектуальной способностью автоматически определять кодировку файла, если она указана в заголовке XML-документа (например, <?xml version="1.0" encoding="windows-1251"?>). Поэтому для XML-файлов обычно не требуется жестко задавать кодировку для ЧтениеXML.ЧтениеТекста: Если вы используете ЧтениеТекста с потоком (в современных версиях) или с именем файла, он также может попытаться автоматически определить кодировку по маркерам BOM (Byte Order Mark), если они присутствуют в файле.ЧтениеТекста: Если конечная цель — это обработка XML-структуры, то предварительное чтение всего XML-файла в одну строку с помощью ЧтениеТекста является лишним и вредным шагом, так как теряются преимущества последовательного чтения XML и возникает избыточное потребление памяти. Используйте ЧтениеXML напрямую.Когда речь идет о файлах в сотни мегабайт или даже гигабайты, даже последовательное чтение может быть долгим, а обработка каждого элемента — ресурсоемкой. В таких сценариях мы можем рассмотреть следующие подходы:
В ходе обсуждения предлагались и другие варианты, которые, однако, имеют свои ограничения:
ЧтениеТекста (из ДиалогВыбораФайла.ПолноеИмяФайла) и передать всю строку на сервер.
ЧтениеТекста с прямым указанием пути к файлу недоступно на веб-клиенте из соображений безопасности браузера. Пользователь не может получить полный путь к файлу на своем локальном компьютере, и уж тем более веб-клиент не может напрямую читать локальные файлы.ДвоичныхДанных напрямую: Идея передать Новый ДвоичныеДанные(ИмяФайла) с клиента на сервер. Это может работать на тонком клиенте, но все равно загружает весь файл в память клиента (пусть и временно), а затем передает его на сервер. Для веб-клиента напрямую создать Новый ДвоичныеДанные(ИмяФайла) из локального пути невозможно по тем же причинам безопасности, что и для ЧтениеТекста. Метод НачатьПомещениеФайлаНаСервер является стандартным и универсальным решением для всех типов клиентов.Таким образом, использование временного хранилища и асинхронных методов помещения файла на сервер, в сочетании с потоковым чтением XML на сервере, является наиболее универсальным, эффективным и масштабируемым подходом для обработки XML-файлов в 1С:Предприятии, особенно для больших объемов данных — зеркальную задачу решает универсальная обработка для выгрузки данных в формат XML.