我正在用.NET编写目录扫描程序。
对于每个文件/目录,我需要以下信息。
class Info {
public bool IsDirectory;
public string Path;
public DateTime ModifiedDate;
public DateTime CreatedDate;
}
我具有此功能:
static List<Info> RecursiveMovieFolderScan(string path){
var info = new List<Info>();
var dirInfo = new DirectoryInfo(path);
foreach (var dir in dirInfo.GetDirectories()) {
info.Add(new Info() {
IsDirectory = true,
CreatedDate = dir.CreationTimeUtc,
ModifiedDate = dir.LastWriteTimeUtc,
Path = dir.FullName
});
info.AddRange(RecursiveMovieFolderScan(dir.FullName));
}
foreach (var file in dirInfo.GetFiles()) {
info.Add(new Info()
{
IsDirectory = false,
CreatedDate = file.CreationTimeUtc,
ModifiedDate = file.LastWriteTimeUtc,
Path = file.FullName
});
}
return info;
}
事实证明,此实现非常慢。有什么办法可以加快速度吗?我正在考虑使用FindFirstFileW进行手动编码,但是如果有一种内置的方法可以更快地避免这种情况,我想避免这种情况。
此实现需要稍作调整,速度提高了5-10倍。
static List<Info> RecursiveScan2(string directory) {
IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
WIN32_FIND_DATAW findData;
IntPtr findHandle = INVALID_HANDLE_VALUE;
var info = new List<Info>();
try {
findHandle = FindFirstFileW(directory + @"\*", out findData);
if (findHandle != INVALID_HANDLE_VALUE) {
do {
if (findData.cFileName == "." || findData.cFileName == "..") continue;
string fullpath = directory + (directory.EndsWith("\\") ? "" : "\\") + findData.cFileName;
bool isDir = false;
if ((findData.dwFileAttributes & FileAttributes.Directory) != 0) {
isDir = true;
info.AddRange(RecursiveScan2(fullpath));
}
info.Add(new Info()
{
CreatedDate = findData.ftCreationTime.ToDateTime(),
ModifiedDate = findData.ftLastWriteTime.ToDateTime(),
IsDirectory = isDir,
Path = fullpath
});
}
while (FindNextFile(findHandle, out findData));
}
} finally {
if (findHandle != INVALID_HANDLE_VALUE) FindClose(findHandle);
}
return info;
}
扩展方法:
public static class FILETIMEExtensions {
public static DateTime ToDateTime(this System.Runtime.InteropServices.ComTypes.FILETIME filetime ) {
long highBits = filetime.dwHighDateTime;
highBits = highBits << 32;
return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime);
}
}
interop defs:
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr FindFirstFileW(string lpFileName, out WIN32_FIND_DATAW lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATAW lpFindFileData);
[DllImport("kernel32.dll")]
public static extern bool FindClose(IntPtr hFindFile);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WIN32_FIND_DATAW {
public FileAttributes dwFileAttributes;
internal System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public int nFileSizeHigh;
public int nFileSizeLow;
public int dwReserved0;
public int dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
。NET文件枚举方法历史悠久,使用缓慢。问题在于没有枚举大型目录结构的即时方法。即使这里接受的答案也与GC分配有关。
我所能做的最好的事情总结在我的库中,并在FileFile中显示为source(CSharpTest.Net.IO namespace)类。此类可以枚举文件和文件夹,而无需不必要的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:\驱动器,输出以下内容:
在232.876秒内枚举810,046个文件,总计307,707,792,662字节。
您的行驶里程可能因驱动器速度而异,但这是我发现的枚举托管代码中文件的最快方法。事件参数是FindFile.FileFoundEventArgs类型的变异类,因此请确保不要保留对它的引用,因为它的值会随所引发的每个事件而改变。
您可能还注意到,公开的DateTime仅以UTC表示。原因是到本地时间的转换是半昂贵的。您可能会考虑使用UTC时间来提高性能,而不是将其转换为本地时间。
取决于您要花费多少时间来删除该功能,直接调用Win32 API函数可能值得您花些时间,因为现有的API会进行大量额外的处理来检查您可能不感兴趣的内容in。
[如果您尚未这样做,并且假设您不打算为Mono项目做贡献,我强烈建议您下载Reflector,并查看Microsoft如何实现您当前使用的API调用。这将使您对需要打电话的内容和可以忽略的地方有个想法。
例如,您可能选择创建一个yield
目录名称的迭代器,而不是创建返回列表的函数,这样一来,您就不会最终遍历同一名称列表两次或三遍各种级别的代码。
我只是碰到了这个。本机版本的不错实现。
此版本虽然仍比使用FindFirst
和FindNext
的版本慢,但比原始.NET版本要快很多。
static List<Info> RecursiveMovieFolderScan(string path)
{
var info = new List<Info>();
var dirInfo = new DirectoryInfo(path);
foreach (var entry in dirInfo.GetFileSystemInfos())
{
bool isDir = (entry.Attributes & FileAttributes.Directory) != 0;
if (isDir)
{
info.AddRange(RecursiveMovieFolderScan(entry.FullName));
}
info.Add(new Info()
{
IsDirectory = isDir,
CreatedDate = entry.CreationTimeUtc,
ModifiedDate = entry.LastWriteTimeUtc,
Path = entry.FullName
});
}
return info;
}
它应该产生与您的本机版本相同的输出。我的测试表明,此版本所花费的时间是使用FindFirst
和FindNext
的版本的1.7倍。在未连接调试器的情况下,以发布模式运行的时间。
奇怪的是,在我的测试中,将GetFileSystemInfos
更改为EnumerateFileSystemInfos
会使运行时间增加大约5%。我宁愿期望它以相同的速度运行,甚至可能更快,因为它不必创建FileSystemInfo
对象的数组。
下面的代码仍然短一些,因为它使框架负责递归。但这比上面的版本慢15%到20%。
static List<Info> RecursiveScan3(string path)
{
var info = new List<Info>();
var dirInfo = new DirectoryInfo(path);
foreach (var entry in dirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
{
info.Add(new Info()
{
IsDirectory = (entry.Attributes & FileAttributes.Directory) != 0,
CreatedDate = entry.CreationTimeUtc,
ModifiedDate = entry.LastWriteTimeUtc,
Path = entry.FullName
});
}
return info;
}
同样,如果将其更改为GetFileSystemInfos
,则速度会稍快(但仅会略微)。
出于我的目的,上面的第一个解决方案足够快。本机版本运行约1.6秒。使用DirectoryInfo
的版本运行时间约为2.9秒。我想如果我经常进行这些扫描,我会改变主意。
非常浅,有371个每个目录中平均10个文件。一些目录包含其他子目录
这只是一条评论,但您的人数确实很高。我使用与您使用的基本相同的递归方法运行以下操作,尽管创建了字符串输出,但我的时间却要短得多。
public void RecurseTest(DirectoryInfo dirInfo,
StringBuilder sb,
int depth)
{
_dirCounter++;
if (depth > _maxDepth)
_maxDepth = depth;
var array = dirInfo.GetFileSystemInfos();
foreach (var item in array)
{
sb.Append(item.FullName);
if (item is DirectoryInfo)
{
sb.Append(" (D)");
sb.AppendLine();
RecurseTest(item as DirectoryInfo, sb, depth+1);
}
else
{ _fileCounter++; }
sb.AppendLine();
}
}
我在许多不同的目录上运行了上面的代码。在我的机器上,由于运行时或文件系统的缓存,第二次扫描目录树的调用通常更快。请注意,该系统没有什么特别之处,只是一个使用了1年的开发工作站。
//缓存的呼叫dirs = 150,files = 420,最大深度= 5花费时间= 53毫秒//缓存的呼叫Dirs = 1117,文件= 9076,最大深度= 11花费时间= 433毫秒//首次通话Dirs = 1052,文件= 5903,最大深度= 12花费时间= 11921毫秒//首次通话Dirs = 793,文件= 10748,最大深度= 10花费的时间= 5433毫秒(第二次运行363毫秒)
考虑到我没有得到创建和修改的日期,该代码已被修改为在接下来的时间也输出此日期。
//现在获取最后的更新和创建时间。dirs = 150,files = 420,最大深度= 5花费的时间= 103毫秒(第二次运行93毫秒)Dirs = 1117,文件= 9076,最大深度= 11花费的时间= 992毫秒(第二次运行984毫秒)Dirs = 793,文件= 10748,最大深度= 10花费的时间= 1382毫秒(第二次运行735毫秒)Dirs = 1052,文件= 5903,最大深度= 12花费的时间= 936毫秒(第二次运行595毫秒)
注意:System.Diagnostics.StopWatch类用于计时。
我将使用或基于此多线程库:http://www.codeproject.com/KB/files/FileFind.aspx
尝试一下(即先进行初始化,然后再使用列表和directoryInfo对象):
static List<Info> RecursiveMovieFolderScan1() {
var info = new List<Info>();
var dirInfo = new DirectoryInfo(path);
RecursiveMovieFolderScan(dirInfo, info);
return info;
}
static List<Info> RecursiveMovieFolderScan(DirectoryInfo dirInfo, List<Info> info){
foreach (var dir in dirInfo.GetDirectories()) {
info.Add(new Info() {
IsDirectory = true,
CreatedDate = dir.CreationTimeUtc,
ModifiedDate = dir.LastWriteTimeUtc,
Path = dir.FullName
});
RecursiveMovieFolderScan(dir, info);
}
foreach (var file in dirInfo.GetFiles()) {
info.Add(new Info()
{
IsDirectory = false,
CreatedDate = file.CreationTimeUtc,
ModifiedDate = file.LastWriteTimeUtc,
Path = file.FullName
});
}
return info;
}
最近我有相同的问题,我认为将所有文件夹和文件输出到文本文件,然后使用streamreader读取文本文件,执行多线程要处理的事情也是很好的。
cmd.exe /u /c dir "M:\" /s /b >"c:\flist1.txt"
[更新]嗨,白鲸,你是正确的。由于回读输出文本文件的开销,我的方法比较慢。实际上,我花了一些时间测试200万个文件的最佳答案和cmd.exe。
The top answer: 2010100 files, time: 53023
cmd.exe method: 2010100 files, cmd time: 64907, scan output file time: 19832.
最佳答案方法(53023)比cmd.exe(64907)快,更不用说如何改善读取输出文本文件了。尽管我的初衷是提供一个不错的答案,但仍然感到抱歉,哈。
我最近(2020年)发现了这篇文章,因为需要对慢速连接中的文件和目录进行计数,这是我能想到的最快的实现。 .NET枚举方法(GetFiles(),GetDirectories())执行了很多幕后工作,相比之下,它们大大降低了它们的速度。
此解决方案利用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。
也可以很容易地将其修改为包括其他数据,例如所有已计数文件的总文件大小,或者从最远的分支开始快速递归并删除空目录。