2010-01-21 2 views
30

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

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

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, теперь все, что мне действительно волнует, это пути к файлам. У меня будет существующий список файлов, который я хочу сравнить с новым списком файлов, чтобы увидеть, какие файлы были добавлены или удалены. Есть ли более быстрый способ создания этого списка путей к файлу? Есть ли что-нибудь, что я могу сделать, чтобы оптимизировать этот поиск файлов вокруг запросов к файлам на общем сетевом диске?


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

Update 2

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

  • GetFileListA(): Non-рекурсивное решение в вышеприведенном обновлении. Я думаю, что это эквивалентно решению Джея.
  • 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 похоже, что Марк самый быстрый.

+0

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

+0

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

+0

Быстрый тест, AllDirectories быстрее, но не сильно. @Eric, я думаю, что он только читает метаданные файла, так что я могу ошибаться в этом. –

ответ

36

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

+0

Этот материал для возврата/возврата является для меня новым. Если я сохраню результаты вашего метода в переменной IEnumerable и запустил несколько итераций ForEach над переменной, будет ли каждый из них ForEach отбрасывать некоторый кеш всех значений или будет ли каждый повторный запуск GetEileList() для ForeEach? –

+0

@Eric - он перезапустит его, но в этом сценарии буферирует его с помощью .ToList() '(LINQ) или нового' List (GetFileList (...)) ' –

+1

Я думаю, что' yield' задержка или отложить выполнение для последующего использования, например, когда вы используете ToList() или распечатываете все ... Это полезно, только если вы ищете первый результат или что-то впереди, чтобы вы могли отклонить остальные выполнение. – Jaider

0

Я был бы склонен возвращать 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. Есть много переменных относительно того, как это повлияет на производительность - вам придется протестировать его, чтобы увидеть.

0

Вы можете использовать параллельный foreach (.Net 4.0) или можете попробовать Poor Man's Parallel.ForEach Iterator для .Net3.5. Это может ускорить ваш поиск.

1

Рассмотрим разделив обновленный метод в двух итераторов:

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

Как вы управляете исключением несанкционированного доступа, зная, что вы не можете использовать возврат доход внутри try catch? –

6

Прохладный вопрос.

Я играл немного, и за счет использования итераторов блоков и 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); 
    } 
} 
+0

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

+0

А как насчет простого заполнения списка ? Список myList = новый Список (GetFileLIst (некоторые вещи)); –

+0

Джей прав, конечно, вам нужно пересечь коллекцию, чтобы увидеть удар производительности. Я преднамеренно опустил, как я эхом отзывался на консоль, чтобы рассчитать время, предполагая, что вы, возможно, делали что-то конкретное или вычисляли сроки более точно. Это то, что я сделал (вызов .Count вызовет обход): var start = DateTime.Now; IEnumerable fileList = GetFileList ("*. Txt", @ "C: \ Temp"); var end = DateTime.Now; Console.WriteLine (String.Format («Найденные файлы: {0}», fileList.Count())); Console.WriteLine (String.Format («Время: {0}», end.Subtract (начало) .TotalMilliseconds)); –

5

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

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

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

Во-первых, мы можем отсрочить выполнение производительности, вернув 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))); 
} 

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

2

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

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

Используйте 'ConcurrentBag ' или '' ConcurrentDictionary вместо вручную 'lock'ing и т.д. –

1

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

+0

дают пример ... –

+0

вы можете попробовать Process.Start (cmd.exe, "dir c: \\ /s>allfiles.txt"); а затем parse allfiles.txt –

1

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

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

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

0

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

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

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

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

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

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

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

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

3

Это займет 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. 
} 
+0

«Длина» не существует в текущем контексте. Что он должен содержать? –

+0

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

+0

Я получил его, спасибо –

Смежные вопросы