Файлы во встроенном языке 1С:Предприятия 8.3: проверка, чтение, запись и копирование

· 9 мин

Содержание

Работа с файлами в 1С:Предприятии 8.3 строится на трёх группах средств встроенного языка. Объект Файл отвечает на вопросы «есть ли файл, какого он размера, когда изменён». Функции глобального контекста (КопироватьФайл, ПереместитьФайл, НайтиФайлы, УдалитьФайлы) двигают файлы по диску. Содержимое читают и пишут ЧтениеТекста/ЗаписьТекста для текста и ДвоичныеДанные/ФайловыеПотоки для всего остального.

Всё это общее для платформы и одинаково работает в любой конфигурации: БП, ЗУП, УТ или собственной разработке. Главное, что нужно решить до первой строки кода, — на каком компьютере код выполняется. Ошибок из-за этого больше всего.

Где выполняется код: файлы на клиенте и на сервере

Код в модуле с директивой &НаСервере видит диск сервера 1С, а не ваш. Путь C:\Обмен\prices.csv в серверной процедуре ищется на машине, где запущен рабочий процесс rphost. В файловой базе без веб-публикации клиент и «сервер» — один компьютер, поэтому разработчик часто узнаёт о разнице только после переноса базы на сервер.

Где кодЧей диск видитТипичная задача
&НаКлиенте, тонкий клиенткомпьютер пользователявыбор файла, сохранение выгрузки на рабочий стол
&НаСервере, общий серверный модульсервер 1Собмены по расписанию, разбор загруженного файла
Регламентное заданиесервер 1Сзабрать файлы из сетевой папки обмена
Веб-клиентпрямого доступа к диску неттолько через диалог выбора и передачу файла

Файл между клиентом и сервером передают через временное хранилище. Клиент помещает файл (диалог выбора или асинхронные методы помещения файлов на сервер), получает адрес, а сервер забирает содержимое:

&НаСервере
Процедура ЗагрузитьНаСервере(АдресФайла)
    ДД = ПолучитьИзВременногоХранилища(АдресФайла);
    ИмяВрем = ПолучитьИмяВременногоФайла("csv");
    ДД.Записать(ИмяВрем);
    // разбор файла ИмяВрем
    УдалитьФайлы(ИмяВрем);
КонецПроцедуры

В конфигурациях с режимом совместимости 8.3.18 и выше на клиенте используйте асинхронные варианты методов: СуществуетАсинх(), КопироватьФайлАсинх(), НайтиФайлыАсинх() и т. п. с оператором Ждать. Синхронные вызовы на клиенте зависят от свойства конфигурации «Режим использования синхронных вызовов расширений платформы и внешних компонент» и в веб-клиенте могут быть запрещены.

Объект Файл: как узнать имя, размер и дату изменения

Новый Файл(ПолноеИмя) не создаёт и не открывает файл на диске. Это описание пути, через которое удобно получать метаданные. Объект можно создать даже для несуществующего файла — например, чтобы разобрать имя на части.

Файл = Новый Файл("C:\Обмен\Входящие\prices.csv");

Если Не Файл.Существует() Тогда
    ВызватьИсключение СтрШаблон("Файл %1 не найден", Файл.ПолноеИмя);
КонецЕсли;

Сообщить(Файл.Имя);               // prices.csv
Сообщить(Файл.ИмяБезРасширения);  // prices
Сообщить(Файл.Расширение);        // .csv (с точкой)
Сообщить(Файл.Путь);              // C:\Обмен\Входящие\
Сообщить(Формат(Файл.Размер(), "ЧГ=0") + " байт");
Сообщить(Файл.ПолучитьВремяИзменения());

Основные свойства и методы:

  • 📄 Имя, ИмяБезРасширения, Расширение, Путь, ПолноеИмя — части пути, читаются без обращения к диску;
  • 🔍 Существует() — есть ли файл или каталог по этому пути;
  • 🗂 ЭтоФайл() и ЭтоКаталог() — что именно лежит по пути;
  • 📏 Размер() — размер в байтах;
  • 🕒 ПолучитьВремяИзменения() и УстановитьВремяИзменения() — локальное время изменения; для сравнения файлов с разных серверов есть ПолучитьУниверсальноеВремяИзменения();
  • 🔒 ПолучитьТолькоЧтение(), УстановитьТолькоЧтение(), ПолучитьНевидимость(), УстановитьНевидимость() — атрибуты.

Размер всегда выводите через Формат(..., "ЧГ=0"). Без этого число 1048576 превратится в строку «1 048 576» с неразрывными пробелами, и сравнение или запись в CSV сломаются.

Копирование, перемещение, поиск и удаление файлов

Отдельного объекта «файловый менеджер» в платформе нет: операции над файлами — это функции глобального контекста. Их вызывают напрямую, без создания объектов.

ФункцияЧто делаетНа что смотреть
КопироватьФайл(Источник, Приемник)копирует файлкаталог приёмника должен существовать
ПереместитьФайл(Источник, Приемник)перемещает или переименовываетзаранее проверьте, нет ли файла с таким именем
НайтиФайлы(Путь, Маска, ВПодкаталогах)возвращает массив объектов Файлмаска *.xml, третий параметр — рекурсия
УдалитьФайлы(Путь, Маска)удаляет файлы по маскебез маски по пути каталога удаляет каталог целиком
СоздатьКаталог(Путь)создаёт каталогошибки нет, если каталог уже есть
ПолучитьРазделительПути()\ на Windows, / на Linuxнужен для переносимого кода

Типовая задача обмена: забрать все XML из папки, обработать и переложить в архив.

Разделитель = ПолучитьРазделительПути();
КаталогВходящих = "\\srv-files\obmen\in";
КаталогАрхива = "\\srv-files\obmen\archive";

СоздатьКаталог(КаталогАрхива);
Найденные = НайтиФайлы(КаталогВходящих, "*.xml", Ложь);

Для Каждого Ф Из Найденные Цикл
    Если Не Ф.ЭтоФайл() Тогда
        Продолжить;
    КонецЕсли;
    ОбработатьФайлОбмена(Ф.ПолноеИмя);
    ПереместитьФайл(Ф.ПолноеИмя, КаталогАрхива + Разделитель + Ф.Имя);
КонецЦикла;

УдалитьФайлы("D:\Обмен") без маски удалит каталог вместе со всем содержимым, без корзины. Перед удалением по пути, собранному из переменных, проверяйте, что переменная не пустая. Пустая строка или корень диска в такой переменной — частая причина потери данных.

Для регламентных заданий используйте UNC-пути (\\сервер\папка), а не буквы сетевых дисков. Служба сервера 1С не видит дисков, подключённых в сеансе пользователя.

Чтение и запись текстовых файлов

Для текста есть два подхода. ТекстовыйДокумент загружает файл в память целиком и даёт доступ к строкам по номеру. ЧтениеТекста и ЗаписьТекста идут по файлу последовательно и подходят для файлов любого размера.

ОбъектКогда братьПамять
ТекстовыйДокументнебольшой файл, нужен доступ к строке по номерувесь файл в памяти
ЧтениеТекстабольшой CSV или лог, построчная обработкаодна строка за раз
ЗаписьТекставыгрузка, лог, дописывание в конецпишет сразу на диск

Построчное чтение CSV

Чтение = Новый ЧтениеТекста(ИмяФайла, КодировкаТекста.UTF8);
Строка = Чтение.ПрочитатьСтроку(); // заголовок
Строка = Чтение.ПрочитатьСтроку();

Пока Строка <> Неопределено Цикл
    Поля = СтрРазделить(Строка, ";", Истина);
    Если Поля.Количество() >= 3 Тогда
        Артикул = СокрЛП(Поля[0]);
        Наименование = СокрЛП(Поля[1]);
        Цена = Число(СтрЗаменить(Поля[2], ",", "."));
        // запись в таблицу или справочник
    КонецЕсли;
    Строка = Чтение.ПрочитатьСтроку();
КонецЦикла;

Чтение.Закрыть();

ПрочитатьСтроку() возвращает Неопределено, когда строки кончились. Пустая строка в середине файла приходит как "", поэтому сравнивать надо именно с Неопределено. Третий параметр СтрРазделить со значением Истина сохраняет пустые поля, иначе колонки «съедут».

Запись и дописывание

// новый файл (существующий будет перезаписан)
Запись = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8);
Запись.ЗаписатьСтроку("Артикул;Наименование;Цена");
Запись.ЗаписатьСтроку(СтрШаблон("%1;%2;%3", "А-100", "Бумага А4", Формат(350.5, "ЧРД=.; ЧГ=0")));
Запись.Закрыть();

// дописать в конец: четвёртый параметр Дописывать = Истина
Лог = Новый ЗаписьТекста(ИмяЛога, КодировкаТекста.UTF8, , Истина);
Лог.ЗаписатьСтроку(Формат(ТекущаяДатаСеанса(), "ДФ='yyyy-MM-dd HH:mm:ss'") + " обмен завершён");
Лог.Закрыть();

Кодировка и BOM

Кодировку указывайте явно всегда — и при чтении, и при записи. Самые частые варианты:

  • 🔤 КодировкаТекста.UTF8 — UTF-8 с маркером BOM в начале файла;
  • 🔤 строка "UTF-8" вместо значения перечисления — UTF-8 без BOM, такой файл ждут многие веб-сервисы и сайты;
  • 🔤 КодировкаТекста.ANSI — Windows-1251, старые выгрузки и банковские файлы;
  • 🔤 КодировкаТекста.OEM — DOS-кодировка, встречается в выводе консольных утилит.

Если вместо кириллицы видите «Рђ» и подобное, файл записан в UTF-8, а читается как ANSI. Обратная ситуация даёт знаки вопроса или квадраты.

XML и JSON отдельно читать как текст не нужно: у ЧтениеXML и ЧтениеJSON есть метод ОткрытьФайл(ИмяФайла), а у ЗаписьXML и ЗаписьJSON — тоже ОткрытьФайл.

Двоичные данные и файловые потоки в 1С

Картинки, PDF, архивы и любые файлы «как есть» хранят в объекте ДвоичныеДанные. Он загружает файл в память целиком, что удобно до десятков мегабайт.

ДД = Новый ДвоичныеДанные("C:\Скан\договор.pdf");
Сообщить(Формат(ДД.Размер(), "ЧГ=0"));
ДД.Записать("C:\Скан\копия.pdf");

// в реквизит типа ХранилищеЗначения со сжатием
Объект.Скан = Новый ХранилищеЗначения(ДД, Новый СжатиеДанных(9));

// в строку для отправки в HTTP-сервис
Строка64 = Base64Строка(ДД);

Для больших файлов (сотни мегабайт, логи, выгрузки) используют потоки — они читают и пишут порциями. Доступ к ним идёт через свойство глобального контекста ФайловыеПотоки (платформа 8.3.9 и выше).

Копирование файла порциями

Источник = ФайловыеПотоки.ОткрытьДляЧтения(ИмяИсточника);
Приемник = ФайловыеПотоки.Создать(ИмяПриемника);
Буфер = Новый БуферДвоичныхДанных(1048576); // 1 МБ

Попытка
    Пока Истина Цикл
        Прочитано = Источник.Прочитать(Буфер, 0, Буфер.Размер);
        Если Прочитано = 0 Тогда
            Прервать;
        КонецЕсли;
        Приемник.Записать(Буфер, 0, Прочитано);
    КонецЦикла;
    Источник.Закрыть();
    Приемник.Закрыть();
Исключение
    Источник.Закрыть();
    Приемник.Закрыть();
    ВызватьИсключение;
КонецПопытки;

Прочитать возвращает число реально прочитанных байт. Последняя порция почти всегда меньше буфера, поэтому записывать нужно Прочитано байт, а не весь буфер — иначе в конец файла попадёт «мусор» от предыдущей порции.

Другие полезные методы потока: Размер(), ТекущаяПозиция(), Перейти(Смещение, ПозицияВПотоке.Начало) — для чтения с нужного места, например заголовка файла.

Контрольная сумма файла

Проверить, что файл не изменился или дошёл без повреждений, можно хешем:

Хеш = Новый ХешированиеДанных(ХешФункция.SHA256);
Хеш.ДобавитьФайл(ИмяФайла);
КонтрольнаяСумма = ПолучитьHexСтрокуИзДвоичныхДанных(Хеш.ХешСумма);

ДобавитьФайл читает файл сам, не загружая его в память целиком. На старых версиях платформы, где нет SHA256, доступны ХешФункция.MD5 и ХешФункция.SHA1.

Временные файлы и каталоги

Временный файл нужен почти в каждой загрузке и выгрузке: табличный документ сохраняют в XLSX, архив распаковывают, полученные двоичные данные пишут на диск для разбора.

  • 📁 КаталогВременныхФайлов() — путь к каталогу временных файлов текущего процесса;
  • 📄 ПолучитьИмяВременногоФайла("xlsx") — уникальное имя с нужным расширением в этом каталоге. Сам файл функция не создаёт.

Платформа не удаляет такие файлы за вас. На сервере, где выгрузки идут годами, каталог временных файлов пользователя службы может вырасти до гигабайт. Удаляйте файл в том же коде, который его создал, в том числе при ошибке:

&НаСервере
Функция ВыгрузитьВExcel(ТабДок)
    ИмяВрем = ПолучитьИмяВременногоФайла("xlsx");
    Попытка
        ТабДок.Записать(ИмяВрем, ТипФайлаТабличногоДокумента.XLSX);
        ДД = Новый ДвоичныеДанные(ИмяВрем);
    Исключение
        УдалитьФайлы(ИмяВрем);
        ВызватьИсключение;
    КонецПопытки;
    УдалитьФайлы(ИмяВрем);
    Возврат ПоместитьВоВременноеХранилище(ДД, УникальныйИдентификатор);
КонецФункции

Временный каталог, например для распаковки ZIP, создают с уникальным именем:

Корень = КаталогВременныхФайлов();
Если Не СтрЗаканчиваетсяНа(Корень, ПолучитьРазделительПути()) Тогда
    Корень = Корень + ПолучитьРазделительПути();
КонецЕсли;
КаталогРаспаковки = Корень + Строка(Новый УникальныйИдентификатор);
СоздатьКаталог(КаталогРаспаковки);

Архив = Новый ЧтениеZipФайла(ИмяАрхива);
Архив.ИзвлечьВсе(КаталогРаспаковки);
Архив.Закрыть();
// обработка файлов
УдалитьФайлы(КаталогРаспаковки);

В конфигурациях на БСП для этого есть общий модуль ФайловаяСистема с функциями создания и удаления временных каталогов. В типовых доработках лучше пользоваться им — так код ведёт себя одинаково с остальной конфигурацией.

Обработка ошибок при работе с файлами

Любая файловая операция может упасть: файл занят, нет прав, сетевая папка недоступна. Оборачивайте работу с файлами в Попытка ... Исключение и пишите причину в журнал регистрации, а не только в сообщение пользователю — регламентное задание сообщений никому не покажет.

Попытка
    КопироватьФайл(Источник, Приемник);
Исключение
    Инфо = ИнформацияОбОшибке();
    ЗаписьЖурналаРегистрации("Обмен.КопированиеФайла",
        УровеньЖурналаРегистрации.Ошибка, , ,
        ПодробноеПредставлениеОшибки(Инфо));
    ВызватьИсключение СтрШаблон("Не удалось скопировать %1: %2",
        Источник, КраткоеПредставлениеОшибки(Инфо));
КонецПопытки;

Три правила, которые снимают большинство проблем:

  1. Закрывайте ЧтениеТекста, ЗаписьТекста и потоки и в ветке Исключение. Незакрытый объект держит файл занятым до конца работы процесса.
  2. Проверяйте Существует() перед чтением и перед перемещением в архив. Сообщение «файл не найден» с полным путём понятнее системного текста.
  3. Не глушите исключение пустой веткой Исключение. Если ошибку нельзя исправить на месте, запишите её в журнал и вызовите заново.
Диагностика: выберите симптом

Если для выдачи прав или смены учётной записи приходится перезапускать службу сервера 1С, все сеансы пользователей оборвутся. Делайте это вне рабочего времени и предупредите людей заранее.

Частые вопросы

Как работать с файлами, если сервер 1С на Linux?
Пути пишите с прямой косой чертой и собирайте через ПолучитьРазделительПути(). Учтите, что имена файлов на Linux чувствительны к регистру: Prices.csv и prices.csv — разные файлы.
Можно ли открыть файл в связанной программе из 1С?
Да, на клиенте для этого есть ЗапуститьПриложениеАсинх() и НачатьЗапускПриложения(). Файл должен лежать на компьютере пользователя, поэтому полученный с сервера файл сначала сохраняют на клиент.
Как сохранить файл из реквизита ХранилищеЗначения на диск?
Получите значение методом Получить() у хранилища, это будут ДвоичныеДанные. Затем вызовите у них Записать(ПолноеИмяФайла) на той стороне, где нужен файл.
Как упаковать файлы в ZIP средствами 1С?
Объект ЗаписьZipФайла: создайте его с именем архива, добавьте файлы методом Добавить() и вызовите Записать(). Внешний архиватор не нужен.
Есть ли ограничение на размер файла?
Жёсткого лимита в объекте Файл нет, но ДвоичныеДанные и ТекстовыйДокумент держат файл в памяти целиком. Для файлов в сотни мегабайт используйте потоки или ЧтениеТекста.