CodeGym /课程 /C# SELF /获取文件和目录的信息

获取文件和目录的信息

C# SELF
第 39 级 , 课程 2
可用

1. 介绍

在 Windows(不只是 Windows)中,每个文件和目录都有一组属性(元数据):完整路径、名字、扩展名、大小、创建/修改/访问时间、属性等。想象一下:文件不仅仅是字节流,它还有一张可以用 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. 深入文件属性的使用

时间戳的详细分析

属性 CreationTime, LastWriteTime, LastAccessTime 返回 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} 天");
}

有趣的是,复制文件时创建时间通常会更新为当前时间,但修改时间可能会保留原始值。这点对备份和活动分析很重要。

处理扩展名和文件名

属性 FullName, NameExtension 看起来简单,但有一些细节:没有扩展名、复合扩展名比如 .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 = { "Б", "КБ", "МБ", "ГБ", "ТБ" };
    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. 元数据的实用场景

按日期查找文件

查找在指定时间段内修改过的文件(适合清理、分析或审计)。


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);

查找重复文件

快速方法是按大小分组。要更精确可以加上内容哈希比较。


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. 与父目录相关的操作

FileInfoDirectory 属性和 DirectoryInfoParent 可以用来向上遍历目录层级。


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(); // 刷新元数据

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