有没有比这更快的方法来查找目录和所有子目录中的所有文件?

Eri*_*tas 36 .net c# directory file-io

我正在编写一个程序,需要在目录及其所有子目录中搜索具有特定扩展名的文件.这将在本地和网络驱动器上使用,因此性能有点问题.

这是我现在使用的递归方法:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我可以将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;
}
Run Code Online (Sandbox Code Playgroud)

更新2

好吧,所以我在本地和远程文件夹上运行了一些测试,这两个文件夹都有很多文件(~1200).以下是我运行测试的方法.结果如下.

  • GetFileListA():上面更新中的非递归解决方案.我认为这相当于Jay的解决方案.
  • GetFileListB():来自原始问题的递归方法
  • GetFileListC():获取具有静态Directory.GetDirectories()方法的所有目录.然后使用静态Directory.GetFiles()方法获取所有文件路径.填充并返回List
  • GetFileListD():Marc Gravell使用队列的解决方案并返回IEnumberable.我使用生成的IEnumerable填充了List
    • 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
Run Code Online (Sandbox Code Playgroud)

..看起来Marc是最快的.

Mar*_*ell 45

试试这个避免递归和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]);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

另请注意,4.0内置了迭代器块版本(EnumerateFiles,EnumerateFileSystemEntries)可能更快(更直接访问文件系统;更少的数组)

  • 我认为`yield`会延迟或​​推迟执行,以便稍后执行,例如,当您使用ToList()或打印出所有内容时...仅在您寻找第一个结果或前面的某些内容时有用,因此您可以取消执行的其余部分。 (2认同)

Jus*_*ell 11

我最近(2020 年)发现了这篇文章,因为需要对慢速连接上的文件和目录进行计数,这是我能想到的最快的实现。.NET 枚举方法(GetFiles()、GetDirectories())执行大量底层工作,相比之下,这些工作大大减慢了速度。

\n\n

此解决方案不返回 FileInfo 对象,但可以进行修改以执行此操作\xe2\x80\x94,或者可能仅返回自定义 FileInfo 对象所需的相关数据。

\n\n

该解决方案利用 Win32 API 和 .NET 的 Parallel.ForEach() 来利用线程池来最大限度地提高性能。

\n\n

P/调用:

\n\n
/// <summary>\n/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilew\n/// </summary>\n[DllImport("kernel32.dll", SetLastError = true)]\npublic static extern IntPtr FindFirstFile(\n    string lpFileName,\n    ref WIN32_FIND_DATA lpFindFileData\n    );\n\n/// <summary>\n/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew\n/// </summary>\n[DllImport("kernel32.dll", SetLastError = true)]\npublic static extern bool FindNextFile(\n    IntPtr hFindFile,\n    ref WIN32_FIND_DATA lpFindFileData\n    );\n\n/// <summary>\n/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose\n/// </summary>\n[DllImport("kernel32.dll", SetLastError = true)]\npublic static extern bool FindClose(\n    IntPtr hFindFile\n    );\n
Run Code Online (Sandbox Code Playgroud)\n\n

方法:

\n\n
public static Tuple<long, long> CountFilesDirectories(\n    string path,\n    CancellationToken token\n    )\n{\n    if (String.IsNullOrWhiteSpace(path))\n        throw new ArgumentNullException("path", "The provided path is NULL or empty.");\n\n    // If the provided path doesn\'t end in a backslash, append one.\n    if (path.Last() != \'\\\\\')\n        path += \'\\\\\';\n\n    IntPtr hFile = IntPtr.Zero;\n    Win32.Kernel32.WIN32_FIND_DATA fd = new Win32.Kernel32.WIN32_FIND_DATA();\n\n    long files = 0;\n    long dirs = 0;\n\n    try\n    {\n        hFile = Win32.Kernel32.FindFirstFile(\n            path + "*", // Discover all files/folders by ending a directory with "*", e.g. "X:\\*".\n            ref fd\n            );\n\n        // If we encounter an error, or there are no files/directories, we return no entries.\n        if (hFile.ToInt64() == -1)\n            return Tuple.Create<long, long>(0, 0);\n\n        //\n        // Find (and count) each file/directory, then iterate through each directory in parallel to maximize performance.\n        //\n\n        List<string> directories = new List<string>();\n\n        do\n        {\n            // If a directory (and not a Reparse Point), and the name is not "." or ".." which exist as concepts in the file system,\n            // count the directory and add it to a list so we can iterate over it in parallel later on to maximize performance.\n            if ((fd.dwFileAttributes & FileAttributes.Directory) != 0 &&\n                (fd.dwFileAttributes & FileAttributes.ReparsePoint) == 0 &&\n                fd.cFileName != "." && fd.cFileName != "..")\n            {\n                directories.Add(System.IO.Path.Combine(path, fd.cFileName));\n                dirs++;\n            }\n            // Otherwise, if this is a file ("archive"), increment the file count.\n            else if ((fd.dwFileAttributes & FileAttributes.Archive) != 0)\n            {\n                files++;\n            }\n        }\n        while (Win32.Kernel32.FindNextFile(hFile, ref fd));\n\n        // Iterate over each discovered directory in parallel to maximize file/directory counting performance,\n        // calling itself recursively to traverse each directory completely.\n        Parallel.ForEach(\n            directories,\n            new ParallelOptions()\n            {\n                CancellationToken = token\n            },\n            directory =>\n            {\n                var count = CountFilesDirectories(\n                    directory,\n                    token\n                    );\n\n                lock (directories)\n                {\n                    files += count.Item1;\n                    dirs += count.Item2;\n                }\n            });\n    }\n    catch (Exception)\n    {\n        // Handle as desired.\n    }\n    finally\n    {\n        if (hFile.ToInt64() != 0)\n            Win32.Kernel32.FindClose(hFile);\n    }\n\n    return Tuple.Create<long, long>(files, dirs);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

在我的本地系统上,GetFiles()/GetDirectories() 的性能可以接近此值,但在较慢的连接(VPN 等)中,我发现这要快得多\xe2\x80\x9445 分钟 vs. 90 秒访问包含约 40k 文件、大小约 40 GB 的远程目录。

\n\n

这也可以相当容易地修改为包括其他数据,例如所有计算的文件的总文件大小,或者从最远的分支开始快速递归并删除空目录。

\n


Bra*_*ham 8

很酷的问题.

我玩了一点,通过利用迭代器块和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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Pau*_*hde 5

如何提高代码性能的简短答案是:你不能.

您体验的实际性能是磁盘或网络的实际延迟,因此无论您将其翻转,您都必须检查并遍历每个文件项并检索目录和文件列表.(这当然不包括硬件或驱动程序修改,以减少或改善磁盘延迟,但很多人已经付出了很多钱来解决这些问题,所以我们暂时忽略它的一面)

鉴于原始约束,已经发布了几个或多或少优雅地包装迭代过程的解决方案(但是,因为我假设我正在从单个硬盘驱动器中读取,并行性将无助于更快地横向目录树,并且甚至可能会增加那个时间,因为你现在有两个或更多的线程争夺驱动器的不同部分的数据,因为它试图寻找和第四个)减少创建的对象的数量等.但是如果我们评估函数将如何最终开发人员使用了一些我们可以提出的优化和概括.

首先,我们可以通过返回IEnumerable来延迟性能的执行,yield return通过在实现IEnumerable的匿名类中编译状态机枚举器并在方法执行时返回来完成此操作.LINQ中的大多数方法都是为了延迟执行而编写的,直到执行迭代,因此在迭代IEnumerable之前,不会执行select或SelectMany中的代码.只有在以后需要获取数据的子集时才会感觉到延迟执行的最终结果,例如,如果您只需要前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)));
}
Run Code Online (Sandbox Code Playgroud)

另外,到目前为止,任何人都没有提到的其他内容是文件权限和安全性.目前,没有检查,处理或权限请求,如果代码遇到无法迭代访问的目录,代码将抛出文件权限异常.


Ken*_*max 5

这需要30秒才能获得符合过滤条件的200万个文件名。之所以这么快是因为我只执行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.
}
Run Code Online (Sandbox Code Playgroud)