Обход файлов и папок средствами встроенного языка 1С
Содержание
Перебрать все файлы в каталоге в 1С проще всего функцией НайтиФайлы(Путь, Маска, ИскатьВПодкаталогах): она возвращает массив объектов Файл, по которому идёт обычный цикл Для Каждого. Частое заблуждение — что в результат попадают только файлы. Нет: туда же приходят и вложенные папки, поэтому каждый элемент нужно проверять через ЭтоКаталог().
Второй подводный камень — место выполнения. Код на сервере ищет файлы на диске сервера 1С и с правами его службы, а не на компьютере пользователя. Всё описанное относится к платформе 8.3 и не зависит от конфигурации: БП, ЗУП, УТ или своя разработка — код один.
Чем 1С работает с файловой системой
Для обхода каталога хватает нескольких элементов встроенного языка. Запоминать их все не нужно, но знать, что есть в арсенале, полезно.
| Элемент | Что делает | Где пригодится |
|---|---|---|
НайтиФайлы() | Возвращает массив объектов Файл по пути и маске | Основа любого обхода |
Новый Файл(ПолноеИмя) | Объект для одного файла или папки | Проверка существования, свойства |
ПолучитьМаскуВсеФайлы() | Маска «все файлы» под текущую ОС | Кроссплатформенный код |
ПолучитьРазделительПути() | \ в Windows, / в Linux | Сборка путей вручную |
КаталогВременныхФайлов() | Путь к временной папке | Промежуточные файлы |
КопироватьФайл(), ПереместитьФайл(), УдалитьФайлы() | Операции над найденными файлами | Разбор папки обмена, архивация |
СоздатьКаталог() | Создаёт папку, включая промежуточные | Папка «Обработано» |
У объекта Файл есть свойства и методы, которые нужны почти в каждом цикле:
- 📄
Имя— имя с расширением:отчет.xml; - 🏷️
ИмяБезРасширения—отчет; - 🔤
Расширение— с точкой:.xml. Об этой точке часто забывают при сравнении; - 📂
Путь— папка, в которой лежит файл, с разделителем на конце; - 🧭
ПолноеИмя— путь целиком, его и передают в другие функции; - ✅
Существует(),ЭтоКаталог(),ЭтоФайл()— проверки; - 📏
Размер()— размер в байтах; - 🕒
ПолучитьВремяИзменения()— дата последнего изменения; - 👁️
ПолучитьНевидимость(),ПолучитьТолькоЧтение()— атрибуты.
Расширение, Имя и ПолноеИмя — это свойства, они берутся из строки пути. А Размер(), ЭтоКаталог() и время изменения — методы, каждый вызов обращается к диску. На больших каталогах эта разница заметна.
Перебор файлов одной папки без подкаталогов
Самый частый сценарий: в папку обмена падают выгрузки, их надо по очереди забрать. Подпапки не нужны.
Процедура ОбработатьПапкуОбмена(Каталог)
Элементы = НайтиФайлы(Каталог, ПолучитьМаскуВсеФайлы(), Ложь);
Для Каждого Элемент Из Элементы Цикл
Если Элемент.ЭтоКаталог() Тогда
Продолжить;
КонецЕсли;
Сообщить(СтрШаблон("%1 — %2 байт", Элемент.Имя, Формат(Элемент.Размер(), "ЧГ=0")));
КонецЦикла;
КонецПроцедуры
Что здесь происходит:
НайтиФайлыполучает путь к папке, маску иЛожь— не спускаться во вложенные папки.- Цикл идёт по массиву. Вложенные папки пропускаем через
Продолжить. - Для файла выводим имя и размер. Число форматируем через
Формат(..., "ЧГ=0"), иначе 1 048 576 байт выведется с неразрывными пробелами.
Третий параметр передавайте всегда явно. Так код читается без заглядывания в синтаксис-помощник, и никто не гадает, какое значение по умолчанию.
Если папку нужно разобрать и освободить, файлы можно переносить прямо в цикле. Массив, который вернула НайтиФайлы, — это снимок на момент вызова. Перемещение или удаление файла не ломает перебор.
ПапкаГотово = Каталог + ПолучитьРазделительПути() + "Обработано";
СоздатьКаталог(ПапкаГотово);
Для Каждого Элемент Из НайтиФайлы(Каталог, "*.xml", Ложь) Цикл
// ... загрузка файла ...
ПереместитьФайл(Элемент.ПолноеИмя, ПапкаГотово + ПолучитьРазделительПути() + Элемент.Имя);
КонецЦикла;
Если в папке «Обработано» уже лежит файл с таким же именем, ПереместитьФайл вызовет исключение. Добавляйте к имени дату или оборачивайте перенос в Попытка (об этом ниже).
Как обойти все файлы в каталоге вместе с подпапками
Есть два пути, и у каждого своя область применения.
Вариант 1. Одним вызовом НайтиФайлы
ВсеЭлементы = НайтиФайлы("D:\Архив", ПолучитьМаскуВсеФайлы(), Истина);
Для Каждого Элемент Из ВсеЭлементы Цикл
Если Элемент.ЭтоФайл() Тогда
// работа с файлом
КонецЕсли;
КонецЦикла;
Коротко и понятно. Минус один: платформа сначала обходит всё дерево и только потом отдаёт массив. На 200 000 файлов это ощутимая пауза и много памяти, а прервать обход на середине нельзя.
Вариант 2. Своя рекурсивная процедура
Рекурсия даёт контроль: можно пропускать папки целиком, ограничить глубину, остановиться после первой находки.
Процедура ОбойтиКаталог(Каталог, Результат, Глубина = 0, МаксГлубина = 20)
Если Глубина > МаксГлубина Тогда
Возврат;
КонецЕсли;
Элементы = НайтиФайлы(Каталог, ПолучитьМаскуВсеФайлы(), Ложь);
Для Каждого Элемент Из Элементы Цикл
Если Элемент.ЭтоКаталог() Тогда
Если НРег(Элемент.Имя) = "backup" Тогда
Продолжить; // эту ветку не обходим
КонецЕсли;
ОбойтиКаталог(Элемент.ПолноеИмя, Результат, Глубина + 1, МаксГлубина);
Иначе
Результат.Добавить(Элемент.ПолноеИмя);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
// Вызов
Файлы = Новый Массив;
ОбойтиКаталог("D:\Архив", Файлы);
Сообщить("Найдено файлов: " + Формат(Файлы.Количество(), "ЧН=0; ЧГ=0"));
Ограничение глубины — не перестраховка. В Windows бывают точки соединения (junction), которые ссылаются на родительскую папку. Без предела рекурсия на такой ссылке уходит в бесконечный цикл и заканчивается переполнением стека.
| Способ | Когда выбирать | Риски |
|---|---|---|
НайтиФайлы(..., Истина) | Дерево до нескольких тысяч файлов, нужно всё | Пауза и память на больших деревьях |
| Своя рекурсия | Нужно исключать папки, ограничить глубину, прервать поиск | Без предела глубины — зацикливание |
НайтиФайлы(..., Ложь) | Плоская папка обмена | Вложенные папки не увидит |
Фильтр по маске, расширению и дате
Маска в НайтиФайлы
Маска поддерживает * (любое количество символов) и ? (один символ):
*.xml— все XML-файлы;ORDERS_*.xml— выгрузки с префиксом;отчет_????.pdf—отчет_и ровно четыре символа, например год.
Маска одна. Записи вида *.xml;*.json или *.xml|*.json не работают как «или». Для нескольких расширений есть два способа: вызвать НайтиФайлы по разу на каждую маску или взять все файлы и отфильтровать в цикле.
Отбор по нескольким расширениям
Допустимые = Новый Соответствие;
Допустимые.Вставить(".xml", Истина);
Допустимые.Вставить(".json", Истина);
Допустимые.Вставить(".csv", Истина);
Для Каждого Элемент Из НайтиФайлы(Каталог, ПолучитьМаскуВсеФайлы(), Истина) Цикл
Если Элемент.ЭтоКаталог() Тогда
Продолжить;
КонецЕсли;
Если Допустимые[НРег(Элемент.Расширение)] = Неопределено Тогда
Продолжить;
КонецЕсли;
// подходящий файл
КонецЦикла;
Здесь два приёма. НРег приводит .XML и .Xml к одному виду. Соответствие проверяет вхождение за одно обращение, без перебора массива расширений.
Регистр и операционная система
- 🪟 В Windows маска
*.xmlнайдёт иORDER.XML. - 🐧 На сервере 1С под Linux регистр различается:
*.xmlне увидитORDER.XML. Надёжнее брать все файлы и сравниватьНРег(Элемент.Расширение). - ✳️ Маска
*.*в Linux требует точку в имени и пропустит файлREADME. Поэтому для «всех файлов» используйтеПолучитьМаскуВсеФайлы()— она вернёт правильную маску для текущей ОС.
Отбор по дате изменения
Типовая задача: удалить из папки логов всё старше 30 дней.
Граница = ТекущаяДата() - 30 * 86400; // 86 400 секунд в сутках
Для Каждого Элемент Из НайтиФайлы(КаталогЛогов, "*.log", Ложь) Цикл
Если Элемент.ПолучитьВремяИзменения() < Граница Тогда
УдалитьФайлы(Элемент.ПолноеИмя);
КонецЕсли;
КонецЦикла;
Удаление через УдалитьФайлы не попадает в корзину — файлы пропадают сразу. Перед первым запуском замените строку удаления на Сообщить(Элемент.ПолноеИмя) и посмотрите, что попадёт под отбор.
Сортировка результата
Порядок элементов в массиве платформа не обещает. Если файлы надо грузить по порядку (например, по дате создания выгрузки), сложите их в таблицу значений и отсортируйте:
Таблица = Новый ТаблицаЗначений;
Таблица.Колонки.Добавить("ПолноеИмя");
Таблица.Колонки.Добавить("Изменен");
Для Каждого Элемент Из НайтиФайлы(Каталог, "*.xml", Ложь) Цикл
Строка = Таблица.Добавить();
Строка.ПолноеИмя = Элемент.ПолноеИмя;
Строка.Изменен = Элемент.ПолучитьВремяИзменения();
КонецЦикла;
Таблица.Сортировать("Изменен Возр");
Ошибки, права доступа и где на самом деле выполняется код
Сервер или клиент
Это главный источник вопросов «на моём компьютере работает, а в базе нет».
- 🖥️ Код в общем модуле с флагом «Сервер», в процедуре с
&НаСервереили в регламентном задании в клиент-серверной базе работает на сервере 1С. ПутьC:\Выгрузкиозначает папку на диске сервера. - 👤 Права — у учётной записи, под которой запущена служба агента сервера 1С, а не у пользователя базы.
- 🌐 Подключённые сетевые диски (
Z:\) службе не видны. Пишите путь в формате UNC:\\fileserver\exchange\in, и выдайте учётной записи службы права на эту папку. - 💻 Чтобы обойти папку на компьютере пользователя, код должен выполняться на клиенте (
&НаКлиенте).
В файловой базе граница стирается: серверный код выполняется на том же компьютере, где запущен клиент. Поэтому ошибка часто всплывает только после перевода базы на сервер.
Перехват исключений
Несуществующая папка обычно не вызывает ошибки: НайтиФайлы просто вернёт пустой массив. Это хуже исключения — обработка «молча» ничего не делает. Проверяйте папку до обхода:
Папка = Новый Файл(Каталог);
Если Не Папка.Существует() Или Не Папка.ЭтоКаталог() Тогда
ВызватьИсключение СтрШаблон("Каталог не найден: %1", Каталог);
КонецЕсли;
Операции над конкретными файлами оборачивайте в Попытка, чтобы один занятый файл не останавливал весь пакет:
Для Каждого Элемент Из НайтиФайлы(Каталог, "*.xml", Ложь) Цикл
Попытка
ПереместитьФайл(Элемент.ПолноеИмя, ПапкаГотово + ПолучитьРазделительПути() + Элемент.Имя);
Исключение
ЗаписьЖурналаРегистрации("Обмен.Файлы", УровеньЖурналаРегистрации.Ошибка, , ,
Элемент.ПолноеИмя + ": " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
Запись в журнал регистрации полезнее Сообщить: регламентное задание сообщений никому не покажет, а журнал откроет администратор.
Безопасный режим
Внешние обработки, расширения в безопасном режиме и код с УстановитьБезопасныйРежим(Истина) к файловой системе не допускаются — платформа вызовет исключение о недостаточных правах. Если на сервере включены профили безопасности, доступ к каталогу нужно разрешить в профиле. В типовых конфигурациях на БСП для этого есть механизм запроса разрешений на использование внешних ресурсов.
Быстрый обход больших каталогов
Пока файлов сотни, о скорости не думают. На архиве в 100 000 файлов разница между аккуратным и небрежным кодом — минуты.
- Фильтруйте маской, а не в цикле, если расширение одно.
НайтиФайлы(Каталог, "*.pdf", Истина)отдаст меньше элементов, чем «все файлы» с проверкой внутри цикла. - Не вызывайте методы без нужды. Сначала отсекайте по
РасширениеиИмя(они не трогают диск), и только для оставшихся вызывайтеРазмер()иПолучитьВремяИзменения(). - Не храните объекты Файл, если хватает строк. Массив
ПолноеИмявесит меньше, чем массив объектов. - Исключайте тяжёлые ветки рекурсией. Папки резервных копий,
.git, временные каталоги не нужно обходить вообще. - Обходите там, где лежат файлы. Если папка на сервере, вызывайте один серверный метод, который вернёт готовый результат. Обход с клиента по сетевому пути медленнее, а передача каждого файла отдельным серверным вызовом — ещё медленнее.
- Долгий разбор — в фоновое задание. Обработку на десятки минут запускайте через
ФоновыеЗадания.Выполнить()с экспортной процедурой серверного общего модуля. Пользователь не ждёт с замороженным окном, а сеанс не обрывается по таймауту.
Асинхронный вариант на клиенте
В клиентском коде современных конфигураций синхронные вызовы часто запрещены свойством конфигурации «Режим использования синхронных вызовов расширений платформы и внешних компонент». Тогда НайтиФайлы на клиенте заменяют асинхронной версией. Начиная с платформы 8.3.18 удобнее всего НайтиФайлыАсинх с оператором Ждать:
&НаКлиенте
Асинх Процедура ПоказатьФайлы(Команда)
Найденные = Ждать НайтиФайлыАсинх(КаталогНаКлиенте, "*.xml", Ложь);
Для Каждого Элемент Из Найденные Цикл
Сообщить(Элемент.Имя);
КонецЦикла;
КонецПроцедуры
В более ранних релизах платформы та же задача решается через НачатьПоискФайлов с описанием оповещения. В веб-клиенте для работы с локальными файлами пользователю нужно установить расширение работы с файлами — без него обойти папку на его компьютере нельзя.
Частые ошибки при обходе файлов
- ❌ Сравнивают расширение без точки.
Элемент.Расширение = "xml"никогда не выполнится — свойство возвращает.xml. - ❌ Собирают путь вручную с
"\". На Linux-сервере такой путь не откроется. БеритеЭлемент.ПолноеИмяилиПолучитьРазделительПути(). - ❌ Не отделяют папки от файлов. Попытка прочитать папку как XML даёт непонятное исключение в середине обработки.
- ❌ Удаляют без пробного прогона. Ошибка в условии даты стирает всю папку, и восстановить её можно только из резервной копии.
- ❌ Проверяют код только в файловой базе. Права службы сервера и UNC-пути всплывают после переноса — тестируйте на клиент-серверной копии.
Частые вопросы
Как посчитать количество файлов в папке, не обрабатывая их?
НайтиФайлы с нужной маской и возьмите Количество() у массива. Если в папке есть подпапки, а считать нужно только файлы, пройдите циклом и считайте элементы, для которых ЭтоФайл() возвращает Истина.Как найти только папки без файлов?
ПолучитьМаскуВсеФайлы() и оставьте те, у которых ЭтоКаталог() равно Истина. Отдельной функции «найти каталоги» в платформе нет.Можно ли получить дату создания файла, а не изменения?
Файл надёжно доступна дата изменения через ПолучитьВремяИзменения(). Если важна именно последовательность выгрузок, удобнее закладывать дату и время в имя файла и сортировать по имени.Почему скрытые файлы попадают в результат?
НайтиФайлы атрибут «скрытый» не учитывает. Отсеивайте такие файлы проверкой ПолучитьНевидимость() — например, служебные Thumbs.db и desktop.ini в Windows.Как обойти папку на FTP-сервере?
НайтиФайлы работает только с файловой системой. Для FTP есть объект FTPСоединение и его метод НайтиФайлы со схожей логикой, но с путями FTP-сервера.