При работе с файловыми хранилищами, архивами или логами программисты 1С часто сталкиваются с задачей рекурсивного обхода каталогов. Когда счет файлов идет на сотни тысяч или миллионы, стандартные средства платформы начинают работать неприемлемо медленно. Разберем, почему это происходит, и проанализируем наиболее эффективные способы решения этой задачи, начиная от простых скриптов и заканчивая низкоуровневыми системными вызовами.
Рассмотрим классический «лобовой» подход, который использует большинство разработчиков. Он заключается в рекурсивном вызове встроенной функции НайтиФайлы(). Проанализируем типичный пример кода:
Процедура ЗаполнитьРекурсивно(Адрес, ТабФайлов)
НайденныеФайлы = НайтиФайлы(Адрес, "*.*");
Для Каждого Файл Из НайденныеФайлы Цикл
Попытка
Если Файл.ЭтоФайл() Тогда
НоваяСтрока = ТабФайлов.Добавить();
НоваяСтрока.ДатаВремя = Файл.ПолучитьВремяИзменения();
НоваяСтрока.Длина = Файл.Размер();
НоваяСтрока.ПутьОригинальный = Файл.ПолноеИмя;
Иначе
// Рекурсивный переход в подпапку
ЗаполнитьРекурсивно(Файл.ПолноеИмя, ТабФайлов);
КонецЕсли;
Исключение
Сообщить("Ошибка доступа: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Почему это медленно? Основная потеря времени происходит не на самом поиске, а на обращении к свойствам объекта Файл. При каждом вызове методов Размер() или ПолучитьВремяИзменения() платформа 1С выполняет отдельный системный запрос к файловой таблице (MFT). Если файлов миллион, мы получаем миллионы микро-запросов к диску, что создает колоссальные накладные расходы.
Одним из способов ускорить процесс является использование системного объекта Scripting.FileSystemObject. Этот метод часто показывает лучшие результаты на локальных дисках, так как механизмы перебора объектов в COM реализованы несколько иначе.
Рассмотрим реализацию обхода через FSO:
Процедура ПолучитьИнформациюОФайлах(Путь)
ТабФайлов.Очистить();
ФСО = Новый COMОбъект("Scripting.FileSystemObject");
ОбходПодпапок(ФСО.GetFolder(Путь));
КонецПроцедуры
Процедура ОбходПодпапок(Folder)
// Обрабатываем файлы в текущей папке
Для Каждого файл Из Folder.Files Цикл
Стр = ТабФайлов.Добавить();
Стр.ДатаИзменения = файл.DateLastModified;
Стр.Имя = файл.Name;
Стр.Путь = файл.Path;
Стр.Размер = файл.Size;
КонецЦикла;
// Рекурсивно заходим в подпапки
Для Каждого Subfolder Из Folder.SubFolders Цикл
ОбходПодпапок(Subfolder);
КонецЦикла;
КонецПроцедуры
По результатам тестов, для структуры из 660 000 файлов такой метод может занять около 12 минут. Это быстрее стандартного метода 1С, но все еще недостаточно эффективно для критически важных задач.
Выясним причину, по которой системные утилиты работают быстрее. Утилита dir оптимизирована для прямого чтения потока данных из файловой системы. Мы можем запустить формирование текстового отчета средствами Windows и затем прочитать его в 1С. Это избавляет нас от создания сотен тысяч промежуточных объектов в памяти 1С.
Разберем алгоритм реализации:
cmd.exe./S для рекурсивного поиска и /T:W для вывода времени записи.>.
ИмяВременногоФайла = КаталогВременныхФайлов() + "file_list.txt";
Команда = "cmd /c dir """ + ПутьККаталогу + """ /S /R > """ + ИмяВременногоФайла + """";
КомандаСистемы(Команда);
// Далее используем ТекстовыйДокумент или ЧтениеТекста для парсинга файла
Текст = Новый ТекстовыйДокумент;
Текст.Прочитать(ИмяВременногоФайла, КодировкаТекста.OEM);
Этот метод позволяет обработать 1 200 000 файлов примерно за 10 минут. Основное преимущество здесь в том, что операционная система сама кэширует чтение файловой таблицы, а 1С получает уже готовый текстовый массив данных.
Если требуется еще большая надежность и скорость (особенно на сетевых ресурсах), проанализируем использование утилиты Robocopy. Она встроена в современные версии Windows и обладает мощными механизмами многопоточного сканирования.
Для получения списка файлов без их копирования используется флаг /L. Пример команды:
robocopy "C:\Source" "C:\FakeDest" /L /S /FP /NJH /NJS /TS /NC /NS
Разберем ключи:
/L — только список (list), файлы не копируются./S — включая подкаталоги./FP — выводить полный путь файла./TS — включать метку времени файла./NJH /NJS — убрать заголовки и сводку (упрощает парсинг).Если ваша конфигурация работает под управлением Windows и вам доступно использование объектов System.IO, мы можем применить методы .NET Framework. Например, класс DirectoryInfo и метод EnumerateFiles работают значительно эффективнее, чем GetFiles, так как начинают возвращать данные сразу, не дожидаясь окончания сканирования всего дерева.
Для достижения абсолютного максимума скорости следует смотреть в сторону внешних компонент на C++, использующих функции WinAPI FindFirstFileW и FindNextFileW. Эти функции позволяют считать структуру WIN32_FIND_DATA, которая содержит имя, размер и даты файла за один единственный вызов. Это исключает лишние обращения к MFT, о которых мы говорили в начале.
При решении задач массового сканирования файлов помните о следующих правилах:
НайтиФайлы(), старайтесь не вызывать методы объекта Файл внутри цикла без крайней необходимости.1cv8.exe или сканируемую папку в исключения.dir или robocopy, почти всегда быстрее, чем прямой обход объектов через COM или встроенные функции 1С на больших объемах.Таким образом, для большинства повседневных задач оптимальным по соотношению «сложность/скорость» является метод с использованием системной команды dir и последующим чтением текстового файла. Если же требуется работа в промышленном масштабе с миллионами объектов, стоит рассмотреть написание внешней компоненты на Native API.