web-dev-qa-db-fra.com

Existe-t-il un moyen plus rapide de rechercher tous les fichiers d’un répertoire et tous les sous-répertoires?

J'écris un programme qui doit rechercher dans un répertoire et tous ses sous-répertoires les fichiers ayant une certaine extension. Cela va être utilisé à la fois sur un lecteur local et sur un lecteur réseau, les performances sont donc un problème.

Voici la méthode récursive que j'utilise maintenant:

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

Je pouvais définir la propriété SearchOption sur AllDirectories et ne pas utiliser de méthode récursive, mais à l'avenir, je souhaiterais insérer du code pour informer l'utilisateur du dossier en cours d'analyse.

Pendant que je crée une liste d’objets FileInfo, tout ce qui m’intéresse vraiment, c’est le chemin des fichiers. Je vais avoir une liste de fichiers existante, que je veux comparer à la nouvelle liste de fichiers pour voir quels fichiers ont été ajoutés ou supprimés. Existe-t-il un moyen plus rapide de générer cette liste de chemins de fichiers? Y a-t-il quelque chose que je puisse faire pour optimiser cette recherche de fichiers en recherchant les fichiers sur un lecteur réseau partagé?


Mise à jour 1

J'ai essayé de créer une méthode non récursive faisant la même chose en recherchant d'abord tous les sous-répertoires, puis en analysant chaque répertoire de manière itérative. Voici la méthode:

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

Mise à jour 2

Très bien, j'ai donc effectué des tests sur un dossier local et un dossier distant, qui contiennent beaucoup de fichiers (~ 1200). Voici les méthodes sur lesquelles j'ai effectué les tests. Les résultats sont ci-dessous.

  • GetFileListA () : Solution non récursive dans la mise à jour ci-dessus. Je pense que c'est équivalent à la solution de Jay.
  • GetFileListB () : Méthode récursive de la question d'origine
  • GetFileListC () : Obtient tous les répertoires avec la méthode statique Directory.GetDirectories (). Obtient ensuite tous les chemins de fichiers avec la méthode statique Directory.GetFiles (). Remplit et retourne une liste
  • GetFileListD () : La solution de Marc Gravell utilisant une file d'attente et retourne IEnumberable. J'ai rempli une liste avec le résultat IEnumerable
    • DirectoryInfo.GetFiles : Aucune méthode supplémentaire n'a été créée. Instancié un DirectoryInfo à partir du chemin du dossier racine. Appelé GetFiles à l'aide de SearchOption.AllDirectories 
  • Directory.GetFiles : Aucune méthode supplémentaire n'a été créée. Appelé la méthode statique GetFiles de l'annuaire à l'aide de 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 ressemble à Marc est le plus rapide.

33
Eric Anastas

Essayez cette version du bloc itérateur qui évite la récursivité et les objets 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]);
        }
    }
}

Notez également que la version 4.0 intègre des versions de bloc itérateur ( EnumerateFiles , EnumerateFileSystemEntries ) plus rapides (accès plus direct au système de fichiers; moins de tableaux)

42
Marc Gravell

Question cool.

J'ai joué un peu et en tirant parti des blocs d'itérateurs et de LINQ, je semble avoir amélioré votre mise en œuvre révisée d'environ 40%.

Je serais intéressé de vous faire tester en utilisant vos méthodes de synchronisation et sur votre réseau pour voir quelle différence ressemble.

En voici la viande 

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

La réponse courte à la question de savoir comment améliorer les performances de ce code est la suivante: vous ne pouvez pas.

La performance réelle qui vous frappe correspond à la latence réelle du disque ou du réseau. Par conséquent, quelle que soit la façon dont vous le retournez, vous devez vérifier et parcourir chaque élément de fichier, puis extraire les listes de répertoires et de fichiers. (Bien entendu, cela exclut les modifications de matériel ou de pilotes pour réduire ou améliorer le temps de latence des disques, mais beaucoup de personnes sont déjà beaucoup payées pour résoudre ces problèmes, nous allons donc ignorer cet aspect pour le moment) 

Compte tenu des contraintes initiales, plusieurs solutions déjà publiées résument de manière plus ou moins élégante le processus d’itération (toutefois, puisque je suppose que je lis sur un seul disque dur, le parallélisme n’aidera PAS à traverser plus rapidement une arborescence de répertoires, et peut même augmenter ce temps puisque vous avez maintenant deux threads ou plus qui se disputent des données sur différentes parties du lecteur alors qu'il tente de rechercher en arrière et en quatrième lieu), réduit le nombre d'objets créés, etc. Cependant, si nous évaluons le fonctionnement de la fonction Consommé par le développeur final, nous pouvons trouver des optimisations et des généralisations.

Premièrement, nous pouvons retarder l'exécution de la performance en renvoyant un IEnumerable, alors que return return y parvient en compilant un énumérateur de machine à états à l'intérieur d'une classe anonyme qui implémente IEnumerable et est renvoyé lors de l'exécution de la méthode. La plupart des méthodes de LINQ sont écrites pour retarder l'exécution jusqu'à l'itération. Par conséquent, le code d'une sélection ou d'une SelectMany ne sera pas exécuté tant que l'IEnumerable n'aura pas été itéré. Le résultat final d'une exécution différée ne se fait sentir que si vous devez prendre un sous-ensemble de données ultérieurement, par exemple, si vous n'avez besoin que des 10 premiers résultats, retarder l'exécution d'une requête renvoyant plusieurs milliers de résultats ne sera pas nécessaire. parcourez les 1000 résultats jusqu'à obtenir plus de dix résultats.

Maintenant, étant donné que vous souhaitez effectuer une recherche dans un sous-dossier, je peux également en déduire qu'il peut être utile de spécifier cette profondeur. Si je le fais, cela généralise également mon problème, mais nécessite également une solution récursive. Puis, plus tard, lorsque quelqu'un décide qu'il doit maintenant effectuer une recherche deux répertoires en profondeur parce que nous avons augmenté le nombre de fichiers et décidé d'ajouter une nouvelle couche de catégorisation vous pouvez simplement apporter une légère modification au lieu de réécrire la fonction.

À la lumière de tout cela, voici la solution que j'ai proposée qui fournit une solution plus générale que certaines des solutions ci-dessus:

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

Par ailleurs, personne n’a encore mentionné les autorisations et la sécurité des fichiers. Actuellement, il n'y a pas de demande de vérification, de traitement ou d'autorisation, et le code lève des exceptions d'autorisation de fichier s'il rencontre un répertoire auquel il n'a pas accès.

5
Paul Rohde

Cela prend 30 secondes pour obtenir 2 millions de noms de fichiers correspondant au filtre. La raison pour laquelle cela est si rapide est que je n’effectue qu’une énumération. Chaque énumération supplémentaire affecte les performances. La longueur variable est ouverte à votre interprétation et n'est pas nécessairement liée à l'exemple d'énumération. 

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

Les méthodes BCL sont portables pour ainsi dire. Si vous restez à 100% géré, je pense que le mieux que vous puissiez faire est d'appeler GetDirectories/Folders tout en vérifiant les droits d'accès (ou éventuellement de ne pas vérifier les droits et d'avoir un autre thread prêt à partir lorsque le premier prend un peu trop longtemps - un signe qu'il s'agit lancer une exception UnauthorizedAccess - cela pourrait être évité avec des filtres d'exception utilisant VB ou, à compter de ce jour, un c # non publié). 

Si vous voulez plus rapide que GetDirectories, vous devez appeler win32 (findomethingEx etc.) qui fournit des indicateurs spécifiques qui permettent d’ignorer les éléments éventuellement inutiles IO lors du parcours des structures MFT. De plus, si le lecteur est un partage réseau, il peut y avoir une grande accélération avec une approche similaire mais en évitant cette fois aussi des allers-retours réseau excessifs.

Maintenant, si vous avez des utilisateurs admin et utilisez ntfs et que vous êtes vraiment pressé avec des millions de fichiers à traiter, le moyen le plus rapide de les parcourir (en supposant que Rust tourne et que la latence du disque tue) est d'utiliser à la fois mft et la journalisation, remplacer essentiellement le service d'indexation par un service qui répond à vos besoins spécifiques. Si vous devez uniquement rechercher des noms de fichiers et non des tailles (ou des tailles, mais vous devez les mettre en cache et utiliser journal pour noter les modifications), cette approche pourrait permettre une recherche pratiquement instantanée de dizaines de millions de fichiers et de dossiers si elle était implémentée de manière idéale. Il se peut qu’un ou deux systèmes de paiement s’occupent de cela. Il y a des exemples de MFT (DiscUtils) et de lecture de journal (google) en C #. J'ai seulement environ 5 millions de fichiers et juste utiliser NTFSSearch est assez bon pour ce montant car il faut environ 10 à 20 secondes pour les rechercher. Avec la lecture du journal ajoutée, cela passerait à moins de 3 secondes pour ce montant.

1
Anonymous Coward

DirectoryInfo semble donner beaucoup plus d'informations que nécessaire, essayez de canaliser une commande dir et d'analyser les informations de cette dernière.

1
user2385360

Essayez la programmation parallèle:

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

Envisagez de scinder la méthode mise à jour en deux itérateurs:

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

De plus, si vous pouviez installer un petit service sur ce serveur que vous pourriez appeler depuis un ordinateur client, vous vous rapprocheriez beaucoup des résultats de votre "dossier local", car la recherche pourrait exécutez sur le serveur et ne vous renvoyons que les résultats. Ce serait votre plus gros gain de vitesse dans le scénario de dossier réseau, mais pourrait ne pas être disponible dans votre cas. J'utilise un programme de synchronisation de fichiers qui inclut cette option: une fois le service installé sur mon serveur, le programme est devenuMANI&EGRAVE;REplus rapide pour identifier les fichiers nouveaux, supprimés et désynchronisés. .

1
Jay

J'ai eu le même problème. Voici ma tentative qui est beaucoup plus rapide que d'appeler Directory.EnumerateFiles, Directory.EnumerateDirectories ou 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);
        }
    }
}

Vous pouvez modifier le code pour rechercher facilement des fichiers ou des répertoires spécifiques.

Cordialement

0
Scordo

Dans ce cas, je serais enclin à renvoyer un IEnumerable <- selon la manière dont vous consommez les résultats, cela pourrait être une amélioration. De plus, vous réduirez l'encombrement de vos paramètres de 1/3 et éviterez de passer incessamment à cette liste.

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

Une autre idée serait d’extraire des objets BackgroundWorker pour parcourir les répertoires. Vous ne voudriez pas d'un nouveau thread pour chaque répertoire, mais vous pourriez les créer au niveau supérieur (premier passage à travers GetFileList()), donc si vous exécutez sur votre lecteur C:\, avec 12 répertoires, chacun de ces répertoires sera recherché par un thread différent, qui sera ensuite recurse à travers les sous-répertoires. Vous aurez un thread qui passe par C:\Windows tandis qu'un autre passe par C:\Program Files. Il y a beaucoup de variables sur la manière dont cela va affecter les performances - vous devrez le tester pour voir.

0
Jay

Vous pouvez utiliser forfor parallèle (.NET 4.0) ou essayer Poor Man's Parallel.ForEach Iterator pour .Net3.5. Cela peut accélérer votre recherche.

0
ata

C’est horrible, et la raison pour laquelle la recherche de fichiers est horrible sur les plates-formes Windows est que MS a commis une erreur, qu’ils ne semblent pas disposés à remédier à la situation. Vous devriez pouvoir utiliser SearchOption.AllDirectories Et nous obtiendrions tous la vitesse souhaitée. Mais vous ne pouvez pas le faire, car GetDirectories a besoin d’un rappel pour vous permettre de décider quoi faire des répertoires auxquels vous n’avez pas accès. MS a oublié ou n'a pas pensé à tester la classe sur ses propres ordinateurs.

Nous nous retrouvons donc tous avec les boucles récursives insensées. 

Dans C #/C++ managé, vous avez très peu d'opérateurs. Ce sont également les options proposées par MS, car leurs codeurs n'ont pas trouvé comment contourner le problème.

L'essentiel est que les éléments d'affichage, tels que TreeViews et FileViews, permettent uniquement de rechercher et d'afficher ce que les utilisateurs peuvent voir. De nombreux assistants sur les commandes, y compris les déclencheurs, vous indiquent quand vous devez renseigner certaines données.

Dans les arbres, à partir du mode réduit, recherchez ce répertoire au fur et à mesure que l'utilisateur l'ouvre dans l'arborescence, ce qui est beaucoup plus rapide que l'attente de remplissage d'un arbre complet . Idem dans FileViews, je suis plutôt orienté vers La règle de 10%, quel que soit le nombre d'éléments insérés dans la zone d'affichage, est de 10% si l'utilisateur fait défiler l'écran, il est bien réactif.

MS fait la pré-recherche et la surveillance du répertoire. Une petite base de données de répertoires, de fichiers, cela signifie que vous avez un bon point de départ rapide sur OnOpen Your Trees, cela tombe un peu lors de l'actualisation.

Mais combinez les deux idées, prenez vos répertoires et vos fichiers de la base de données, mais effectuez une recherche d'actualisation lorsqu'un nœud d'arborescence est développé (uniquement ce nœud d'arborescence) et qu'un répertoire différent est sélectionné dans l'arborescence.

Mais la meilleure solution consiste à ajouter votre système de recherche de fichiers en tant que service. Les États-Unis l'ont déjà, mais pour autant que je sache, nous n'y avons pas accès. Je suppose que cela est dû au fait qu'il est immunisé contre les erreurs d'accès à l'annuaire. Comme pour MS, si vous avez un service fonctionnant au niveau de l’administrateur, vous devez faire attention à ne pas céder votre sécurité juste pour gagner un peu plus de vitesse.

0
Bob