[英]Is there a faster way than this to find all the files in a directory and all sub directories?
我正在編寫一個程序,需要在一個目錄及其所有子目錄中搜索具有特定擴展名的文件。 這將在本地和網絡驅動器上使用,因此性能有點問題。
這是我現在使用的遞歸方法:
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
. . .so 看起來 Marc 是最快的。
試試這個避免遞歸和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
)可能更快(更直接地訪問文件系統;更少的數組)
很酷的問題。
我玩了一會兒,通過利用迭代器塊和 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);
}
}
如何提高該代碼的性能的簡短回答是:你不能。
真正影響您體驗的性能是磁盤或網絡的實際延遲,因此無論您以哪種方式翻轉它,您都必須檢查和迭代每個文件項並檢索目錄和文件列表。 (這當然不包括硬件或驅動程序修改以減少或改善磁盤延遲,但很多人已經支付了很多錢來解決這些問題,所以我們現在將忽略這一方面)
鑒於最初的限制,已經發布了幾個解決方案,或多或少優雅地包裝了迭代過程(但是,因為我假設我正在從單個硬盤驅動器讀取,並行性將無助於更快地橫向遍歷目錄樹,並且甚至可能會增加該時間,因為您現在有兩個或更多線程在驅動器的不同部分爭奪數據,因為它試圖回溯和第四)減少創建的對象數量等。但是,如果我們評估函數將如何由最終開發人員使用,我們可以提出一些優化和概括。
首先,我們可以通過返回 IEnumerable 來延遲性能的執行,yield return 通過在實現 IEnumerable 的匿名類內部的狀態機枚舉器中進行編譯來實現這一點,並在方法執行時返回。 編寫 LINQ 中的大多數方法是為了延遲執行直到執行迭代,因此在迭代 IEnumerable 之前,不會執行 select 或 SelectMany 中的代碼。 延遲執行的最終結果只有在您稍后需要獲取數據的子集時才會感覺到,例如,如果您只需要前 10 個結果,則延遲執行返回數千個結果的查詢不會遍歷整個 1000 個結果,直到您需要 10 個以上。
現在,考慮到您想要進行子文件夾搜索,我還可以推斷,如果您可以指定該深度,它可能會很有用,如果我這樣做,它也會概括我的問題,但也需要遞歸解決方案。 然后,當有人因為我們增加了文件數量並決定添加另一層分類而決定現在需要深入搜索兩個目錄時,您只需稍作修改即可,而無需重新編寫函數。
鑒於所有這些,這是我提出的解決方案,它提供了比上述其他一些解決方案更通用的解決方案:
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)));
}
順便提一下,到目前為止,任何人都沒有提到的其他內容是文件權限和安全性。 目前,沒有檢查、處理或權限請求,如果代碼遇到它無權迭代的目錄,它將拋出文件權限異常。
這需要 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.
}
可以這么說,BCL 方法是可移植的。 如果保持 100% 管理,我相信你能做的最好的事情就是在檢查訪問權限時調用 GetDirectories/Folders(或者可能不檢查權限並在第一個線程花費太長時間時准備好另一個線程 - 這表明它是關於拋出 UnauthorizedAccess 異常 - 這可能可以通過使用 VB 或截至今天未發布的 c# 的異常過濾器來避免)。
如果你想要比 GetDirectories 更快,你必須調用 win32(findsomethingEx 等),它提供了特定的標志,允許在遍歷 MFT 結構時忽略可能不必要的 IO。 此外,如果驅動器是網絡共享,通過類似的方法可以有很大的加速,但這次也避免了過多的網絡往返。
現在,如果您有管理員並使用 ntfs 並且非常急於處理數百萬個文件,那么絕對最快的方式來處理它們(假設在磁盤延遲殺死的地方旋轉銹蝕)是結合使用 mft 和日記,本質上是用針對您的特定需求的索引服務替換索引服務。 如果您只需要查找文件名而不是大小(或大小也是如此,但您必須緩存它們並使用日志來注意更改),如果實施得當,這種方法可以允許幾乎即時搜索數千萬個文件和文件夾。 可能有一兩個付費軟件對此感到困擾。 在 C# 中有 MFT (DiscUtils) 和日志閱讀 (google) 的示例。 我只有大約 500 萬個文件,僅使用 NTFSSearch 就足夠了,因為搜索它們大約需要 10-20 秒。 添加日記閱讀后,該數量將下降到 <3 秒。
DirectoryInfo 似乎提供了比您需要的更多的信息,請嘗試使用 dir 命令並從中解析信息。
我最近(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。
這也可以很容易地修改以包含其他數據,例如所有計算的文件的總文件大小,或者快速遞歸並刪除空目錄,從最遠的分支開始。
嘗試並行編程:
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));
}
考慮將更新的方法拆分為兩個迭代器:
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的標識是新的文件的速度上安裝的服務,刪除和出不同步。
我需要從我的 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;
}
}
}
在這種情況下,我傾向於返回一個 IEnumerable<> —— 取決於你如何使用結果,這可能是一個改進,另外你將參數占用空間減少 1/3 並避免不斷傳遞該 List。
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
對象以瀏覽目錄。 您不希望每個目錄都有一個新線程,但您可以在頂層創建它們(首先通過GetFileList()
),因此如果您在C:\\
驅動器上執行,有 12 個目錄,那么這些目錄中的每一個都將由不同的線程搜索,然后將通過子目錄遞歸。 您將有一個線程通過C:\\Windows
而另一個線程通過C:\\Program Files
。 關於這將如何影響性能有很多變數——您必須對其進行測試才能看到。
您可以使用並行 foreach (.Net 4.0) 或者您可以嘗試使用Poor Man's Parallel.ForEach Iterator for .Net3.5。 這可以加快您的搜索速度。
這太可怕了,Windows 平台上的文件搜索工作糟糕的原因是因為 MS 犯了一個錯誤,他們似乎不願意糾正。 您應該能夠使用 SearchOption.AllDirectories 我們都將獲得我們想要的速度。 但是您不能這樣做,因為 GetDirectories 需要回調,以便您可以決定如何處理您無權訪問的目錄。 MS 忘記或沒想到在自己的計算機上測試課程。
所以,我們都剩下無意義的遞歸循環。
在 C#/Managed C++ 中,您幾乎沒有選擇,這些也是 MS 采取的選項,因為他們的編碼人員也沒有想出如何解決它。
主要是顯示項目,例如 TreeViews 和 FileViews,只搜索和顯示用戶可以看到的內容。 控件上有很多幫助程序,包括觸發器,它們會告訴您何時需要填寫某些數據。
在樹中,從折疊模式開始,在用戶在樹中打開該目錄時搜索該目錄,這比等待整個樹被填滿要快得多。 在 FileViews 中也是如此,我傾向於 10% 的規則,顯示區域中有多少項目適合,如果用戶滾動,則另外 10% 准備好,這是很好的響應。
MS 進行預搜索和目錄監視。 一個目錄、文件的小數據庫,這意味着你 OnOpen 你的樹等有一個很好的快速起點,它在刷新時有點下降。
但是混合這兩種想法,從數據庫中獲取您的目錄和文件,但是在擴展樹節點(僅該樹節點)並在樹中選擇不同的目錄時進行刷新搜索。
但更好的解決方案是將文件搜索系統添加為服務。 MS 已經有了這個,但據我所知,我們無法訪問它,我懷疑這是因為它不受“訪問目錄失敗”錯誤的影響。 就像 MS 一樣,如果您有一項在管理員級別運行的服務,您需要小心不要僅僅為了一點額外的速度而放棄您的安全性。
我有同樣的問題。 這是我的嘗試,它比遞歸調用 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);
}
}
}
您可以修改代碼以輕松搜索特定文件或目錄。
問候
在 .net core 中,您可以執行以下操作。 它可以遞歸搜索所有子目錄,性能良好,忽略不訪問的路徑。 我還嘗試了其他方法
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;
}
出於文件和目錄搜索的目的,我想提供具有廣泛搜索機會的多線程 .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);
***
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.