Реализация загрузки данных из внешних структурированных файлов (CSV, текстовые файлы с разделителями) в прикладные объекты конфигурации — классическая задача при разработке на платформе 1С:Предприятие 8.3. В процессе написания обработчиков загрузки разработчики нередко сталкиваются с ошибками компиляции встроенного языка, когда синтаксический анализатор сообщает о попытке вызова несуществующей функции вместо извлечения данных из прочитанной строки. Подобная неполадка полностью блокирует выполнение модуля формы или объекта.
Для создания устойчивого алгоритма необходимо не только устранить синтаксическую ошибку, но и пересмотреть сопутствующие архитектурные решения: клиент-серверное взаимодействие при чтении файлов, контроль границ коллекций, работу с перечислениями и оптимизацию обращений к базе данных.
В процессе компиляции обработчика загрузки система выводит диагностическое сообщение вида: {Обработка.ИмпортИзФайлаТовары.Форма.Форма.Форма(27,28)}: Процедура или функция с указанным именем не определена (Массив) при выполнении строки присвоения НоваяСтрокаТЗ.Описание = Массив (2);.
Корень проблемы заключается в правилах синтаксиса встроенного языка платформы 1С:Предприятие:
() зарезервированы исключительно для вызова процедур, функций или методов объектов конфигурации, а также для передачи фактических параметров. Когда компилятор встречает конструкцию Массив(0) или Массив (2), он интерпретирует идентификатор Массив как имя глобальной или локальной функции, ожидающей передачи целочисленного аргумента.Массив в контексте модуля и общих модулях конфигурации отсутствует, компилятор останавливает валидацию модуля и сообщает о неопределенном имени метода.Массив(0), а ниже — раздельное Массив (2), анализатор синтаксиса в зависимости от режима проверки и контекста может выдать ошибку именно на строке с пробелом перед скобкой либо на первом вхождении некорректной синтаксической конструкции. Ошибочными при этом являются все аналогичные обращения.Коллекция Массив в платформе 1С:Предприятие представляет собой индексированный список значений, где нумерация элементов всегда начинается с нуля: от 0 до Количество() - 1. Для извлечения и изменения значений элементов используются два штатных способа:
НоваяСтрокаТЗ.Код = Массив[0];
НоваяСтрокаТЗ.Товар = Массив[1];
НоваяСтрокаТЗ.Описание = Массив[2];
Получить() в круглых скобках:
НоваяСтрокаТЗ.Код = Массив.Получить(0);
НоваяСтрокаТЗ.Товар = Массив.Получить(1);
НоваяСтрокаТЗ.Описание = Массив.Получить(2);
Устранение круглых скобок решает проблему компиляции, однако в исходной реализации загрузки сохраняется целый ряд архитектурных и алгоритмических дефектов, приводящих к сбоям в рабочей среде:
СтрРазделить(Строка, ";") возвращает массив подстрок. Если в загружаемом файле встретится пустая строка, строка с пропущенными разделителями или строка комментария, количество элементов в массиве окажется меньше шести. Попытка обращения к Массив[5] немедленно приведет к фатальной ошибке выполнения: «Индекс находится за границами массива». Перед извлечением значений необходимо всегда проверять фактический размер массива.СтрРазделить(ИсходнаяСтрока, Разделители, ВключатьПустые) равен Ложь. Если в файле последние колонки не заполнены (например, оканчиваются на ;;;), они будут отброшены, что искусственно сократит длину массива. Для гарантированного получения фиксированного числа колонок параметр ВключатьПустые должен быть установлен в Истина.&НаСервере указывает, что процедура чтения выполняется на стороне сервера 1С:Предприятия. Если переменная ПутьКФайлу содержит путь к файлу на локальном диске пользователя (например, клиентский путь на рабочей станции), сервер не сможет открыть этот файл, так как не имеет доступа к файловой системе клиентского компьютера. В управляемом приложении файл должен помещаться во временное хранилище на клиенте и передаваться на сервер в виде двоичных данных или потока.Перечисления.Категории[Стр.Категория] ожидает точное имя идентификатора значения перечисления, заданное в конфигураторе (на латинице или кириллице без пробелов и спецсимволов). Если во внешнем файле содержатся синонимы значений (например, «Бытовая техника» вместо идентификатора «БытоваяТехника»), такое обращение всегда будет вызывать исключение. Для сопоставления по синонимам требуется перебор значений метаданных или использование соответствий.Справочники.Города.НайтиПоНаименованию() и СоздатьЭлемент() внутри цикла обхода строк создают массовые одиночные транзакции и запросы к СУБД. При обработке файла на несколько тысяч строк импорт может занять часы. Данные должны кэшироваться в объектах Соответствие, а поиск производиться пакетно.Для создания отказоустойчивой обработки импорта рекомендуется придерживаться следующей последовательности действий:
ПоместитьФайлНаСервер().ПолучитьИмяВременногоФайла(), после чего безопасно открываются объектом ТекстовыйДокумент с явным указанием кодировки (ANSI или UTF-8).СтрРазделить с флагом сохранения пустых значений. Проверяется соответствие количества элементов ожидаемому числу колонок. Некорректные строки фиксируются в журнале или отчете об ошибках без остановки всего процесса.Соответствие, что исключает повторные запросы к базе по одинаковым строкам.НачатьТранзакцию() и ЗафиксироватьТранзакцию(), что обеспечивает целостность базы данных и ускоряет запись в десятки раз.Ниже представлен оптимизированный вариант модуля формы управляемого приложения, полностью устраняющий синтаксические ошибки и уязвимости исходного алгоритма:
&НаКлиенте
Процедура ЗагрузитьФайл(Команда)
ПараметрыДиалога = Новый ПараметрыДиалогаПомещенияФайлов;
ПараметрыДиалога.Заголовок = "Выберите файл для импорта данных";
ПараметрыДиалога.Фильтр = "Текстовые файлы (*.csv;*.txt)|*.csv;*.txt";
ОповещениеОЗавершении = Новый ОписаниеОповещения("ПослеВыбораФайла", ЭтотОбъект);
НачатьПомещениеФайлаНаСервер(ОповещениеОЗавершении, , , ПараметрыДиалога, УникальныйИдентификатор);
КонецПроцедуры
&НаКлиенте
Процедура ПослеВыбораФайла(ОписаниеПомещенногоФайла, ДополнительныеПараметры) Экспорт
Если ОписаниеПомещенногоФайла = Неопределено Тогда
Возврат;
КонецЕсли;
РезультатИмпорта = ПрочитатьИЗагрузитьНаСервере(ОписаниеПомещенногоФайла.Адрес);
ПоказатьОповещениеПользователя("Импорт завершен", , РезультатИмпорта);
КонецПроцедуры
&НаСервере
Функция ПрочитатьИЗагрузитьНаСервере(АдресХранилища)
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("txt");
ДвоичныеДанные.Записать(ИмяВременногоФайла);
ТекстовыйДок = Новый ТекстовыйДокумент;
ТекстовыйДок.Прочитать(ИмяВременногоФайла, КодировкаТекста.UTF8);
УдалитьФайлы(ИмяВременногоФайла);
ВсегоСтрок = ТекстовыйДок.КоличествоСтрок();
Если ВсегоСтрок < 2 Тогда
Возврат "Файл не содержит строк данных для загрузки.";
КонецЕсли;
// Кэш ссылок для оптимизации поиска в цикле
КэшГородов = Новый Соответствие;
КэшФирм = Новый Соответствие;
ЗагруженоТоваров = 0;
Ошибок = 0;
НачатьТранзакцию();
Попытка
Для НомерСтроки = 2 По ВсегоСтрок Цикл
ТекущаяСтрока = ТекстовыйДок.ПолучитьСтроку(НомерСтроки);
Если ПустаяСтрока(СокрЛП(ТекущаяСтрока)) Тогда
Продолжить;
КонецЕсли;
// Включаем пустые колонки для соблюдения структуры
Колонки = СтрРазделить(ТекущаяСтрока, ";", Истина);
Если Колонки.Количество() < 6 Тогда
Ошибок = Ошибок + 1;
Продолжить;
КонецЕсли;
// Безопасное обращение по индексу через квадратные скобки
СтрКод = СокрЛП(Колонки[0]);
СтрТовар = СокрЛП(Колонки[1]);
СтрОписание = СокрЛП(Колонки[2]);
СтрКатегория = СокрЛП(Колонки[3]);
СтрПоставщик = СокрЛП(Колонки[4]);
СтрГород = СокрЛП(Колонки[5]);
// Получение или создание города с кэшированием
СсылкаГород = КэшГородов[СтрГород];
Если СсылкаГород = Неопределено И Не ПустаяСтрока(СтрГород) Тогда
СсылкаГород = Справочники.Города.НайтиПоНаименованию(СтрГород, Истина);
Если Не ЗначениеЗаполнено(СсылкаГород) Тогда
ОбъектГород = Справочники.Города.СоздатьЭлемент();
ОбъектГород.Наименование = СтрГород;
ОбъектГород.Записать();
СсылкаГород = ОбъектГород.Ссылка;
КонецЕсли;
КэшГородов.Вставить(СтрГород, СсылкаГород);
КонецЕсли;
// Получение или создание поставщика с кэшированием
СсылкаПоставщик = КэшФирм[СтрПоставщик];
Если СсылкаПоставщик = Неопределено И Не ПустаяСтрока(СтрПоставщик) Тогда
СсылкаПоставщик = Справочники.Фирмы.НайтиПоНаименованию(СтрПоставщик, Истина);
Если Не ЗначениеЗаполнено(СсылкаПоставщик) Тогда
ОбъектФирма = Справочники.Фирмы.СоздатьЭлемент();
ОбъектФирма.Наименование = СтрПоставщик;
ОбъектФирма.Записать();
СсылкаПоставщик = ОбъектФирма.Ссылка;
КонецЕсли;
КэшФирм.Вставить(СтрПоставщик, СсылкаПоставщик);
КонецЕсли;
// Поиск и заполнение товара
ТоварСсылка = Справочники.Товары.НайтиПоНаименованию(СтрТовар, Истина);
Если Не ЗначениеЗаполнено(ТоварСсылка) Тогда
ОбъектТовар = Справочники.Товары.СоздатьЭлемент();
ОбъектТовар.Наименование = СтрТовар;
Если Не ПустаяСтрока(СтрКод) Тогда
ОбъектТовар.Код = СтрКод;
КонецЕсли;
ОбъектТовар.Описание = СтрОписание;
ОбъектТовар.ОсновнойПоставщик = СсылкаПоставщик;
// Безопасное определение категории
Попытка
ОбъектТовар.Категория = Перечисления.Категории[СтрКатегория];
Исключение
// Идентификатор не найден среди значений перечисления
КонецПопытки;
ОбъектТовар.Записать();
ЗагруженоТоваров = ЗагруженоТоваров + 1;
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение "Ошибка при импорте данных: " + ОписаниеОшибки();
КонецПопытки;
Возврат СтрШаблон("Успешно загружено товаров: %1. Пропущено некорректных строк: %2.", ЗагруженоТоваров, Ошибок);
КонецФункции
Для предотвращения сбоев при регулярном обмене данными рекомендуется внедрять следующие правила проектирования:
КодировкаТекста в метод Прочитать(). Использование кодировки по умолчанию на различных серверах под управлением Windows и Linux может приводить к искажению кириллических символов.НайтиПоНаименованию() вторым параметром передавайте Истина (поиск по точному совпадению), чтобы исключить ошибочную привязку элементов, чьи наименования начинаются с одинаковых символов.