web-dev-qa-db-ja.com

ディレクトリとすべてのサブディレクトリ内のすべてのファイルを検索するこれよりも速い方法はありますか?

特定の拡張子を持つファイルのディレクトリとそのすべてのサブディレクトリを検索する必要があるプログラムを書いています。これは、ローカルドライブとネットワークドライブの両方で使用されるため、パフォーマンスが少し問題になります。

私が今使用している再帰的な方法は次のとおりです。

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():上記の更新の非再帰的ソリューション。 Jayのソリューションと同等だと思います。
  • GetFileListB():元の質問からの再帰的メソッド
  • GetFileListC():静的なDirectory.GetDirectories()メソッドですべてのディレクトリを取得します。次に、静的なDirectory.GetFiles()メソッドを使用して、すべてのファイルパスを取得します。リストに入力して返します
  • GetFileListD():キューを使用し、IEnumberableを返すMarc Gravellのソリューション。結果のIEnumerable をリストに追加しました
    • DirectoryInfo.GetFiles:追加のメソッドは作成されません。ルートフォルダーパスからDirectoryInfoをインスタンス化しました。 SearchOption.AllDirectoriesを使用してGetFilesを呼び出しました
  • Directory.GetFiles:追加のメソッドは作成されません。 SearchOption.AllDirectoriesを使用して、ディレクトリの静的GetFilesメソッドを呼び出しました
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はMarcが最速のようです。

36
Eric Anastas

再帰と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にはイテレータブロックバージョンが組み込まれていることに注意してください( EnumerateFilesEnumerateFileSystemEntries )。 ;少ない配列)

45
Marc Gravell

いい質問です。

少し遊んで、イテレータブロックと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);
    }
}
8
Brad Cunningham

これは、フィルターを満たす200万のファイル名を取得するのに30秒かかります。これが非常に高速である理由は、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
Kentonbmax

そのコードのパフォーマンスを改善する方法の簡単な答えは次のとおりです。

実際にパフォーマンスに影響を与えるのは、ディスクまたはネットワークの実際のレイテンシです。そのため、どちらの方法でフリップしても、各ファイル項目を確認および反復し、ディレクトリとファイルのリストを取得する必要があります。 (もちろん、ディスクレイテンシを削減または改善するためのハードウェアまたはドライバーの変更は除外されますが、これらの問題を解決するために多くの人がすでに多くのお金を支払われているため、ここではその側面を無視します)

元の制約を考えると、反復プロセスを多少エレガントにラップするいくつかのソリューションがすでに投稿されています(ただし、私は単一のハードドライブから読んでいると仮定しているため、並列処理はディレクトリツリーをより迅速に横断するのに役立ちませんドライブの異なる部分のデータを求めて2つ以上のスレッドが戻ってシークしようとするので、その時間はさらに長くなる可能性があります4番目)作成されるオブジェクトの数を減らすなど最終開発者が消費する最適化と一般化がいくつかあります。

最初に、IEnumerableを返すことでパフォーマンスの実行を遅らせることができます。IEnumerableを実装し、メソッドの実行時に返される匿名クラス内のステートマシン列挙子でコンパイルすることにより、yield returnはこれを達成します。 LINQのほとんどのメソッドは、反復が実行されるまで実行を遅らせるように記述されているため、IEnumerableが反復処理されるまでselectまたはSelectManyのコードは実行されません。遅延実行の最終結果は、後からデータのサブセットを取得する必要がある場合にのみ感じられます。たとえば、最初の10件の結果のみが必要な場合、数千の結果を返すクエリの実行は遅延しません10個以上必要になるまで、1000件の結果全体を繰り返し処理します。

さて、サブフォルダー検索を実行したい場合、その深さを指定できると便利かもしれませんが、これを行うと問題も一般化されますが、再帰的な解決策も必要になります。その後、後で誰かが検索する必要があると判断した場合ファイル数を増やしてカテゴリの別のレイヤーを追加することを決定したため、2つのディレクトリの深さ書き直す代わりにわずかな変更を加えることができます関数。

以上のことを踏まえて、上記のいくつかのソリューションよりも一般的なソリューションを提供する、私が思いついたソリューションを以下に示します。

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

ちなみに、これまで誰も言及していないことは、ファイルのアクセス許可とセキュリティです。現在、チェック、処理、またはアクセス許可の要求はありません。コードは、反復するアクセス権がないディレクトリに遭遇すると、ファイルのアクセス許可例外をスローします。

5
Paul Rohde

BCLメソッドは、いわば移植可能です。 100%の管理を維持する場合は、アクセス権を確認しながらGetDirectories/Foldersを呼び出す(または、最初のスレッドが少し長すぎる場合に権限を確認せずに別のスレッドを準備する)ことをお勧めしますUnauthorizedAccess例外をスローする-これは、VBまたは今日未リリースのc#)を使用する例外フィルターで回避できる場合があります。

GetDirectoriesよりも高速にしたい場合は、Win32(findsomethingExなど)を呼び出す必要があります。これは、MFT構造の走査中に不要な可能性のあるIOを無視できる特定のフラグを提供します。ドライブがネットワーク共有の場合、同様のアプローチで大幅に高速化できますが、今回は過度のネットワークラウンドトリップも回避します。

ここで、管理者がntfsを使用していて、何百万ものファイルを処理するのが本当に急いでいる場合、それらを処理する絶対的な最速の方法です(Rustディスクレイテンシーが強制終了する場合) mftとジャーナリングの両方を組み合わせて使用​​し、本質的にインデックスサービスを特定のニーズに合わせたものに置き換えます。サイズではなくファイル名のみを検索する必要がある場合(またはサイズもありますが、変更を通知するにはジャーナルを使用する必要があります)このアプローチは、理想的に実装されていれば、数千万のファイルとフォルダーの実質的に即時の検索を可能にします。これに悩む1つまたは2つの有料ソフトウェアがあるかもしれません。C#のMFT(DiscUtils)とジャーナル読み取り(google)の両方のサンプルがあります約500万個のファイルしかなく、NTFSSearchを使用するだけで、検索に約10〜20秒かかるので十分です。

3

DirectoryInfoは必要以上に多くの情報を提供しているようです。dirコマンドをパイピングし、そこから情報を解析してみてください。

2
user2385360

並列プログラミングを試してください:

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
Jaider

更新されたメソッドを2つの反復子に分割することを検討してください。

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

また、ネットワーク固有のシナリオに加えて、クライアントマシンから呼び出すことができる小さなサービスをそのサーバーにインストールできた場合、検索が可能になるため、「ローカルフォルダー」の結果にはるかに近くなります。 onサーバーを実行し、結果を返すだけです。これは、ネットワークフォルダーシナリオにおける最大の速度向上になりますが、状況によっては利用できない場合があります。私はこのオプションを含むファイル同期プログラムを使用してきました-サーバーにサービスをインストールすると、プログラムは[〜#〜] way [〜#〜]のファイルの識別が速くなりました新規、削除、および非同期でした。

1
Jay

同じ問題がありました。 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);
        }
    }
}

コードを変更して、特定のファイルまたはディレクトリを簡単に検索できます。

よろしく

0
Scordo

この場合、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個のディレクトリ、これらの各ディレクトリは異なるスレッドによって検索され、サブディレクトリを再帰的に検索します。 1つのスレッドが_C:\Windows_を通過し、別のスレッドが_C:\Program Files_を通過します。これがパフォーマンスにどのように影響するかについては、多くの変数があります。確認するにはテストする必要があります。

0
Jay

並列foreach(.Net 4.0)を使用するか、.Net3.5で Poor Man's Parallel.ForEach Iterator を試すことができます。これにより、検索を高速化できます。

0
ata

それは恐ろしいことであり、Windowsプラットフォームでのファイル検索作業が恐ろしいのは、MSがミスを犯したためです。 SearchOption.AllDirectoriesを使用できる必要があります。必要な速度に戻ります。しかし、GetDirectoriesはコールバックを必要とするため、アクセスできないディレクトリに対して何をすべきかを決定できるため、それはできません。 MSは自分のコンピューターでクラスをテストすることを忘れたか、考えていませんでした。

だから、我々はすべてナンセンスな再帰ループを残している。

C#/ Managed C++内にはごくわずかなオピオンしかありませんが、これらはMSが取るオプションでもあります。これらのコーダーもそれを回避する方法を考え出していないからです。

主なものは、TreeViewsやFileViewsなどの表示アイテムで、ユーザーが見ることができるものだけを検索して表示することです。コントロールには、トリガーを含め、データを入力する必要があるときに通知するヘルパーが豊富にあります。

ツリーでは、折りたたみモードから始めて、ユーザーがツリーでディレクトリを開いたときにそのディレクトリを検索します。これは、ツリー全体がいっぱいになるのを待つよりもはるかに高速です。 FileViewsでも同じように、10%のルール、表示領域に収まるアイテムの数、ユーザーがスクロールすると別の10%の準備が整う傾向があり、反応が良いです。

MSは事前検索とディレクトリ監視を行います。ディレクトリ、ファイルの小さなデータベースは、これはあなたがツリーなどをOnOpenすることを意味します。

ただし、2つのアイデアを混ぜ合わせて、データベースからディレクトリとファイルを取得しますが、ツリーノード(そのツリーノードのみ)が展開され、ツリーで別のディレクトリが選択されると、検索を更新します。

しかし、より良い解決策は、ファイル検索システムをサービスとして追加することです。 MSはすでにこれを持っていますが、私たちがそれにアクセスできないことを知っている限り、それは「ディレクトリへのアクセスの失敗」エラーの影響を受けないと考えています。 MSの場合と同様に、管理レベルでサービスを実行している場合、速度を少しだけ上げるためにセキュリティを提供しないように注意する必要があります。

0
Bob