wake-up-neo.com

Gibt es einen schnelleren Weg, um alle Dateien in einem Verzeichnis und allen Unterverzeichnissen zu finden?

Ich schreibe ein Programm, das ein Verzeichnis und alle seine Unterverzeichnisse nach Dateien durchsuchen muss, die eine bestimmte Erweiterung haben. Dies wird sowohl auf einem lokalen als auch auf einem Netzlaufwerk verwendet, daher ist die Leistung ein Problem.

Hier ist die rekursive Methode, die ich jetzt verwende:

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

Ich könnte die Suchoption auf AllDirectories setzen und keine rekursive Methode verwenden. In der Zukunft möchte ich jedoch Code einfügen, um den Benutzer darüber zu informieren, welcher Ordner gerade geprüft wird.

Während ich jetzt eine Liste von FileInfo-Objekten erstelle, sind mir nur die Pfade zu den Dateien wichtig. Ich habe eine Liste mit Dateien, die ich mit der neuen Liste von Dateien vergleichen möchte, um zu sehen, welche Dateien hinzugefügt oder gelöscht wurden. Gibt es einen schnelleren Weg, um diese Liste der Dateipfade zu erstellen? Gibt es irgendetwas, das ich tun kann, um diese Dateisuche zu optimieren und die Dateien auf einem freigegebenen Netzlaufwerk abzufragen?


Update 1

Ich habe versucht, eine nicht rekursive Methode zu erstellen, die dasselbe tut, indem ich zunächst alle Unterverzeichnisse aufspürt und dann jedes Verzeichnis nach Dateien iterativ durchsucht. Hier ist die Methode:

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

Okay, ich habe einige Tests auf einem lokalen und einem Remote-Ordner durchgeführt, die beide eine Menge Dateien enthalten (~ 1200). Hier sind die Methoden, mit denen ich die Tests durchgeführt habe. Die Ergebnisse sind unten.

  • GetFileListA () : Nicht rekursive Lösung in der Aktualisierung oben. Ich denke, dass es Jay's Lösung entspricht.
  • GetFileListB () : Rekursive Methode aus der ursprünglichen Frage
  • GetFileListC () : Ruft alle Verzeichnisse mit der statischen Directory.GetDirectories () -Methode ab. Ruft dann alle Dateipfade mit der statischen Directory.GetFiles () -Methode ab. Füllt eine Liste aus und gibt sie zurück
  • GetFileListD () : Marc Gravells Lösung verwendet eine Warteschlange und gibt IEnumberable zurück. Ich habe eine Liste mit dem resultierenden IEnumerable .__ gefüllt.
    • DirectoryInfo.GetFiles : Es wurde keine zusätzliche Methode erstellt. Verzeichnisinfo aus dem Stammordnerpfad instanziiert. GetFiles mit SearchOption.AllDirectories aufgerufen 
  • Directory.GetFiles : Keine zusätzliche Methode erstellt. Ruft die statische GetFiles-Methode des Verzeichnisses mithilfe von SearchOption.AllDirectories auf
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 sieht es aus, als wäre Marc der Schnellste.

33
Eric Anastas

Versuchen Sie diese Version des Iteratorblocks, um Rekursion und die Info-Objekte zu vermeiden:

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

Beachten Sie auch, dass 4.0 über Iterator-Blockversionen ( EnumerateFiles , EnumerateFileSystemEntries ) verfügt, die möglicherweise schneller sind (direkter Zugriff auf das Dateisystem; weniger Arrays).

42
Marc Gravell

Coole Frage.

Ich habe ein bisschen herumgespielt und durch die Nutzung von Iteratorblöcken und LINQ habe ich Ihre überarbeitete Implementierung um etwa 40% verbessert

Ich wäre daran interessiert, dass Sie es mit Ihren Timing-Methoden und in Ihrem Netzwerk testen, um zu sehen, wie der Unterschied aussieht.

Hier ist das Fleisch davon 

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

Die kurze Antwort darauf, wie Sie die Leistung dieses Codes verbessern können, lautet: Sie können nicht.

Die tatsächliche Leistung, auf die Sie stoßen, ist die tatsächliche Latenzzeit der Festplatte oder des Netzwerks. Unabhängig davon, auf welche Weise Sie sie umdrehen, müssen Sie jedes einzelne Dateielement überprüfen und durchlaufen sowie Verzeichnis- und Dateilisten abrufen. (Das schließt natürlich Hardware- oder Treibermodifikationen aus, um die Festplattenlatenz zu reduzieren oder zu verbessern, aber viele Leute haben bereits eine Menge Geld dafür bezahlt, um diese Probleme zu lösen. Daher ignorieren wir diese Seite vorerst.) 

In Anbetracht der ursprünglichen Einschränkungen gibt es bereits mehrere Lösungen, die den Iterationsprozess mehr oder weniger elegant umhüllen (Da ich jedoch davon ausgehe, dass ich von einer einzigen Festplatte aus lese, wird Parallelität NICHT dazu beitragen, einen Verzeichnisbaum schneller zu durchqueren, und Diese Zeit kann sich sogar verlängern, da Sie jetzt über zwei oder mehr Threads verfügen, die um Daten in verschiedenen Bereichen des Laufwerks kämpfen, wenn versucht wird, zurück zu suchen, und viertens die Anzahl der erstellten Objekte verringern Vom Endentwickler verbraucht, gibt es einige Optimierungen und Verallgemeinerungen, die wir entwickeln können.

Erstens können wir die Ausführung der Performance durch die Rückgabe eines IEnumerable-Objekts verzögern. Die Renditeausgabe bewirkt dies durch Kompilieren in einem Zustandsmaschinen-Enumerator innerhalb einer anonymen Klasse, die IEnumerable implementiert und bei Ausführung der Methode zurückgegeben wird. Die meisten Methoden in LINQ werden geschrieben, um die Ausführung zu verzögern, bis die Iteration ausgeführt wird. Daher wird der Code in select oder SelectMany erst ausgeführt, wenn IEnumerable durchlaufen wird. Das Endergebnis einer verzögerten Ausführung wird nur dann wahrgenommen, wenn Sie zu einem späteren Zeitpunkt eine Teilmenge der Daten benötigen, wenn Sie beispielsweise nur die ersten 10 Ergebnisse benötigen. Die Ausführung einer Abfrage, die mehrere tausend Ergebnisse zurückgibt, wird nicht verzögert Durchlaufen Sie die gesamten 1000 Ergebnisse, bis Sie mehr als zehn Ergebnisse benötigen.

In Anbetracht der Tatsache, dass Sie eine Unterordner-Suche durchführen möchten, kann ich auch folgern, dass es nützlich sein kann, wenn Sie diese Tiefe angeben können, und wenn ich dies mache, verallgemeinert sie auch mein Problem, erfordert aber eine rekursive Lösung. Wenn sich später jemand entscheidet, dass er two-Verzeichnisse gründlich durchsuchen muss, weil wir die Anzahl der Dateien erhöht und beschlossen haben, eine weitere Kategorisierungsebene hinzuzufügen, können Sie einfach eine geringfügige Änderung vornehmen, anstatt die Funktion neu zu schreiben.

In Anbetracht dessen ist die Lösung, die ich mir ausgedacht habe, eine allgemeinere Lösung als einige der oben genannten:

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

Nebenbei bemerkt sind Dateiberechtigungen und Sicherheit etwas, was bisher noch niemand erwähnt hat. Derzeit gibt es keine Überprüfungs-, Handhabungs- oder Berechtigungsanforderungen, und der Code löst Dateiberechtigungsausnahmen aus, wenn er auf ein Verzeichnis stößt, für das er keinen Zugriff hat.

5
Paul Rohde

Dies dauert 30 Sekunden, um 2 Millionen Dateinamen zu erhalten, die den Filter erfüllen. Der Grund dafür ist so schnell, weil ich nur eine Aufzählung durchführe. Jede weitere Aufzählung beeinflusst die Leistung. Die variable Länge ist für Ihre Interpretation offen und nicht unbedingt auf das Aufzählungsbeispiel bezogen. 

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.
}
4
Kentonbmax

Die BCL-Methoden sind sozusagen portabel. Wenn Sie zu 100% verwaltet werden, glaube ich, ist das Beste, was Sie tun können, GetDirectories/Folders aufzurufen, während Sie die Zugriffsrechte prüfen (oder möglicherweise nicht die Rechte überprüfen und einen anderen Thread bereithalten, wenn der erste etwas zu lange dauert - ein Zeichen, dass es sich um etwas handelt UnauthorizedAccess-Ausnahme auslösen - dies kann bei Ausnahmefiltern mit VB oder ab heute nicht veröffentlichtem c #) vermieden werden. 

Wenn Sie schneller als GetDirectories möchten, müssen Sie win32 (findsomethingEx usw.) aufrufen, das bestimmte Flags enthält, die möglicherweise das Ignorieren von möglicherweise unnötigem IO beim Durchlaufen der MFT-Strukturen zulassen. Wenn es sich bei dem Laufwerk um eine Netzwerkfreigabe handelt, kann durch einen ähnlichen Ansatz eine große Beschleunigung erreicht werden, diesmal jedoch auch übermäßige Netzwerkrundfahrten.

Wenn Sie nun über Admin verfügen und ntfs verwenden und sich in einer Eile mit Millionen von Dateien befinden, die Sie durchlaufen müssen, ist der absolut schnellste Weg, um sie zu durchlaufen (vorausgesetzt, Sie drehen Rust, wo die Datenträgerlatenz abbricht), ist die Verwendung von MFT und Journaling in Kombination. Im Wesentlichen wird der Indexierungsdienst durch einen Dienst ersetzt, der auf Ihre speziellen Anforderungen zugeschnitten ist. Wenn Sie nur nach Dateinamen und nicht nach Größen (oder auch nach Größen suchen müssen), müssen Sie sie jedoch zwischenspeichern und die Änderungen mit Hilfe des Journals durchführen. Wenn Sie dies ideal implementieren, könnte dieser Ansatz eine praktisch sofortige Suche nach Millionen von Dateien und Ordnern ermöglichen. Möglicherweise haben sich ein oder zwei Paywares damit beschäftigt. Es gibt Beispiele für MFT (DiscUtils) und für das Lesen von Journalen (google) in C #. Ich habe nur etwa 5 Millionen Dateien und die Verwendung von NTFSSearch ist für diese Menge gut genug, da es etwa 10 bis 20 Sekunden dauert, um sie zu durchsuchen. Durch das Hinzufügen von Journalen würde dieser Betrag auf <3 Sekunden sinken.

1

DirectoryInfo scheint viel mehr Informationen zu geben, als Sie benötigen. Versuchen Sie, einen Befehl dir zu übermitteln und die Informationen daraus zu analysieren.

1
user2385360

Parallele Programmierung versuchen:

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

Sie können die aktualisierte Methode in zwei Iteratoren aufteilen:

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

Wenn Sie in der Lage waren, einen kleinen Dienst auf diesem Server zu installieren, den Sie von einem Client-Computer aus aufrufen könnten, würden Sie neben dem netzwerkspezifischen Szenario auch den Ergebnissen Ihres lokalen Ordners viel näher kommen, da die Suche dies könnte Führen Sie on the server aus und senden Sie die Ergebnisse an Sie zurück. Dies wäre der größte Geschwindigkeitsschub im Netzwerkordner-Szenario, ist jedoch in Ihrer Situation möglicherweise nicht verfügbar. Ich habe ein Dateisynchronisierungsprogramm verwendet, das diese Option enthält. Nachdem ich den Dienst auf meinem Server installiert hatte, wurde das ProgrammWAYschneller, um die Dateien zu identifizieren, die neu, gelöscht und nicht synchron waren .

1
Jay

Ich hatte das gleiche Problem. Hier ist mein Versuch, der viel schneller ist als der Aufruf von Directory.EnumerateFiles, Directory.EnumerateDirectories oder Directory.EnumerateFileSystemEntries rekursiv:

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

Sie können den Code ändern, um nach bestimmten Dateien oder Verzeichnissen zu suchen.

Grüße

0
Scordo

In diesem Fall würde ich gerne ein IEnumerable <> zurückgeben. Abhängig davon, wie Sie die Ergebnisse verwenden, kann dies eine Verbesserung darstellen. Außerdem reduzieren Sie Ihren Footprint für Parameter um 1/3 und vermeiden, dass diese Liste ständig umgangen wird.

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

Eine andere Idee wäre, BackgroundWorker-Objekte auszulagern, um Verzeichnisse zu durchsuchen. Sie möchten keinen neuen Thread für jedes Verzeichnis, aber Sie können sie auf der obersten Ebene erstellen (erster Durchlauf durch GetFileList()). Wenn Sie also auf Ihrem C:\-Laufwerk mit 12 Verzeichnissen arbeiten, wird jedes dieser Verzeichnisse von einem Verzeichnis durchsucht anderer Thread, der dann durch Unterverzeichnisse rekursiert. Ein Thread geht durch C:\Windows, während ein anderer durch C:\Program Files geht. Es gibt eine Reihe von Variablen, wie sich dies auf die Leistung auswirkt - man müsste es testen, um zu sehen.

0
Jay

Sie können parallel foreach (.Net 4.0) verwenden oder Paralleler.ForEach Iterator von Poor Man für .Net3.5 versuchen. Das kann Ihre Suche beschleunigen.

0
ata

Es ist schrecklich und der Grund, warum die Dateisuche auf Windows-Plattformen schrecklich ist, ist, dass MS einen Fehler gemacht hat und nicht gewillt zu sein scheint. Sie sollten in der Lage sein, SearchOption.AllDirectories Zu verwenden. Und wir würden alle die Geschwindigkeit erhalten, die wir wollen. Dies ist jedoch nicht möglich, da GetDirectories einen Rückruf benötigt, damit Sie entscheiden können, was Sie mit den Verzeichnissen tun möchten, auf die Sie keinen Zugriff haben. MS hat vergessen oder nicht gedacht, die Klasse auf ihren eigenen Computern zu testen.

Wir sind also alle mit den rekursiven Nonsens-Loops beschäftigt. 

Innerhalb von C #/Managed C++ gibt es sehr wenige Möglichkeiten. Dies sind auch die Optionen, die von MS gewählt werden, da ihre Programmierer nicht herausgefunden haben, wie sie es umgehen können.

Die Hauptsache ist bei Anzeigeelementen, wie z. B. TreeViews und FileViews, nur Suchen und Anzeigen, was die Benutzer sehen können. Es gibt viele Helfer an den Steuerelementen, einschließlich der Trigger, die Ihnen sagen, wann Sie Daten eingeben müssen.

Durchsuchen Sie in einem Baum, der im ausgeblendeten Modus beginnt, dieses eine Verzeichnis, und wenn der Benutzer es in der Baumstruktur öffnet, ist dies viel schneller als das Warten, bis eine ganze Baumstruktur gefüllt ist .. _. Das gleiche gilt für FileViews 10% Regel, wie viele Elemente in den Anzeigebereich passen, haben weitere 10% bereit, wenn der Benutzer einen Bildlauf durchführt, es ist gut ansprechbar.

MS führt die Vorsuche und Verzeichnisüberwachung durch. Eine kleine Datenbank mit Verzeichnissen, Dateien, das bedeutet, dass Sie OnOpen Ihre Bäume usw. einen guten schnellen Ausgangspunkt haben, es fällt ein wenig bei der Aktualisierung aus.

Mischen Sie die beiden Ideen, nehmen Sie Ihre Verzeichnisse und Dateien aus der Datenbank, aber führen Sie eine Aktualisierungssuche durch, da ein Baumknoten erweitert wird (nur dieser Baumknoten) und ein anderes Verzeichnis in der Baumstruktur ausgewählt wird.

Besser ist es jedoch, Ihr Dateisuchsystem als Dienst hinzuzufügen. MS hat dies bereits, aber soweit ich weiß, dass wir keinen Zugriff darauf haben, vermute ich, dass es gegen "fehlgeschlagene Zugriffe auf Verzeichnisse" gefeit ist. Wenn Sie einen Dienst auf Admin-Ebene ausführen, müssen Sie, genau wie bei der MS-Version, darauf achten, dass Sie Ihre Sicherheit nicht aus Gründen der zusätzlichen Geschwindigkeit verraten.

0
Bob