C#删除所有空子目录

60

我有一个任务是清理大量目录。我希望从一个目录开始,删除任何不包含文件(无论有多深)的子目录(只删除目录,不删除文件)。如果起始目录不包含文件或子目录,则也将被删除。我希望有人可以指导我使用C#编写现有代码来完成这个任务,而不是重新发明轮子。


3
使用PowerShell如何? - Mitch Wheat
10个回答

117

使用 C# 代码。

static void Main(string[] args)
{
    processDirectory(@"c:\temp");
}

private static void processDirectory(string startLocation)
{
    foreach (var directory in Directory.GetDirectories(startLocation))
    {
        processDirectory(directory);
        if (Directory.GetFiles(directory).Length == 0 && 
            Directory.GetDirectories(directory).Length == 0)
        {
            Directory.Delete(directory, false);
        }
    }
}

20
一个更快的编写if语句的方法可能是 if (Directory.GetFileSystemEntries(directory).Length == 0) - Jesse C. Slicer
18
(!Directory.EnumerateFileSystemEntries(directory).Any())这样写更好。 (说明:这是一段代码,意思是检查一个指定目录是否为空) - prime23
6
这是一个递归函数。参见:http://en.wikipedia.org/wiki/Recursive_function - Jean-François Côté
@JesseC.Slicer 那也可以。但我不确定哪个速度最快。 - puretppc
1
Get* 调用在返回之前枚举所有条目,而 Enumerate 调用则会产生项,因此检查 Any 可以使其更早退出(做更少的工作)而不是收集所有条目 - 这可能会在具有大量项目的目录上产生巨大的差异。 - NiKiZe
显示剩余2条评论

55

如果你可以针对.NET 4.0,你可以使用Directory类的新方法来枚举目录,以避免在仅想知道是否至少有一个文件时列出目录中的每个文件而支付性能损失。

这些方法包括:

  • Directory.EnumerateDirectories
  • Directory.EnumerateFiles
  • Directory.EnumerateFileSystemEntries

以下是使用递归的可能实现:

static void Main(string[] args)
{
    DeleteEmptyDirs("Start");
}

static void DeleteEmptyDirs(string dir)
{
    if (String.IsNullOrEmpty(dir))
        throw new ArgumentException(
            "Starting directory is a null reference or an empty string", 
            "dir");

    try
    {
        foreach (var d in Directory.EnumerateDirectories(dir))
        {
            DeleteEmptyDirs(d);
        }

        var entries = Directory.EnumerateFileSystemEntries(dir);

        if (!entries.Any())
        {
            try
            {
                Directory.Delete(dir);
            }
            catch (UnauthorizedAccessException) { }
            catch (DirectoryNotFoundException) { }
        }
    }
    catch (UnauthorizedAccessException) { }
}

你也提到目录树可能非常深,因此如果您正在探测的路径过长,可能会出现一些异常。


谢谢,但不幸的是我们不使用 .Net 4.0。我希望我们能使用,因为我有大约20,000个文件夹要处理。 - Jay
3
好的回答。你可以使用 if ( ! entries.Any() ) 替代 if (String.IsNullOrEmpty(entries.FirstOrDefault())),我认为这样更简洁明了。请注意,不要改变原来的意思。 - Danko Durbić
2
完全同意你的观点,@Danko Durbić。我没有注意到没有参数的重载,已经在自问为什么Enumerable没有快速检查空IEnumerable的东西了。谢谢,我更新了答案。 - João Angelo
这是大目录的答案,因为GetDirectories对于相当大的目录来说速度太慢了。我会说,如果你没有.NET 4.0,那么你应该使用此线程中的API调用http://stackoverflow.com/questions/1741306/c-sharp-get-file-names-and-last-write-times-for-large-directories - Michael Hohlios

12

在已经提到的三种方法上,对C:\Windows进行1000次测试的结果如下:

GetFiles+GetDirectories:630ms
GetFileSystemEntries:295ms
EnumerateFileSystemEntries.Any:71ms

在空文件夹上运行它,得到了这个结果(再次重复1000次):

GetFiles+GetDirectories:131ms
GetFileSystemEntries:66ms
EnumerateFileSystemEntries.Any:64ms

因此,当您检查空文件夹时,EnumerateFileSystemEntries是迄今为止最好的选择。


一个空文件夹需要131毫秒吗?! - Patrick
1
1000次在空文件夹中执行。 - Wolf5

5

这是一个利用并行执行来更快地完成某些情况下任务的版本:

public static void DeleteEmptySubdirectories(string parentDirectory){
  System.Threading.Tasks.Parallel.ForEach(System.IO.Directory.GetDirectories(parentDirectory), directory => {
    DeleteEmptySubdirectories(directory);
    if(!System.IO.Directory.EnumerateFileSystemEntries(directory).Any()) System.IO.Directory.Delete(directory, false);
  });   
}

以下是单线程模式下的相同代码:

public static void DeleteEmptySubdirectoriesSingleThread(string parentDirectory){
  foreach(string directory in System.IO.Directory.GetDirectories(parentDirectory)){
    DeleteEmptySubdirectories(directory);
    if(!System.IO.Directory.EnumerateFileSystemEntries(directory).Any()) System.IO.Directory.Delete(directory, false);
  }
}

这里有一些示例代码,您可以使用它来测试您的场景中的结果:

var stopWatch = new System.Diagnostics.Stopwatch();
for(int i = 0; i < 100; i++) {
  stopWatch.Restart();
  DeleteEmptySubdirectories(rootPath);
  stopWatch.Stop();
  StatusOutputStream.WriteLine("Parallel: "+stopWatch.ElapsedMilliseconds);
  stopWatch.Restart();
  DeleteEmptySubdirectoriesSingleThread(rootPath);
  stopWatch.Stop();
  StatusOutputStream.WriteLine("Single: "+stopWatch.ElapsedMilliseconds);
}

这里是我机器的一些结果,针对一个跨越广域网的文件共享目录。该共享目录目前只有16个子文件夹和2277个文件。

Parallel: 1479
Single: 4724
Parallel: 1691
Single: 5603
Parallel: 1540
Single: 4959
Parallel: 1592
Single: 4792
Parallel: 1671
Single: 4849
Parallel: 1485
Single: 4389

3
如果你依赖于 DirectoryInfo.Delete 只删除空目录,你可以编写一个简洁的扩展方法。
public static void DeleteEmptyDirs(this DirectoryInfo dir)
{
    foreach (DirectoryInfo d in dir.GetDirectories())
        d.DeleteEmptyDirs();

    try { dir.Delete(); }
    catch (IOException) {}
    catch (UnauthorizedAccessException) {}
}

使用方法:

static void Main()
{
    new DirectoryInfo(@"C:\temp").DeleteEmptyDirs();
}

3
从这里开始,来自用于删除空目录的PowerShell脚本
$items = Get-ChildItem -Recurse

foreach($item in $items)
{
      if( $item.PSIsContainer )
      {
            $subitems = Get-ChildItem -Recurse -Path $item.FullName
            if($subitems -eq $null)
            {
                  "Remove item: " + $item.FullName
                  Remove-Item $item.FullName
            }
            $subitems = $null
      }
}

注意:使用需自负风险!

抱歉,PowerShell 不是一个选项。谢谢你的关注。 - Jay
本主题讨论的是C#。 - shtse8
@shtse8: 完成任务就在于使用最简便的工具。 - Mitch Wheat

1
    private static void deleteEmptySubFolders(string ffd, bool deleteIfFileSizeZero=false)
{
    DirectoryInfo di = new DirectoryInfo(ffd);
    foreach (DirectoryInfo diSon in di.GetDirectories("*", SearchOption.TopDirectoryOnly))
    {
        FileInfo[] fis = diSon.GetFiles("*.*", SearchOption.AllDirectories);
        if (fis == null || fis.Length < 1)
        {
            diSon.Delete(true);
        }
        else
        {
            if (deleteIfFileSizeZero)
            {
                long total = 0;
                foreach (FileInfo fi in fis)
                {
                    total = total + fi.Length;
                    if (total > 0)
                    {
                        break;
                    }
                }

                if (total == 0)
                {
                    diSon.Delete(true);
                    continue;
                }
            }

            deleteEmptySubFolders(diSon.FullName, deleteIfFileSizeZero);
        }
    }
}

0
//Recursive scan of empty dirs. See example output bottom

string startDir = @"d:\root";

void Scan(string dir, bool stepBack)
    {
        //directory not empty
        if (Directory.GetFileSystemEntries(dir).Length > 0)
        {
            if (!stepBack)
            {
                foreach (string subdir in Directory.GetDirectories(dir))
                    Scan(subdir, false);
            } 
        }
        //directory empty so delete it.
        else
        {
            Directory.Delete(dir);
            string prevDir = dir.Substring(0, dir.LastIndexOf("\\"));
            if (startDir.Length <= prevDir.Length)
                Scan(prevDir, true);
        }
    }
//call like this
Scan(startDir, false);

/*EXAMPLE outputof d:\root with empty subfolders and one filled with files
   Scanning d:\root
   Scanning d:\root\folder1 (not empty)
   Scanning d:\root\folder1\folder1sub1 (not empty)
   Scanning d:\root\folder1\folder1sub1\folder2sub2 (deleted!)
   Scanning d:\root\folder1\folder1sub1 (deleted!)
   Scanning d:\root\folder1 (deleted)
   Scanning d:\root (not empty)
   Scanning d:\root\folder2 (not empty)
   Scanning d:\root\folder2\folder2sub1 (deleted)
   Scanning d:\root\folder2 (not empty)
   Scanning d:\root\folder2\notempty (not empty) */

1
该算法将删除给定起始目录中的所有空目录。当一个目录被删除时,先前的目录将被重新扫描,因为先前的目录现在可能为空。 - Jamil
不需要那样做。你应该考虑递归的好处,也许可以慢慢地调试已经提供的答案。 - Oliver
真的,但这个程序有什么不递归的呢?我有点太快了,所以我做了一个小修正。请参见输出示例,了解此代码的功能。我认为这很有效率。 - Jamil
如果您将foreach放在if语句之上,就不需要stepBack参数,这将使您的代码更易于阅读(最终您的代码将得到与已给出答案相同的结果)。此外,您应该使用Directory.Enumerate...()而不是Directory.Get...()。因此,您的答案并没有提供任何新的东西,反而使其过时了。 - Oliver
我认为这个参数可以提高效率(虽然我可能是错的,但这是我的贡献 :) ),原因是:只有被删除的文件夹的父文件夹会再次被重新扫描,而不会再次扫描所有子目录。你可以自己试一下,比较一下使用和不使用stepback参数的输出结果。你会发现,如果没有这个参数,文件夹会被多次扫描。 - Jamil
采用João Angelo的答案,并且每个目录只会被扫描一次(不使用任何递归参数)。 - Oliver

0
    foreach (var folder in Directory.GetDirectories(myDir, "*", System.IO.SearchOption.AllDirectories))
    {
        {
            try
            {
                if (Directory.GetFiles(folder, "*", System.IO.SearchOption.AllDirectories).Length == 0)
                    Directory.Delete(folder, true);
            }
            catch { }
        }
    }

0
foreach (var emptyDir in Directory.GetDirectories(root,"*",SearchOption.AllDirectories)
                                  .Select(p=>new DirectoryInfo(p))
                                  .Where(p=>p.GetFiles("*.*", SearchOption.AllDirectories).Count() == 0))
{
        
    if(emptyDir.Exists){emptyDir.Delete(true);}

    
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接