Есть ли более быстрый способ найти все файлы в каталоге и всех подкаталогах? - PullRequest
35 голосов
/ 21 января 2010

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

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

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

. , Похоже, что Марк самый быстрый.

Ответы [ 13 ]

45 голосов
/ 21 января 2010

Попробуйте эту версию блока итератора, которая позволяет избежать рекурсии и объектов 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), которые могут быть быстрее (более прямой доступ к файловой системе; меньше массивов)

8 голосов
/ 21 января 2010

Классный вопрос.

Я немного поиграл и, используя блоки итераторов и LINQ, я, кажется, улучшил вашу пересмотренную реализацию примерно на 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);
    }
}
5 голосов
/ 06 октября 2015

Это займет 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 голосов
/ 22 января 2010

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

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

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

Во-первых, мы можем задержать выполнение производительности, возвращая IEnumerable, yield return выполняет это, компилируя в перечислителе конечного автомата внутри анонимного класса, который реализует 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)));
}

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

3 голосов
/ 02 июня 2014

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

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

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

2 голосов
/ 15 мая 2013

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

1 голос
/ 21 декабря 2012

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

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));
}
1 голос
/ 21 января 2010

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

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 быстрее при определении файлов, которые были новыми, удалены и несинхронизированы .

0 голосов
/ 01 февраля 2019

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

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);
        }
    }
}

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

Привет

0 голосов
/ 22 марта 2018

Для поиска файлов и каталогов я хотел бы предложить использовать многопоточную библиотеку .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); 

***
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...