我正在编写一个程序,需要在目录及其所有子目录中搜索具有特定扩展名的文件。这将在本地和网络驱动器上使用,因此性能有点问题。
这是我现在使用的递归方法:
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)。以下是我运行测试的方法。结果如下。
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
。 。 。看起来像Marc是最快的。
答案 0 :(得分: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]);
}
}
}
另请注意,4.0内置迭代器块版本(EnumerateFiles
,EnumerateFileSystemEntries
)可能更快(更直接访问文件系统;更少数组)
答案 1 :(得分: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);
}
}
答案 2 :(得分: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)));
}
另外,到目前为止,任何人都没有提到的其他内容是文件权限和安全性。目前,没有检查,处理或权限请求,如果代码遇到无法迭代访问的目录,代码将抛出文件权限异常。
答案 3 :(得分: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.
}
答案 4 :(得分:3)
BCL方法可以随意使用。如果保持100%管理,我相信你能做的最好的事情就是在检查访问权限的同时调用GetDirectories / Folders(或者可能不检查权限,并且当第一个需要花费太长时间时让另一个线程准备就绪 - 这表明它是关于抛出UnauthorizedAccess异常 - 使用VB或今天未发布的c#的异常过滤器可以避免这种情况。
如果你想比GetDirectories更快,你必须调用win32(findsomethingEx等),它提供特定的标志,允许在遍历MFT结构时忽略可能不必要的IO。此外,如果驱动器是网络共享,通过类似的方法可以有很大的加速,但这次也避免了过多的网络往返。
现在,如果你有管理员并使用ntfs并且真的很快就要经历数百万个文件,那么通过它们的绝对最快的方式(假设在磁盘延迟杀死的情况下旋转生锈)是使用mft和日志在组合中,基本上将索引服务替换为针对您的特定需求的索引服务。如果您只需要查找文件名而不是大小(或者大小,但是您必须缓存它们并使用日志来注意更改),这种方法可以实现在理想情况下实际上即时搜索数千万个文件和文件夹。可能有一两个付费软件对此感到困扰。在C#中有MFT(DiscUtils)和期刊阅读(谷歌)的样本。我只有大约500万个文件,只使用NTFSSearch就足够了,因为搜索它们大约需要10-20秒。随着期刊阅读的增加,该数额将下降至<3秒。
答案 5 :(得分:2)
DirectoryInfo似乎提供了比您需要的更多信息,尝试管道dir命令并从中解析信息。
答案 6 :(得分:2)
我最近(2020年)发现此帖子是因为需要对慢速连接中的文件和目录进行计数,这是我能想到的最快的实现。 .NET枚举方法(GetFiles(),GetDirectories())执行了很多幕后工作,相比之下它们大大降低了速度。
此解决方案不返回FileInfo对象,但可以对其进行修改,或者可能仅返回自定义FileInfo对象所需的相关数据。
该解决方案利用Win32 API和.NET的Parallel.ForEach()来利用线程池来最大化性能。
P /调用:
/// <summary>
/// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilew
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr FindFirstFile(
string lpFileName,
ref WIN32_FIND_DATA lpFindFileData
);
/// <summary>
/// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FindNextFile(
IntPtr hFindFile,
ref WIN32_FIND_DATA lpFindFileData
);
/// <summary>
/// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FindClose(
IntPtr hFindFile
);
方法:
public static Tuple<long, long> CountFilesDirectories(
string path,
CancellationToken token
)
{
if (String.IsNullOrWhiteSpace(path))
throw new ArgumentNullException("path", "The provided path is NULL or empty.");
// If the provided path doesn't end in a backslash, append one.
if (path.Last() != '\\')
path += '\\';
IntPtr hFile = IntPtr.Zero;
Win32.Kernel32.WIN32_FIND_DATA fd = new Win32.Kernel32.WIN32_FIND_DATA();
long files = 0;
long dirs = 0;
try
{
hFile = Win32.Kernel32.FindFirstFile(
path + "*", // Discover all files/folders by ending a directory with "*", e.g. "X:\*".
ref fd
);
// If we encounter an error, or there are no files/directories, we return no entries.
if (hFile.ToInt64() == -1)
return Tuple.Create<long, long>(0, 0);
//
// Find (and count) each file/directory, then iterate through each directory in parallel to maximize performance.
//
List<string> directories = new List<string>();
do
{
// If a directory (and not a Reparse Point), and the name is not "." or ".." which exist as concepts in the file system,
// count the directory and add it to a list so we can iterate over it in parallel later on to maximize performance.
if ((fd.dwFileAttributes & FileAttributes.Directory) != 0 &&
(fd.dwFileAttributes & FileAttributes.ReparsePoint) == 0 &&
fd.cFileName != "." && fd.cFileName != "..")
{
directories.Add(System.IO.Path.Combine(path, fd.cFileName));
dirs++;
}
// Otherwise, if this is a file ("archive"), increment the file count.
else if ((fd.dwFileAttributes & FileAttributes.Archive) != 0)
{
files++;
}
}
while (Win32.Kernel32.FindNextFile(hFile, ref fd));
// Iterate over each discovered directory in parallel to maximize file/directory counting performance,
// calling itself recursively to traverse each directory completely.
Parallel.ForEach(
directories,
new ParallelOptions()
{
CancellationToken = token
},
directory =>
{
var count = CountFilesDirectories(
directory,
token
);
lock (directories)
{
files += count.Item1;
dirs += count.Item2;
}
});
}
catch (Exception)
{
// Handle as desired.
}
finally
{
if (hFile.ToInt64() != 0)
Win32.Kernel32.FindClose(hFile);
}
return Tuple.Create<long, long>(files, dirs);
}
在我的本地系统上,GetFiles()/ GetDirectories()的性能可以接近此水平,但是在速度较慢的连接(VPN等)上,我发现它的速度要快得多,访问时间从45分钟缩短到90秒约40k文件的远程目录,约40 GB。
这也可以很容易地修改为包括其他数据,例如所有已计数文件的总文件大小,或者从最远的分支开始快速递归遍历并删除空目录。
答案 7 :(得分:1)
考虑将更新的方法拆分为两个迭代器:
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;
}
此外,除了特定于网络的方案之外,如果您能够在该服务器上安装可以从客户端计算机调用的小型服务,那么您将更接近“本地文件夹”结果,因为搜索可以在服务器上执行,只需将结果返回给您。这将是您在网络文件夹方案中最大的速度提升,但在您的情况下可能无法使用。我一直在使用包含此选项的文件同步程序 - 一旦我在我的服务器上安装了该服务,程序就会更快地 WAY 识别新的,删除的和不属于的文件同步。
答案 8 :(得分:1)
尝试并行编程:
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));
}
答案 9 :(得分:0)
我倾向于返回IEnumerable&lt;&gt;在这种情况下 - 取决于你如何消耗结果,它可能是一个改进,加上你减少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
对象分离到troll目录。您不希望为每个目录创建一个新线程,但您可以在顶层创建它们(首先通过GetFileList()
),因此如果您在C:\
驱动器上执行,每个目录有12个目录,这些目录将由不同的线程搜索,然后通过子目录进行递归。您将有一个线程通过C:\Windows
,而另一个线程通过C:\Program Files
。关于这将如何影响性能有很多变量 - 你必须测试它才能看到。
答案 10 :(得分:0)
您可以使用并行foreach(.Net 4.0),也可以尝试使用Poor Man's Parallel.ForEach Iterator for .Net3.5。这可以加快你的搜索速度。
答案 11 :(得分:0)
这太糟糕了,在Windows平台上文件搜索工作很糟糕的原因是因为MS犯了一个错误,他们似乎不愿意纠正。你应该可以使用 SearchOption.AllDirectories 我们都会得到我们想要的速度。但是你不能这样做,因为GetDirectories需要回调,以便你可以决定如何处理你无法访问的目录。 MS忘了或没想过要在自己的电脑上测试课程。
所以,我们都留下了无意义的递归循环。
在C#/ Managed C ++中你很少有选择,这些也是MS的选择,因为他们的编码器也没有找到解决方法。
主要的是显示项目,例如TreeViews和FileViews,只搜索并显示用户可以看到的内容。控件上有很多助手,包括触发器,可以告诉您何时需要填写一些数据。
在树中,从折叠模式开始,搜索一个目录,当用户在树中打开它时,这比等待整个树填充要快得多。 同样在FileViews中,我倾向于10%的规则,如果用户滚动,有多少项目适合显示区域,另外10%准备就绪,它反应灵敏。
MS执行预搜索和目录监视。一个目录,文件的小数据库,这意味着你OnOpen你的树等有一个很好的快速起点,它在刷新时有点落下。
但是混合这两个想法,从数据库中获取您的目录和文件,但是当树节点被扩展(只是那个树节点)并在树中选择不同的目录时进行刷新搜索。
但更好的解决方案是将文件搜索系统添加为服务。 MS已经有了这个,但据我所知,我们无法访问它,我怀疑这是因为它无法访问目录&#39;错误。 就像MS一样,如果你有一个在管理员级别运行的服务,你需要小心你不是为了一点额外的速度而放弃你的安全。
答案 12 :(得分:0)
我有同样的问题。这是我的尝试,它比递归调用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);
}
}
}
您可以修改代码以轻松搜索特定文件或目录。
致谢
答案 13 :(得分:0)
我需要从我的C:分区中获取所有文件,因此我结合了Marc和Jaider的答案,并获得了无递归和并行编程的功能,在30秒内处理了大约370k个文件。也许这会帮助某人:
void DirSearch(string path)
{
ConcurrentQueue<string> pendingQueue = new ConcurrentQueue<string>();
pendingQueue.Enqueue(path);
ConcurrentBag<string> filesNames = new ConcurrentBag<string>();
while(pendingQueue.Count > 0)
{
try
{
pendingQueue.TryDequeue(out path);
var files = Directory.GetFiles(path);
Parallel.ForEach(files, x => filesNames.Add(x));
var directories = Directory.GetDirectories(path);
Parallel.ForEach(directories, (x) => pendingQueue.Enqueue(x));
}
catch (Exception)
{
continue;
}
}
}
答案 14 :(得分:0)
在.net核心中,您可以执行以下操作。它可以以良好的性能递归搜索所有子目录,而无需访问即可忽略路径。 我还尝试了
中找到的其他方法https://www.codeproject.com/Articles/1383832/System-IO-Directory-Alternative-using-WinAPI
public static IEnumerable<string> ListFiles(string baseDir)
{
EnumerationOptions opt = new EnumerationOptions();
opt.RecurseSubdirectories = true;
opt.ReturnSpecialDirectories = false;
//opt.AttributesToSkip = FileAttributes.Hidden | FileAttributes.System;
opt.AttributesToSkip = 0;
opt.IgnoreInaccessible = true;
var tmp = Directory.EnumerateFileSystemEntries(baseDir, "*", opt);
return tmp;
}
答案 15 :(得分:-1)
对于文件和目录搜索目的,我想提供使用具有广泛搜索机会的多线程.NET库。 有关库的所有信息,您可以在GitHub上找到:https://github.com/VladPVS/FastSearchLibrary
如果您想下载它,可以在此处执行:https://github.com/VladPVS/FastSearchLibrary/releases
工作得非常快。亲自检查一下!
如果您有任何疑问,请询问他们。
这是一个如何使用它的示范性示例:
class Searcher
{
private static object locker = new object();
private FileSearcher searcher;
List<FileInfo> files;
public Searcher()
{
files = new List<FileInfo>(); // create list that will contain search result
}
public void Startsearch()
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
// create tokenSource to get stop search process possibility
searcher = new FileSearcher(@"C:\", (f) =>
{
return Regex.IsMatch(f.Name, @".*[Dd]ragon.*.jpg$");
}, tokenSource); // give tokenSource in constructor
searcher.FilesFound += (sender, arg) => // subscribe on FilesFound event
{
lock (locker) // using a lock is obligatorily
{
arg.Files.ForEach((f) =>
{
files.Add(f); // add the next part of the received files to the results list
Console.WriteLine($"File location: {f.FullName}, \nCreation.Time: {f.CreationTime}");
});
if (files.Count >= 10) // one can choose any stopping condition
searcher.StopSearch();
}
};
searcher.SearchCompleted += (sender, arg) => // subscribe on SearchCompleted event
{
if (arg.IsCanceled) // check whether StopSearch() called
Console.WriteLine("Search stopped.");
else
Console.WriteLine("Search completed.");
Console.WriteLine($"Quantity of files: {files.Count}"); // show amount of finding files
};
searcher.StartSearchAsync();
// start search process as an asynchronous operation that doesn't block the called thread
}
}
这是另一个例子:
***
List<string> folders = new List<string>
{
@"C:\Users\Public",
@"C:\Windows\System32",
@"D:\Program Files",
@"D:\Program Files (x86)"
}; // list of search directories
List<string> keywords = new List<string> { "word1", "word2", "word3" }; // list of search keywords
FileSearcherMultiple multipleSearcher = new FileSearcherMultiple(folders, (f) =>
{
if (f.CreationTime >= new DateTime(2015, 3, 15) &&
(f.Extension == ".cs" || f.Extension == ".sln"))
foreach (var keyword in keywords)
if (f.Name.Contains(keyword))
return true;
return false;
}, tokenSource, ExecuteHandlers.InCurrentTask, true);
***