Подтвердить что ты не робот

Существует ли более быстрый способ найти все файлы в каталоге и во всех подкаталогах?

Я пишу программу, которая должна искать каталог и все его подкаталоги для файлов с определенным расширением. Это будет использоваться как на локальном, так и на сетевом диске, поэтому производительность является проблемой.

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

private void GetFileList(string fileSearchPattern, string rootFolderPath, List<FileInfo> files)
{
    DirectoryInfo di = new DirectoryInfo(rootFolderPath);

    FileInfo[] fiArr = di.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
    files.AddRange(fiArr);

    DirectoryInfo[] diArr = di.GetDirectories();

    foreach (DirectoryInfo info in diArr)
    {
        GetFileList(fileSearchPattern, info.FullName, files);
    }
}

Я могу установить SearchOption в AllDirectories и не использовать рекурсивный метод, но в будущем мне захочется вставить код, чтобы уведомить пользователя о том, какая папка в настоящее время проверяется.

Пока я создаю список объектов FileInfo, все, что мне действительно волнует, это пути к файлам. У меня будет существующий список файлов, который я хочу сравнить с новым списком файлов, чтобы увидеть, какие файлы были добавлены или удалены. Есть ли более быстрый способ создания этого списка путей к файлу? Есть ли что-то, что я могу сделать, чтобы оптимизировать этот поиск файлов вокруг запросов к файлам на общем сетевом диске?


Обновление 1

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

public static List<FileInfo> GetFileList(string fileSearchPattern, string rootFolderPath)
{
    DirectoryInfo rootDir = new DirectoryInfo(rootFolderPath);

    List<DirectoryInfo> dirList = new List<DirectoryInfo>(rootDir.GetDirectories("*", SearchOption.AllDirectories));
    dirList.Add(rootDir);

    List<FileInfo> fileList = new List<FileInfo>();

    foreach (DirectoryInfo dir in dirList)
    {
        fileList.AddRange(dir.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly));
    }

    return fileList;
}

Обновление 2

Хорошо, поэтому я запустил несколько тестов в локальной и удаленной папке, у которых есть много файлов (~ 1200). Вот методы, на которых я запускал тесты. Ниже приведены результаты.

  • GetFileListA(). Нерекурсивное решение в приведенном выше обновлении. Я думаю, что это эквивалентно решению Джея.
  • GetFileListB(): рекурсивный метод из исходного вопроса
  • GetFileListC(). Получает все каталоги со статическим методом Directory.GetDirectories(). Затем получает все пути к файлу со статическим методом Directory.GetFiles(). Заполняет и возвращает список
  • GetFileListD(): решение Марка Гравелла с использованием очереди и возвращает IEnumberable. Я заполнил список с результатом IEnumerable
    • DirectoryInfo.GetFiles: никакого дополнительного метода не создано. Создал экземпляр DirectoryInfo из корневого каталога. Вызывается GetFiles с использованием SearchOption.AllDirectories
  • Directory.GetFiles: дополнительный метод не создан. Вызывается статическим методом GetFiles каталога с использованием SearchOption.AllDirectories
Method                       Local Folder       Remote Folder
GetFileListA()               00:00.0781235      05:22.9000502
GetFileListB()               00:00.0624988      03:43.5425829
GetFileListC()               00:00.0624988      05:19.7282361
GetFileListD()               00:00.0468741      03:38.1208120
DirectoryInfo.GetFiles       00:00.0468741      03:45.4644210
Directory.GetFiles           00:00.0312494      03:48.0737459

.,.so выглядит как Марк самый быстрый.

4b9b3361

Ответ 1

Попробуйте эту версию блока итератора, которая позволяет избежать рекурсии и объектов Info:

public static IEnumerable<string> GetFileList(string fileSearchPattern, string rootFolderPath)
{
    Queue<string> pending = new Queue<string>();
    pending.Enqueue(rootFolderPath);
    string[] tmp;
    while (pending.Count > 0)
    {
        rootFolderPath = pending.Dequeue();
        try
        {
            tmp = Directory.GetFiles(rootFolderPath, fileSearchPattern);
        }
        catch (UnauthorizedAccessException)
        {
            continue;
        }
        for (int i = 0; i < tmp.Length; i++)
        {
            yield return tmp[i];
        }
        tmp = Directory.GetDirectories(rootFolderPath);
        for (int i = 0; i < tmp.Length; i++)
        {
            pending.Enqueue(tmp[i]);
        }
    }
}

Обратите внимание, что 4.0 имеет встроенные версии блока итератора (EnumerateFiles, EnumerateFileSystemEntries), который может быть быстрее (более прямой доступ к файловой системе, меньше массивов)

Ответ 2

Холодный вопрос.

Я немного поиграл и, используя блоки итераторов, и LINQ I, похоже, улучшил вашу пересмотренную реализацию примерно на 40%

Мне было бы интересно, если вы проверите его, используя свои методы синхронизации и в своей сети, чтобы увидеть, как выглядит разница.

Вот его мясо

private static IEnumerable<FileInfo> GetFileList(string searchPattern, string rootFolderPath)
{
    var rootDir = new DirectoryInfo(rootFolderPath);
    var dirList = rootDir.GetDirectories("*", SearchOption.AllDirectories);

    return from directoriesWithFiles in ReturnFiles(dirList, searchPattern).SelectMany(files => files)
           select directoriesWithFiles;
}

private static IEnumerable<FileInfo[]> ReturnFiles(DirectoryInfo[] dirList, string fileSearchPattern)
{
    foreach (DirectoryInfo dir in dirList)
    {
        yield return dir.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
    }
}

Ответ 3

Краткий ответ о том, как улучшить производительность этого кода, это: Вы не можете.

Реальная производительность влияет на ваше восприятие - это реальная латентность диска или сети, поэтому независимо от того, каким образом вы его переворачиваете, вам нужно проверить и перебрать каждый элемент файла и получить список каталогов и файлов. (Это, конечно, исключает модификации оборудования или драйвера для уменьшения или улучшения задержки на диске, но многие люди уже заплатили много денег, чтобы решить эти проблемы, поэтому мы будем игнорировать эту сторону на данный момент)

Учитывая исходные ограничения, существует несколько уже опубликованных решений, которые более или менее элегантно обертывают процесс итерации (однако, поскольку я предполагаю, что я читаю с одного жесткого диска, parallelism НЕ поможет более быстро повернуть дерево каталогов и даже может увеличиться с тех пор, как теперь у вас есть два или более потока, сражающихся за данные на разных частях диска, когда он пытается искать назад и четвертый), уменьшает количество созданных объектов и т.д. Однако, если мы оценим как функция будет потребляться конечным разработчиком, есть некоторые оптимизации и обобщения, которые мы можем придумать.

Во-первых, мы можем отсрочить выполнение производительности, вернув IEnumerable, return yield справится с этим путем компиляции в перечислителе конечных автоматов внутри анонимного класса, который реализует IEnumerable и возвращается, когда метод выполняется. Большинство методов в LINQ записываются для задержки выполнения до тех пор, пока не будет выполнена итерация, поэтому код в select или SelectMany не будет выполняться до тех пор, пока IEnumerable не будет повторен. Конечный результат отложенного исполнения ощущается только тогда, когда вам нужно взять подмножество данных позднее, например, если вам нужны только первые 10 результатов, задержка выполнения запроса, который возвращает несколько тысяч результатов, не будет итерации по всем 1000 результатам, пока вам не понадобится более десяти.

Теперь, учитывая, что вы хотите выполнить поиск в подпапках, я также могу сделать вывод, что может быть полезно, если вы можете указать эту глубину, и если я это сделаю, это также обобщает мою проблему, но также требует рекурсивного решения. Затем, позже, когда кто-то решит, что теперь нужно искать два каталога глубоко, потому что мы увеличили количество файлов и решили добавить еще один класс категоризации, вы можете просто сделать небольшую модификацию вместо повторной записи функции.

В свете всего этого, вот решение, которое я придумал, дает более общее решение, чем некоторые из вышеперечисленных:

public static IEnumerable<FileInfo> BetterFileList(string fileSearchPattern, string rootFolderPath)
{
    return BetterFileList(fileSearchPattern, new DirectoryInfo(rootFolderPath), 1);
}

public static IEnumerable<FileInfo> BetterFileList(string fileSearchPattern, DirectoryInfo directory, int depth)
{
    return depth == 0
        ? directory.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly)
        : directory.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly).Concat(
            directory.GetDirectories().SelectMany(x => BetterFileList(fileSearchPattern, x, depth - 1)));
}

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

Ответ 4

Это займет 30 секунд, чтобы получить 2 миллиона имен файлов, соответствующих фильтру. Причина, по которой это так быстро, состоит в том, что я выполняю только 1 перечисление. Каждое дополнительное перечисление влияет на производительность. Переменная длина открыта для вашей интерпретации и не обязательно связана с примером перечисления.

if (Directory.Exists(path))
{
    files = Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)
    .Where(s => s.EndsWith(".xml") || s.EndsWith(".csv"))
    .Select(s => s.Remove(0, length)).ToList(); // Remove the Dir info.
}

Ответ 5

Рассмотрим разбиение обновленного метода на два итератора:

private static IEnumerable<DirectoryInfo> GetDirs(string rootFolderPath)
{
     DirectoryInfo rootDir = new DirectoryInfo(rootFolderPath);
     yield return rootDir;

     foreach(DirectoryInfo di in rootDir.GetDirectories("*", SearchOption.AllDirectories));
     {
          yield return di;
     }
     yield break;
}

public static IEnumerable<FileInfo> GetFileList(string fileSearchPattern, string rootFolderPath)
{
     var allDirs = GetDirs(rootFolderPath);
     foreach(DirectoryInfo di in allDirs())
     {
          var files = di.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
          foreach(FileInfo fi in files)
          {
               yield return fi;
          }
     }
     yield break;
}

Кроме того, в дополнение к сценарию, специфичному для сети, если вы смогли установить небольшую услугу на этом сервере, с которой вы могли бы позвонить с клиентского компьютера, вы бы приблизились к результатам своей "локальной папки", потому что поиск может выполняться на сервере и просто возвращать результаты вам. Это будет вашим самым большим ускорением в сценарии сетевых папок, но может быть недоступным в вашей ситуации. Я использую программу синхронизации файлов, которая включает эту опцию - как только я установил службу на своем сервере, программа быстрее стала WAY при идентификации файлов, которые были новыми, удаленными и несинхронными.

Ответ 6

Попробуйте параллельное программирование:

private string _fileSearchPattern;
private List<string> _files;
private object lockThis = new object();

public List<string> GetFileList(string fileSearchPattern, string rootFolderPath)
{
    _fileSearchPattern = fileSearchPattern;
    AddFileList(rootFolderPath);
    return _files;
}

private void AddFileList(string rootFolderPath)
{
    var files = Directory.GetFiles(rootFolderPath, _fileSearchPattern);
    lock (lockThis)
    {
        _files.AddRange(files);
    }

    var directories = Directory.GetDirectories(rootFolderPath);

    Parallel.ForEach(directories, AddFileList); // same as Parallel.ForEach(directories, directory => AddFileList(directory));
}

Ответ 7

Справочная информация, по-видимому, дает гораздо больше информации, чем вам нужно, попробуйте подключить команду dir и проанализировать информацию.

Ответ 8

Методы BCL являются портативными, так сказать. Если вы останетесь на 100%, я считаю, что лучшее, что вы можете сделать, это вызвать GetDirectories/Folders при проверке прав доступа (или, возможно, не проверять права и иметь другой поток, готовый к работе, когда первый занимает слишком много времени - знак того, что он для исключения UnauthorizedAccess - этого можно избежать с помощью фильтров исключений, использующих VB или на сегодняшний день неизданный С#).

Если вы хотите быстрее, чем GetDirectories, вы должны вызвать win32 (findomethingEx и т.д.), который предоставляет определенные флаги, которые позволяют игнорировать, возможно, ненужный IO при обходе структур MFT. Кроме того, если диск является общим сетевым ресурсом, может быть отличное ускорение с помощью аналогичного подхода, но на этот раз избежать чрезмерных переходов по сети.

Теперь, если у вас есть админ и вы используете ntfs, и вы действительно спешите с миллионами файлов, чтобы пройти через них, самый быстрый способ пройти через них (при условии, что вращение ржавчины, когда убивает диск), используется как mft, так и журналирование в сочетании, по существу, заменяя службу индексирования той, которая предназначена для вашей конкретной потребности. Если вам нужно только найти имена файлов, а не размеры (или размеры тоже, но затем вы должны кэшировать их и использовать журнал для уведомления об изменениях), этот подход может обеспечить практически мгновенный поиск десятков миллионов файлов и папок, если он будет идеально реализован. Там может быть одна или две зарплаты, которые беспокоили это. В С# есть образцы как MFT (DiscUtils), так и чтения журнала (google). У меня только около 5 миллионов файлов, и просто использование NTFSSearch достаточно для этой суммы, так как для их поиска требуется около 10-20 секунд. С добавлением чтения журнала он снизился до < 3 секунды для этой суммы.

Ответ 9

Я был бы склонен возвращать IEnumerable < > в этом случае - в зависимости от того, как вы используете результаты, это может быть улучшением, плюс вы уменьшите размер вашего параметра на 1/3 и избегаете прохождения вокруг этого списка непрерывно.

private IEnumerable<FileInfo> GetFileList(string fileSearchPattern, string rootFolderPath)
{
    DirectoryInfo di = new DirectoryInfo(rootFolderPath);

    var fiArr = di.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
    foreach (FileInfo fi in fiArr)
    {
        yield return fi;
    }

    var diArr = di.GetDirectories();

    foreach (DirectoryInfo di in diArr)
    {
        var nextRound = GetFileList(fileSearchPattern, di.FullnName);
        foreach (FileInfo fi in nextRound)
        {
            yield return fi;
        }
    }
    yield break;
}

Еще одна идея - открутить объекты BackgroundWorker для прокрутки каталогов. Вам не нужен новый поток для каждого каталога, но вы можете создать их на верхнем уровне (сначала пройдите через GetFileList()), поэтому, если вы выполните на своем диске C:\ с 12 каталогами, каждый из этих каталогов будет искать другой поток, который затем рекурсирует через подкаталоги. У вас будет один поток, проходящий через C:\Windows, а другой пройдет через C:\Program Files. Есть много переменных относительно того, как это повлияет на производительность - вам нужно будет проверить это, чтобы видеть.

Ответ 11

Это ужасно, и работа по поиску файлов с ошибками ужасна на платформах Windows, потому что MS допустила ошибку, которую они, похоже, не хотят ставить правильно. Вы должны иметь возможность использовать SearchOption.AllDirectories И мы все получили бы скорость назад, которую хотим. Но вы не можете этого сделать, потому что GetDirectories нуждается в обратном вызове, чтобы вы могли решить, что делать с каталогами, к которым у вас нет доступа. MS забыла или не подумала протестировать класс на своих компьютерах.

Итак, мы все остаемся с бессмысленными рекурсивными циклами.

В С#/Managed С++ у вас очень мало приложений, это также варианты, которые MS принимает, потому что их кодеры не разработали, как обойти это.

Главное - с элементами отображения, такими как TreeViews и FileViews, только поиск и просмотр того, что пользователи могут видеть. Есть множество помощников на элементах управления, включая триггеры, которые сообщают вам, когда вам нужно заполнить некоторые данные.

В деревьях, начиная с свернутого режима, поиск по одному каталогу, когда и когда пользователь открывает его в дереве, это намного быстрее, чем ждать заполнения всего дерева. То же самое в FileViews, я склонен к правилу 10%, как когда-либо много элементов вписываются в область отображения, еще 10% готовы, если пользователь прокручивает, он хорошо реагирует.

MS выполняет предварительный поиск и просмотр каталога. Небольшая база данных каталогов, файлов, это означает, что вы OnOpen ваши деревья и т.д. Имеют хорошую быструю отправную точку, она немного падает на обновление.

Но смешайте две идеи, возьмите свои каталоги и файлы из базы данных, но выполните поиск обновлений, поскольку дерево node будет расширено (только это дерево node) и в качестве другого каталога будет выбрано дерево.

Но лучшим решением является добавление системы поиска файлов в качестве службы. У MS уже есть это, но насколько я знаю, мы не получаем к нему доступа, я подозреваю, что это связано с тем, что он невосприимчив к ошибкам "неудачного доступа к каталогу". Как и в случае с MS, если у вас есть служба, работающая на уровне администратора, вам нужно быть осторожным, чтобы вы не отдали свою безопасность только ради небольшой дополнительной скорости.

Ответ 12

У меня такая же проблема. Вот моя попытка, которая намного быстрее вызова рекурсивного Directory.EnumerateFiles, Directory.EnumerateDirectories или Directory.EnumerateFileSystemEntries:

public static IEnumerable<string> EnumerateDirectoriesRecursive(string directoryPath)
{
    return EnumerateFileSystemEntries(directoryPath).Where(e => e.isDirectory).Select(e => e.EntryPath);
}

public static IEnumerable<string> EnumerateFilesRecursive(string directoryPath)
{
    return EnumerateFileSystemEntries(directoryPath).Where(e => !e.isDirectory).Select(e => e.EntryPath);
}

public static IEnumerable<(string EntryPath, bool isDirectory)> EnumerateFileSystemEntries(string directoryPath)
{
    Stack<string> directoryStack = new Stack<string>(new[] { directoryPath });

    while (directoryStack.Any())
    {
        foreach (string fileSystemEntry in Directory.EnumerateFileSystemEntries(directoryStack.Pop()))
        {
            bool isDirectory = (File.GetAttributes(fileSystemEntry) & (FileAttributes.Directory | FileAttributes.ReparsePoint)) == FileAttributes.Directory;

            yield return (fileSystemEntry, isDirectory);

            if (isDirectory)
                directoryStack.Push(fileSystemEntry);
        }
    }
}

Вы можете изменить код для поиска конкретных файлов или каталогов легко.

С уважением

Ответ 13

Для поиска файлов и каталогов я хотел бы предложить использовать многопоточную библиотеку .NET, которая обладает широкими возможностями поиска. Всю информацию о библиотеке вы можете найти на GitHub: https://github.com/VladPVS/FastSearchLibrary

Если вы хотите скачать его, вы можете сделать это здесь: https://github.com/VladPVS/FastSearchLibrary/releases

Работает очень быстро. Проверьте это сами!

Если у вас есть какие-либо вопросы, пожалуйста, задавайте их.

Это один пример того, как вы можете его использовать:

class Searcher
{
    private static object locker = new object(); 

    private FileSearcher searcher;

    List<FileInfo> files;

    public Searcher()
    {
        files = new List<FileInfo>(); // create list that will contain search result
    }

    public void Startsearch()
    {
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        // create tokenSource to get stop search process possibility

        searcher = new FileSearcher(@"C:\", (f) =>
        {
            return Regex.IsMatch(f.Name, @".*[Dd]ragon.*.jpg$");
        }, tokenSource);  // give tokenSource in constructor


        searcher.FilesFound += (sender, arg) => // subscribe on FilesFound event
        {
            lock (locker) // using a lock is obligatorily
            {
                arg.Files.ForEach((f) =>
                {
                    files.Add(f); // add the next part of the received files to the results list
                    Console.WriteLine($"File location: {f.FullName}, \nCreation.Time: {f.CreationTime}");
                });

                if (files.Count >= 10) // one can choose any stopping condition
                    searcher.StopSearch();
            }
        };

        searcher.SearchCompleted += (sender, arg) => // subscribe on SearchCompleted event
        {
            if (arg.IsCanceled) // check whether StopSearch() called
                Console.WriteLine("Search stopped.");
            else
                Console.WriteLine("Search completed.");

            Console.WriteLine($"Quantity of files: {files.Count}"); // show amount of finding files
        };

        searcher.StartSearchAsync();
        // start search process as an asynchronous operation that doesn't block the called thread
    }
}

Это еще один пример:

***
List<string> folders = new List<string>
{
  @"C:\Users\Public",
  @"C:\Windows\System32",
  @"D:\Program Files",
  @"D:\Program Files (x86)"
}; // list of search directories

List<string> keywords = new List<string> { "word1", "word2", "word3" }; // list of search keywords

FileSearcherMultiple multipleSearcher = new FileSearcherMultiple(folders, (f) =>
{
  if (f.CreationTime >= new DateTime(2015, 3, 15) &&
     (f.Extension == ".cs" || f.Extension == ".sln"))
    foreach (var keyword in keywords)
      if (f.Name.Contains(keyword))
        return true;
  return false;
}, tokenSource, ExecuteHandlers.InCurrentTask, true); 

***