快速获取特定路径下的所有文件和目录

67

我正在创建一个备份应用程序,其中 C# 扫描一个目录。 以前我使用下面的代码来获取目录中所有文件和子文件:

DirectoryInfo di = new DirectoryInfo("A:\\");
var directories= di.GetFiles("*", SearchOption.AllDirectories);

foreach (FileInfo d in directories)
{
       //Add files to a list so that later they can be compared to see if each file
       // needs to be copid or not
}
唯一的问题是有时候文件无法访问,我会得到多个错误。一个我遇到的错误示例如下:error

因此,我创建了一个递归方法,它将扫描当前目录中的所有文件。如果该目录中有子目录,则会再次调用该方法并传递该目录。这种方法的好处是我可以将文件放在try catch块内,如果没有错误则将它们添加到列表中,并将目录添加到另一个列表中,如果出现错误。
try
{
    files = di.GetFiles(searchPattern, SearchOption.TopDirectoryOnly);               
}
catch
{
     //info of this folder was not able to get
     lstFilesErrors.Add(sDir(di));
     return;
}

这个方法很好用,唯一的问题是当我扫描一个大目录时,它需要太长时间。如何加速这个过程?如果需要,这是我的实际方法。

private void startScan(DirectoryInfo di)
{
    //lstFilesErrors is a list of MyFile objects
    // I created that class because I wanted to store more specific information
    // about a file such as its comparePath name and other properties that I need 
    // in order to compare it with another list

    // lstFiles is a list of MyFile objects that store all the files
    // that are contained in path that I want to scan

    FileInfo[] files = null;
    DirectoryInfo[] directories = null;
    string searchPattern = "*.*";

    try
    {
        files = di.GetFiles(searchPattern, SearchOption.TopDirectoryOnly);               
    }
    catch
    {
        //info of this folder was not able to get
        lstFilesErrors.Add(sDir(di));
        return;
    }

    // if there are files in the directory then add those files to the list
    if (files != null)
    {
        foreach (FileInfo f in files)
        {
            lstFiles.Add(sFile(f));
        }
    }


    try
    {
        directories = di.GetDirectories(searchPattern, SearchOption.TopDirectoryOnly);
    }
    catch
    {
        lstFilesErrors.Add(sDir(di));
        return;
    }

    // if that directory has more directories then add them to the list then 
    // execute this function
    if (directories != null)
        foreach (DirectoryInfo d in directories)
        {
            FileInfo[] subFiles = null;
            DirectoryInfo[] subDir = null;

            bool isThereAnError = false;

            try
            {
                subFiles = d.GetFiles();
                subDir = d.GetDirectories();

            }
            catch
            {
                isThereAnError = true;                                                
            }

            if (isThereAnError)
                lstFilesErrors.Add(sDir(d));
            else
            {
                lstFiles.Add(sDir(d));
                startScan(d);
            }


        }

}

如果我尝试使用类似以下方式处理异常,那么问题就出现了:

DirectoryInfo di = new DirectoryInfo("A:\\");
FileInfo[] directories = null;
            try
            {
                directories = di.GetFiles("*", SearchOption.AllDirectories);

            }
            catch (UnauthorizedAccessException e)
            {
                Console.WriteLine("There was an error with UnauthorizedAccessException");
            }
            catch
            {
                Console.WriteLine("There was antother error");
            }

如果发生异常,那么我就得不到任何文件。


3
不要捕获所有异常,只需捕获特定的异常(例如 UnauthorisedAccessException),否则编程错误(例如 NullReferenceException)和系统错误(例如 OutOfMemoryException)将被掩盖为应用程序错误。 - Paul Ruane
这将取决于层次结构中文件的数量。如果有许多文件,这将需要很长时间。那就是它的本质。 - Jim Mischel
顺便提一下,在这里我展示了一个更简单的递归目录列表方法:http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=159。您可以修改该代码以处理异常并将内容存储在您的列表中。 - Jim Mischel
7个回答

47

这种方法更快。当在一个目录下放置许多文件时,你会感受到它的速度优势。我外接的A:\硬盘容量几乎为1个TB,因此在处理大量文件时,这样做会产生很大的差异。

static void Main(string[] args)
{
    DirectoryInfo di = new DirectoryInfo("A:\\");
    FullDirList(di, "*");
    Console.WriteLine("Done");
    Console.Read();
}

static List<FileInfo> files = new List<FileInfo>();  // List that will hold the files and subfiles in path
static List<DirectoryInfo> folders = new List<DirectoryInfo>(); // List that hold direcotries that cannot be accessed
static void FullDirList(DirectoryInfo dir, string searchPattern)
{
    // Console.WriteLine("Directory {0}", dir.FullName);
    // list the files
    try
    {
        foreach (FileInfo f in dir.GetFiles(searchPattern))
        {
            //Console.WriteLine("File {0}", f.FullName);
            files.Add(f);                    
        }
    }
    catch
    {
        Console.WriteLine("Directory {0}  \n could not be accessed!!!!", dir.FullName);                
        return;  // We alredy got an error trying to access dir so dont try to access it again
    }

    // process each directory
    // If I have been able to see the files in the directory I should also be able 
    // to look at its directories so I dont think I should place this in a try catch block
    foreach (DirectoryInfo d in dir.GetDirectories())
    {
        folders.Add(d);
        FullDirList(d, searchPattern);                    
    }

}

顺便说一下,我是通过Jim Mischel的评论得到了这个答案的。


谢谢。这个方法比Directory.GetFileSystemEntries快100倍。 - Takeo Nishioka

19

.NET 4.0中有一个称为Directory.EnumerateFiles的方法,它返回一个IEnumerable<string>类型的结果,并且不会一次性加载所有文件到内存中。只有当您开始遍历返回的集合时,才会返回文件并且可以处理异常


这很好,但如果我遇到异常,查询就会停止,除非我做错了什么。 - Tono Nam

14

.NET文件枚举方法一直被诟病效率低下,问题在于没有一种瞬间枚举大型目录结构的方法。即使这里所接受的答案也存在垃圾回收分配的问题。

我能做到的最好方法是打包进我的库中,并以FindFile (源代码) 类的形式在CSharpTest.Net.IO 命名空间中公开。该类可以枚举文件和文件夹,而不需要不必要的GC分配和字符串编组。

使用方法很简单,RaiseOnAccessDenied属性将跳过用户无权访问的目录和文件:

    private static long SizeOf(string directory)
    {
        var fcounter = new CSharpTest.Net.IO.FindFile(directory, "*", true, true, true);
        fcounter.RaiseOnAccessDenied = false;

        long size = 0, total = 0;
        fcounter.FileFound +=
            (o, e) =>
            {
                if (!e.IsDirectory)
                {
                    Interlocked.Increment(ref total);
                    size += e.Length;
                }
            };

        Stopwatch sw = Stopwatch.StartNew();
        fcounter.Find();
        Console.WriteLine("Enumerated {0:n0} files totaling {1:n0} bytes in {2:n3} seconds.",
                          total, size, sw.Elapsed.TotalSeconds);
        return size;
    }

对于我的本地C:\驱动器,此代码输出以下内容:

枚举出810,046个文件,总计307,707,792,662字节,用时232.876秒。

您的结果可能因驱动器速度而异,但这是我发现的使用托管代码枚举文件的最快方法。事件参数是类型为FindFile.FileFoundEventArgs的变异类,所以请确保不要保留对它的引用,因为它的值会随每次引发的事件而改变。


2
你还忘记调用FIND()方法。我在lambda后面放置了fcounter.Find()方法,它很好地解决了问题。 - Tono Nam
1
哦,呃...哈哈,是的,这个示例有漏洞;) 谢谢你指出来。 - csharptest.net
我需要使用多个搜索模式来查找特定的文件。在@csharptest.net中,我应该如何把它们代替“”?我已经尝试了".txt;.exe"和".txt|*.exe"。 - Pratik Pattanayak
这对于列出大型目录来说非常快速! - quentin-starin
如果有人感兴趣的话,我将其封装成一个不错的API,包括mime类型过滤和一些单元测试:https://bitbucket.org/snippets/qes/ob9z - quentin-starin
显示剩余2条评论

3
也许这对你有帮助。你可以使用 "DirectoryInfo.EnumerateFiles" 方法,并根据需要处理 UnauthorizedAccessException 异常。
using System;
using System.IO;

class Program
{
    static void Main(string[] args)
    {
        DirectoryInfo diTop = new DirectoryInfo(@"d:\");
        try
        {
            foreach (var fi in diTop.EnumerateFiles())
            {
                try
                {
                    // Display each file over 10 MB; 
                    if (fi.Length > 10000000)
                    {
                        Console.WriteLine("{0}\t\t{1}", fi.FullName, fi.Length.ToString("N0"));
                    }
                }
                catch (UnauthorizedAccessException UnAuthTop)
                {
                    Console.WriteLine("{0}", UnAuthTop.Message);
                }
            }

            foreach (var di in diTop.EnumerateDirectories("*"))
            {
                try
                {
                    foreach (var fi in di.EnumerateFiles("*", SearchOption.AllDirectories))
                    {
                        try
                        {
                            // Display each file over 10 MB; 
                            if (fi.Length > 10000000)
                            {
                                Console.WriteLine("{0}\t\t{1}",  fi.FullName, fi.Length.ToString("N0"));
                            }
                        }
                        catch (UnauthorizedAccessException UnAuthFile)
                        {
                            Console.WriteLine("UnAuthFile: {0}", UnAuthFile.Message);
                        }
                    }
                }
                catch (UnauthorizedAccessException UnAuthSubDir)
                {
                    Console.WriteLine("UnAuthSubDir: {0}", UnAuthSubDir.Message);
                }
            }
        }
        catch (DirectoryNotFoundException DirNotFound)
        {
            Console.WriteLine("{0}", DirNotFound.Message);
        }
        catch (UnauthorizedAccessException UnAuthDir)
        {
            Console.WriteLine("UnAuthDir: {0}", UnAuthDir.Message);
        }
        catch (PathTooLongException LongPath)
        {
            Console.WriteLine("{0}", LongPath.Message);
        }
    }
}

3
我知道这已经过时了,但是...另一个选项可能是使用FileSystemWatcher,像这样:
void SomeMethod()
{
    System.IO.FileSystemWatcher m_Watcher = new System.IO.FileSystemWatcher();
    m_Watcher.Path = path;
    m_Watcher.Filter = "*.*";
    m_Watcher.NotifyFilter = m_Watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
    m_Watcher.Created += new FileSystemEventHandler(OnChanged);
    m_Watcher.EnableRaisingEvents = true;
}

private void OnChanged(object sender, FileSystemEventArgs e)
    {
        string path = e.FullPath;

        lock (listLock)
        {
            pathsToUpload.Add(path);
        }
    }

这将允许您使用极轻量级的进程监视目录中的文件更改,然后存储更改的文件名称,以便在适当的时间备份它们。

3

(从我的其他答案中复制了这一段)

在搜索目录中的所有文件时显示进度

快速枚举文件

当然,你已经知道,有很多方法可以进行枚举……但没有一种是瞬间完成的。你可以尝试使用文件系统的USN日志来进行扫描。看看CodePlex上的这个项目:VB.NET中的MFT Scanner……它在不到15秒的时间内找到了我IDE SATA(而不是SSD)驱动器中的所有文件,并找到了311000个文件。

您将需要按路径过滤文件,以便只返回要查找的路径中的文件。但这是工作中最容易的部分!


这似乎需要管理员权限,否则传递 new DriveInfo("c") 将导致 ACCESS_DENIED 异常。它也仅限于启用了日志的 NTFS 分区。除此之外,这是一个很好的解决方案,因为它绝对比使用普通 API 快得多。不确定为什么核心框架没有利用它,或者为什么文件系统没有提供任何访问级别都可以访问的只读版本。 - Kraang Prime
@SamuelJackson 当使用日志来枚举MFT项时,所有更改都将列出,我是指,即使是其他用户、管理员或系统本身所做的更改。一切都有!这就是为什么需要的访问级别是备份操作员...允许读取文件系统中的任何内容,但不能执行,也不能写入不属于自己的文件。 - Miguel Angelo

2
你可以使用此方法获取所有目录和子目录。然后简单地循环处理文件。
string[] folders = System.IO.Directory.GetDirectories(@"C:\My Sample Path\","*", System.IO.SearchOption.AllDirectories);

foreach(string f in folders)
{
   //call some function to get all files in folder
}

6
看起来你没有理解问题。 - mafu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接