CodeGym /課程 /C# SELF /取得檔案與目錄資訊

取得檔案與目錄資訊

C# SELF
等級 39 , 課堂 2
開放

1. 介紹

在 Windows(以及其他系統)中,每個檔案和目錄都有一組屬性(metadata):完整路徑、名稱、副檔名、大小、建立/修改/存取時間、屬性等等。想像一下:檔案不只是位元組,還是一張可以用 FileInfoDirectoryInfo 讀取的完整資料表。

屬性 說明 範例
完整路徑 檔案/資料夾的完整名稱
C:\data\myfile.txt
名稱 不含路徑的名稱
myfile.txt
副檔名 .txt, .csv, .jpg 等等
.txt
大小 (Bytes) 檔案大小(位元組)
4096
建立時間 檔案/資料夾被建立的時間
2024-04-16 19:30:10
修改時間 最後一次變更內容的時間
2024-05-01 18:14:02
屬性 例如唯讀、隱藏等
ReadOnly, Hidden
父資料夾 包含該檔案/目錄的資料夾
C:\data

2. 深入檔案屬性操作

時間戳記的詳細分析

屬性 CreationTimeLastWriteTimeLastAccessTime 會回傳 DateTime,它們的行為會依檔案系統與對檔案的操作而異。


var fileInfo = new FileInfo("document.txt");

if (fileInfo.Exists)
{
    Console.WriteLine($"已建立: {fileInfo.CreationTime:yyyy-MM-dd HH:mm:ss}");
    Console.WriteLine($"已修改: {fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
    Console.WriteLine($"最後存取: {fileInfo.LastAccessTime:yyyy-MM-dd HH:mm:ss}");
    
    // 建立與最後修改的差異
    var age = fileInfo.LastWriteTime - fileInfo.CreationTime;
    Console.WriteLine($"檔案變更持續了: {age.TotalDays:F1} 天");
}

有趣的是,複製檔案通常會把建立時間更新為當前時間,但修改時間可能保留原始值。這一點對備份和活動分析很重要。

處理副檔名與檔名

屬性 FullNameNameExtension 看起來很簡單,但有些細節要注意:沒有副檔名、複合副檔名像 .tar.gz、以及以點開頭的隱藏檔。


var files = new[]
{
    new FileInfo("document.txt"),
    new FileInfo("archive.tar.gz"),
    new FileInfo("README"),
    new FileInfo(".gitignore")
};

foreach (var file in files)
{
    Console.WriteLine($"完整名稱: {file.FullName}");
    Console.WriteLine($"名稱: {file.Name}");
    Console.WriteLine($"副檔名: '{file.Extension}'");
    
    // 不含副檔名的名稱
    string nameWithoutExtension = Path.GetFileNameWithoutExtension(file.Name);
    Console.WriteLine($"不含副檔名的名稱: {nameWithoutExtension}");
    Console.WriteLine("---");
}

檔案大小與格式化

屬性 Length 回傳位元組;對使用者來說,用 KB/MB/GB 顯示會更友善。輔助函式範例如下:


static string FormatFileSize(long bytes)
{
    string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
    int counter = 0;
    decimal number = bytes;
    
    while (Math.Round(number / 1024) >= 1)
    {
        number /= 1024;
        counter++;
    }
    
    return $"{number:N1} {suffixes[counter]}";
}

// 使用範例
var file = new FileInfo("bigfile.zip");
if (file.Exists)
{
    Console.WriteLine($"檔案大小: {FormatFileSize(file.Length)}");
}

3. 進階檔案屬性操作

屬性由列舉型別 FileAttributes 表示(位元旗標),因此一個檔案可以同時有多個屬性。用 HasFlag 來檢查會很方便。


var fileInfo = new FileInfo("important.txt");

Console.WriteLine($"檔案屬性: {fileInfo.Attributes}");

// 檢查特定屬性
if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden))
{
    Console.WriteLine("檔案被隱藏了!");
}

if (fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly))
{
    Console.WriteLine("檔案為唯讀!");
}

if (fileInfo.Attributes.HasFlag(FileAttributes.System))
{
    Console.WriteLine("這是系統檔案!");
}

屬性也可以程式化地修改:


// 設為隱藏檔
fileInfo.Attributes |= FileAttributes.Hidden;

// 移除「唯讀」屬性
fileInfo.Attributes &= ~FileAttributes.ReadOnly;

// 一次設定多個屬性
fileInfo.Attributes = FileAttributes.ReadOnly | FileAttributes.Hidden;

4. 進階目錄操作

用樣板搜尋檔案

方法 GetFiles()GetDirectories() 接受樣板字串,能幫助過濾內容。


var dir = new DirectoryInfo(@"C:\Projects");

if (dir.Exists)
{
    // 找到所有文字檔
    var textFiles = dir.GetFiles("*.txt");
    Console.WriteLine($"找到的文字檔數量: {textFiles.Length}");
    
    // 找到所有以 "temp" 開頭的檔案
    var tempFiles = dir.GetFiles("temp*");
    
    // 找到所有圖片檔案
    var imageExtensions = new[] { "*.jpg", "*.png", "*.gif", "*.bmp" };
    var allImages = imageExtensions.SelectMany(ext => dir.GetFiles(ext)).ToArray();
    
    Console.WriteLine($"找到的圖片數量: {allImages.Length}");
}

在子目錄中遞迴搜尋

要遍歷所有子目錄可以使用 SearchOption.AllDirectories


var dir = new DirectoryInfo(@"C:\Development");

// 找出所有子資料夾中的 C# 檔
var csharpFiles = dir.GetFiles("*.cs", SearchOption.AllDirectories);
Console.WriteLine($"總共找到 .cs 檔: {csharpFiles.Length}");

// 顯示前 10 個檔案與其路徑
foreach (var file in csharpFiles.Take(10))
{
    Console.WriteLine($"{file.FullName} ({FormatFileSize(file.Length)})");
}

目錄內容分析

一個彙總分析範例:檔案/資料夾數量、總大小、按副檔名分布,以及前五大檔案。


static void AnalyzeDirectory(DirectoryInfo dir)
{
    if (!dir.Exists)
    {
        Console.WriteLine("目錄不存在!");
        return;
    }
    
    var files = dir.GetFiles();
    var subdirs = dir.GetDirectories();
    
    Console.WriteLine($"分析目錄: {dir.FullName}");
    Console.WriteLine($"檔案數: {files.Length}, 子目錄數: {subdirs.Length}");
    
    if (files.Length == 0)
    {
        Console.WriteLine("找不到檔案。");
        return;
    }
    
    long totalSize = files.Sum(f => f.Length);
    Console.WriteLine($"檔案總大小: {FormatFileSize(totalSize)}");
    
    // 按副檔名分組
    var byExtension = files.GroupBy(f => f.Extension.ToLower())
                           .OrderByDescending(g => g.Sum(f => f.Length));
    
    Console.WriteLine("\n按檔案類型分布:");
    foreach (var group in byExtension)
    {
        string ext = string.IsNullOrEmpty(group.Key) ? "(無副檔名)" : group.Key;
        long groupSize = group.Sum(f => f.Length);
        Console.WriteLine($"  {ext}: {group.Count()} 個檔案, {FormatFileSize(groupSize)}");
    }
    
    // 前五大檔案
    var largestFiles = files.OrderByDescending(f => f.Length).Take(5);
    Console.WriteLine("\n最大的檔案:");
    foreach (var file in largestFiles)
    {
        Console.WriteLine($"  {file.Name}: {FormatFileSize(file.Length)}");
    }
}

5. 優化的目錄大小計算

對於大型資料夾,避免使用 GetFiles() 一次載入所有項目,改用延遲列舉的 EnumerateFiles()/EnumerateDirectories(),並處理例外,同時可顯示進度。


static long GetDirectorySizeAdvanced(DirectoryInfo dir, bool showProgress = false)
{
    long totalSize = 0;
    int fileCount = 0;
    var inaccessibleDirs = new List<string>();
    
    try
    {
        // 對於大型目錄使用 EnumerateFiles(延遲載入)
        foreach (var file in dir.EnumerateFiles())
        {
            try
            {
                totalSize += file.Length;
                fileCount++;
                
                if (showProgress && fileCount % 1000 == 0)
                {
                    Console.WriteLine($"已處理檔案數: {fileCount}, 大小: {FormatFileSize(totalSize)}");
                }
            }
            catch (UnauthorizedAccessException)
            {
                // 檔案無法存取,跳過
            }
            catch (IOException)
            {
                // 讀取檔案時出問題,跳過
            }
        }
        
        // 遞迴處理子目錄
        foreach (var subdir in dir.EnumerateDirectories())
        {
            try
            {
                totalSize += GetDirectorySizeAdvanced(subdir, showProgress);
            }
            catch (UnauthorizedAccessException)
            {
                inaccessibleDirs.Add(subdir.FullName);
            }
        }
    }
    catch (UnauthorizedAccessException)
    {
        inaccessibleDirs.Add(dir.FullName);
    }
    
    if (inaccessibleDirs.Any() && showProgress)
    {
        Console.WriteLine($"無法存取的目錄數: {inaccessibleDirs.Count}");
    }
    
    return totalSize;
}

// 使用範例
var targetDir = new DirectoryInfo(@"C:\Users");
Console.WriteLine("開始計算目錄大小...");
long size = GetDirectorySizeAdvanced(targetDir, showProgress: true);
Console.WriteLine($"總大小: {FormatFileSize(size)}");

6. metadata 的實用應用

按日期搜尋檔案

搜尋在指定期間內被修改的檔案(對清理、分析或稽核很方便)。


static void FindFilesByDate(DirectoryInfo dir, DateTime fromDate, DateTime toDate)
{
    Console.WriteLine($"搜尋檔案:從 {fromDate:yyyy-MM-dd} 到 {toDate:yyyy-MM-dd}");
    
    var matchingFiles = dir.GetFiles("*", SearchOption.AllDirectories)
                           .Where(f => f.LastWriteTime >= fromDate && f.LastWriteTime <= toDate)
                           .OrderByDescending(f => f.LastWriteTime);
    
    Console.WriteLine($"找到檔案數: {matchingFiles.Count()}");
    
    foreach (var file in matchingFiles.Take(20))
    {
        Console.WriteLine($"{file.LastWriteTime:yyyy-MM-dd HH:mm} - {file.Name} ({FormatFileSize(file.Length)})");
    }
}

// 範例:找到過去一週內修改的所有檔案
var dir = new DirectoryInfo(@"C:\Documents");
var weekAgo = DateTime.Now.AddDays(-7);
FindFilesByDate(dir, weekAgo, DateTime.Now);

尋找重複檔案

快速方法是先按大小分組;要更精準可以再比對內容的雜湊值(hash)。


static void FindPotentialDuplicates(DirectoryInfo dir)
{
    Console.WriteLine($"在 {dir.FullName} 搜尋潛在重複檔案");
    
    var files = dir.GetFiles("*", SearchOption.AllDirectories)
                   .Where(f => f.Length > 0) // 排除空檔案
                   .GroupBy(f => f.Length)
                   .Where(g => g.Count() > 1) // 只保留有多個檔案的群組
                   .OrderByDescending(g => g.Key); // 以大小排序
    
    foreach (var sizeGroup in files.Take(10))
    {
        Console.WriteLine($"\n大小為 {FormatFileSize(sizeGroup.Key)} 的檔案 ({sizeGroup.Count()} 個):");
        foreach (var file in sizeGroup)
        {
            Console.WriteLine($"  {file.FullName}");
            Console.WriteLine($"    已修改: {file.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
        }
    }
}

監控目錄變更

顯示在最近 N 分鐘內被修改的檔案。


static void MonitorRecentChanges(DirectoryInfo dir, int minutesBack = 60)
{
    var cutoffTime = DateTime.Now.AddMinutes(-minutesBack);
    
    var recentFiles = dir.GetFiles("*", SearchOption.AllDirectories)
                         .Where(f => f.LastWriteTime > cutoffTime)
                         .OrderByDescending(f => f.LastWriteTime);
    
    Console.WriteLine($"在過去 {minutesBack} 分鐘內被修改的檔案:");
    
    if (!recentFiles.Any())
    {
        Console.WriteLine("沒有發現變更。");
        return;
    }
    
    foreach (var file in recentFiles)
    {
        var minutesAgo = (DateTime.Now - file.LastWriteTime).TotalMinutes;
        Console.WriteLine($"{file.Name} - {minutesAgo:F0} 分鐘前 ({FormatFileSize(file.Length)})");
    }
}

7. 操作父目錄

FileInfo 的屬性 DirectoryDirectoryInfoParent 可以用來往上走層級。


var file = new FileInfo(@"C:\Projects\MyApp\src\Program.cs");

Console.WriteLine($"檔案: {file.Name}");
Console.WriteLine($"資料夾: {file.Directory.Name}");
Console.WriteLine($"父資料夾: {file.Directory.Parent.Name}");
Console.WriteLine($"專案根資料夾: {file.Directory.Parent.Parent.Name}");

// 可以一直往上走到根目錄
var currentDir = file.Directory;
while (currentDir.Parent != null)
{
    Console.WriteLine($"層級: {currentDir.Name}");
    currentDir = currentDir.Parent;
}
Console.WriteLine($"根目錄: {currentDir.Name}");

8. 陷阱與常見錯誤

1. 快取。物件 FileInfoDirectoryInfo 會快取值。如果在建立物件後檔案被修改,資料可能已過時。使用 Refresh() 來更新。


var file = new FileInfo("test.txt");
file.Refresh(); // 更新 metadata

2. 存取例外。有些檔案和資料夾無法存取:務必處理 UnauthorizedAccessException 與其他存取錯誤。


try
{
    var files = new DirectoryInfo(path).GetFiles();
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("無存取權限");
}

3. 時間戳記。複製時 CreationTime 可能會變更,而 LastWriteTime 可能保留。這會影響報表與同步演算法。


File.Copy("a.txt", "b.txt");

4. 效能。 GetFiles() 會一次載入所有結果,在大型目錄上可能會很慢。對於延遲列舉,偏好使用 EnumerateFiles()

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION