При разработке механизмов импорта данных из внешних файлов в управляемом приложении разработчики часто сталкиваются с ошибками контекста выполнения. Типичные примеры таких ошибок: «Метод объекта не обнаружен (ПолучитьСтроку)» или невозможность создать объект ТаблицаЗначений в коде с директивой &НаКлиенте. Проанализируем, почему возникают эти проблемы, как устроено правильное взаимодействие между клиентом и сервером при загрузке файлов, и рассмотрим надежные варианты реализации чтения формата CSV.
Выясним причину, из-за которой код завершается аварийно. В клиент-серверной архитектуре платформы 1С:Предприятие 8.3 действуют строгие ограничения на доступность типов данных и методов:
ТаблицаЗначений не существует в контексте тонкого клиента и веб-клиента. Попытка выполнить конструкцию Новый ТаблицаЗначений на клиенте вызывает исключение.ТекстовыйДокумент в управляемых формах имеет ограниченный функционал на клиенте, а чтение больших файлов целиком в память через этот объект замедляет работу системы. Для потокового чтения текста предназначен объект ЧтениеТекста или специализированный объект ЧтениеCSV.C:\Users\...). Поэтому файл с клиента необходимо сначала передать во временное хранилище и только затем обрабатывать на сервере.Разберем по шагам базовый и наиболее универсальный алгоритм. Пользователь интерактивно выбирает файл на своем компьютере, после чего платформа отправляет его в оперативное временное хранилище. Для этого воспользуемся асинхронной процедурой НачатьПомещениеФайлаНаСервер (или НачатьПомещениеФайла).
Посмотрим на пример клиентского кода:
&НаКлиенте
Процедура ЗагрузитьCSV(Команда)
Оповещение = Новый ОписаниеОповещения("ПослеПомещенияФайла", ЭтотОбъект);
// Интерактивный вызов диалога и отправка файла в хранилище за одно действие
НачатьПомещениеФайлаНаСервер(
Оповещение,
, // Адрес в хранилище (будет создан автоматически)
, // Диалог открытия файла будет открыт платформой
, // Параметры диалога
ЭтаФорма.УникальныйИдентификатор
);
КонецПроцедуры
&НаКлиенте
Процедура ПослеПомещенияФайла(ОписаниеФайла, ДополнительныеПараметры) Экспорт
Если ОписаниеФайла = Неопределено Тогда
Возврат;
КонецЕсли;
// Передаем полученный адрес во временном хранилище на сервер
ПрочитатьИЗаполнитьНаСервере(ОписаниеФайла.Адрес);
КонецПроцедуры
Рассмотрим подробнее обработку полученного файла на сервере. Начиная с версии платформы 8.3.6, для разбора таких файлов рекомендуется использовать объект ЧтениеCSV. В отличие от простого построчного разбиения через СтрРазделить или самописные функции, ЧтениеCSV корректно обрабатывает экранированные кавычки, многострочный текст внутри колонок и различные разделители.
Чтобы не сохранять промежуточные файлы на диск сервера и не зависеть от операционной системы (Windows/Linux) и прав доступа к папкам, прочитаем данные напрямую из потока в оперативной памяти.
Разберем пример серверной процедуры:
&НаСервере
Процедура ПрочитатьИЗаполнитьНаСервере(АдресВХранилище)
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресВХранилище);
Если ДвоичныеДанные = Неопределено Тогда
ВызватьИсключение "Не удалось получить файл из временного хранилища.";
КонецЕсли;
// Открываем поток для чтения без сохранения на жесткий диск
Поток = ДвоичныеДанные.ОткрытьПотокДляЧтения();
Чтение = Новый ЧтениеCSV;
// Настраиваем параметры: разделитель полей - точка с запятой
ПараметрыCSV = Новый ПараметрыЧтенияCSV;
ПараметрыCSV.РазделительПолей = ";";
ПараметрыCSV.СимволМаскировки = """";
Чтение.ОткрытьПоток(Поток, ПараметрыCSV);
// Читаем заголовок (первую строку)
МассивКолонок = Чтение.Прочитать();
Если МассивКолонок = Неопределено Тогда
Чтение.Закрыть();
Возврат;
КонецЕсли;
// Очищаем табличную часть перед заполнением
Объект.ДанныеЗаМесяц.Очистить();
// Последовательно читаем строки файла
Пока Истина Цикл
СтрокаДанных = Чтение.Прочитать();
Если СтрокаДанных = Неопределено Тогда
Прервать; // Достигнут конец файла
КонецЕсли;
// Заполняем строку табличной части документа
НоваяСтрока = Объект.ДанныеЗаМесяц.Добавить();
// Пример заполнения реквизитов по индексам колонок CSV
Если СтрокаДанных.Количество() > 0 Тогда
НоваяСтрока.Артикул = СокрЛП(СтрокаДанных[0]);
КонецЕсли;
Если СтрокаДанных.Количество() > 1 Тогда
НоваяСтрока.Количество = Число(СтрокаДанных[1]);
КонецЕсли;
КонецЦикла;
Чтение.Закрыть();
КонецПроцедуры
Если структура файла простая и гарантированно не содержит переносов строк внутри полей, можно использовать объект ЧтениеТекста в связке с системной функцией СтрРазделить. Посмотрим, как это реализуется с сохранением во временный файл:
&НаСервере
Процедура ПрочитатьЧерезЧтениеТекста(АдресВХранилище)
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресВХранилище);
// Получаем уникальное имя временного файла в каталоге сервера
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("csv");
ДвоичныеДанные.Записать(ИмяВременногоФайла);
Чтение = Новый ЧтениеТекста(ИмяВременногоФайла, КодировкаТекста.UTF8);
Попытка
// Пропускаем строку заголовков
Заголовок = Чтение.ПрочитатьСтроку();
Пока Истина Цикл
ТекстСтроки = Чтение.ПрочитатьСтроку();
Если ТекстСтроки = Неопределено Тогда
Прервать;
КонецЕсли;
// Встроенная функция платформы для разделения строки
МассивЗначений = СтрРазделить(ТекстСтроки, ";", Истина);
Если МассивЗначений.Количество() >= 2 Тогда
НоваяСтрока = Объект.ДанныеЗаМесяц.Добавить();
НоваяСтрока.НоменклатураНаименование = СокрЛП(МассивЗначений[0]);
НоваяСтрока.Сумма = Число(МассивЗначений[1]);
КонецЕсли;
КонецЦикла;
Фиксировать
Исключение
Чтение.Закрыть();
УдалитьФайлы(ИмяВременногоФайла);
ВызватьИсключение ОписаниеОшибки();
КонецПопытки;
// Обязательно закрываем дескриптор файла и удаляем временный файл
Чтение.Закрыть();
УдалитьФайлы(ИмяВременногоФайла);
КонецПроцедуры
Элементы.ИмяТаблицы.ДобавитьСтроку() предназначена только для интерактивного управления интерфейсом. На сервере заполнение данных документа выполняется исключительно через реквизит данных: Объект.ИмяТабличнойЧасти.Добавить().ЧтениеТекста.Закрыть() или ЧтениеCSV.Закрыть()), чтобы не блокировать файлы процессами сервера.C:\temp\file.txt приводит к сбоям прав доступа и ошибкам на серверах под управлением Linux. Применяйте потоки из ДвоичныеДанные либо системную функцию ПолучитьИмяВременногоФайла().