Обход файлов и папок средствами встроенного языка 1С

· 10 мин

Содержание

Перебрать все файлы в каталоге в 1С проще всего функцией НайтиФайлы(Путь, Маска, ИскатьВПодкаталогах): она возвращает массив объектов Файл, по которому идёт обычный цикл Для Каждого. Частое заблуждение — что в результат попадают только файлы. Нет: туда же приходят и вложенные папки, поэтому каждый элемент нужно проверять через ЭтоКаталог().

Второй подводный камень — место выполнения. Код на сервере ищет файлы на диске сервера 1С и с правами его службы, а не на компьютере пользователя. Всё описанное относится к платформе 8.3 и не зависит от конфигурации: БП, ЗУП, УТ или своя разработка — код один.

Чем 1С работает с файловой системой

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

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

У объекта Файл есть свойства и методы, которые нужны почти в каждом цикле:

  • 📄 Имя — имя с расширением: отчет.xml;
  • 🏷️ ИмяБезРасширения — отчет;
  • 🔤 Расширение — с точкой: .xml. Об этой точке часто забывают при сравнении;
  • 📂 Путь — папка, в которой лежит файл, с разделителем на конце;
  • 🧭 ПолноеИмя — путь целиком, его и передают в другие функции;
  • ✅ Существует(), ЭтоКаталог(), ЭтоФайл() — проверки;
  • 📏 Размер() — размер в байтах;
  • 🕒 ПолучитьВремяИзменения() — дата последнего изменения;
  • 👁️ ПолучитьНевидимость(), ПолучитьТолькоЧтение() — атрибуты.

Расширение, Имя и ПолноеИмя — это свойства, они берутся из строки пути. А Размер(), ЭтоКаталог() и время изменения — методы, каждый вызов обращается к диску. На больших каталогах эта разница заметна.

Перебор файлов одной папки без подкаталогов

Самый частый сценарий: в папку обмена падают выгрузки, их надо по очереди забрать. Подпапки не нужны.

Процедура ОбработатьПапкуОбмена(Каталог)

	Элементы = НайтиФайлы(Каталог, ПолучитьМаскуВсеФайлы(), Ложь);

	Для Каждого Элемент Из Элементы Цикл
		Если Элемент.ЭтоКаталог() Тогда
			Продолжить;
		КонецЕсли;

		Сообщить(СтрШаблон("%1 — %2 байт", Элемент.Имя, Формат(Элемент.Размер(), "ЧГ=0")));
	КонецЦикла;

КонецПроцедуры

Что здесь происходит:

  1. НайтиФайлы получает путь к папке, маску и Ложь — не спускаться во вложенные папки.
  2. Цикл идёт по массиву. Вложенные папки пропускаем через Продолжить.
  3. Для файла выводим имя и размер. Число форматируем через Формат(..., "ЧГ=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 файлов разница между аккуратным и небрежным кодом — минуты.

  1. Фильтруйте маской, а не в цикле, если расширение одно. НайтиФайлы(Каталог, "*.pdf", Истина) отдаст меньше элементов, чем «все файлы» с проверкой внутри цикла.
  2. Не вызывайте методы без нужды. Сначала отсекайте по Расширение и Имя (они не трогают диск), и только для оставшихся вызывайте Размер() и ПолучитьВремяИзменения().
  3. Не храните объекты Файл, если хватает строк. Массив ПолноеИмя весит меньше, чем массив объектов.
  4. Исключайте тяжёлые ветки рекурсией. Папки резервных копий, .git, временные каталоги не нужно обходить вообще.
  5. Обходите там, где лежат файлы. Если папка на сервере, вызывайте один серверный метод, который вернёт готовый результат. Обход с клиента по сетевому пути медленнее, а передача каждого файла отдельным серверным вызовом — ещё медленнее.
  6. Долгий разбор — в фоновое задание. Обработку на десятки минут запускайте через ФоновыеЗадания.Выполнить() с экспортной процедурой серверного общего модуля. Пользователь не ждёт с замороженным окном, а сеанс не обрывается по таймауту.

Асинхронный вариант на клиенте

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

&НаКлиенте
Асинх Процедура ПоказатьФайлы(Команда)

	Найденные = Ждать НайтиФайлыАсинх(КаталогНаКлиенте, "*.xml", Ложь);

	Для Каждого Элемент Из Найденные Цикл
		Сообщить(Элемент.Имя);
	КонецЦикла;

КонецПроцедуры

В более ранних релизах платформы та же задача решается через НачатьПоискФайлов с описанием оповещения. В веб-клиенте для работы с локальными файлами пользователю нужно установить расширение работы с файлами — без него обойти папку на его компьютере нельзя.

Частые ошибки при обходе файлов

  • ❌ Сравнивают расширение без точки. Элемент.Расширение = "xml" никогда не выполнится — свойство возвращает .xml.
  • ❌ Собирают путь вручную с "\". На Linux-сервере такой путь не откроется. Берите Элемент.ПолноеИмя или ПолучитьРазделительПути().
  • ❌ Не отделяют папки от файлов. Попытка прочитать папку как XML даёт непонятное исключение в середине обработки.
  • ❌ Удаляют без пробного прогона. Ошибка в условии даты стирает всю папку, и восстановить её можно только из резервной копии.
  • ❌ Проверяют код только в файловой базе. Права службы сервера и UNC-пути всплывают после переноса — тестируйте на клиент-серверной копии.

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

Как посчитать количество файлов в папке, не обрабатывая их?
Вызовите НайтиФайлы с нужной маской и возьмите Количество() у массива. Если в папке есть подпапки, а считать нужно только файлы, пройдите циклом и считайте элементы, для которых ЭтоФайл() возвращает Истина.
Как найти только папки без файлов?
Возьмите все элементы маской ПолучитьМаскуВсеФайлы() и оставьте те, у которых ЭтоКаталог() равно Истина. Отдельной функции «найти каталоги» в платформе нет.
Можно ли получить дату создания файла, а не изменения?
У объекта Файл надёжно доступна дата изменения через ПолучитьВремяИзменения(). Если важна именно последовательность выгрузок, удобнее закладывать дату и время в имя файла и сортировать по имени.
Почему скрытые файлы попадают в результат?
НайтиФайлы атрибут «скрытый» не учитывает. Отсеивайте такие файлы проверкой ПолучитьНевидимость() — например, служебные Thumbs.db и desktop.ini в Windows.
Как обойти папку на FTP-сервере?
НайтиФайлы работает только с файловой системой. Для FTP есть объект FTPСоединение и его метод НайтиФайлы со схожей логикой, но с путями FTP-сервера.