ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PowerShell一键统计Windows文件夹大小:递归汇总磁盘占用

PowerShell一键统计Windows文件夹大小:递归汇总磁盘占用 先把结论放前面我用PowerShell写了一个Windows脚本能在指定路径下把每一个“直接文件/文件夹”的大小都算出来文件夹内部还递归汇总最后按大小倒序排成表格顺便输出一个总占用。这种需求在运维和日常电脑清理里特别常见尤其是在老的Windows服务器上磁盘飘红、又没法装第三方图形工具时一个脚本几秒钟就能把“到底是谁在占空间”的问题查明白然后对症下药。这篇文章从需求理解、方案选型、脚本设计到运行方式和常见坑全部拆开来讲适合运维人员、开发工程师也适合只想揪出C盘吃满元凶的普通用户。只要你的电脑装的是Windows不需要额外装软件复制粘贴脚本就能用。1. 需求分析到底要解决一个什么问题1.1 先给“直接子项”下一个清晰定义这个标题里最关键的两个字是“直接”。也就是说我们传入一个路径比如D:\data脚本要统计的是D:\data下面第一层的每个条目像D:\data\logs、D:\data\backup这样的文件夹以及D:\data\notes.txt这样的文件。每个文件夹的大小指的是把里面所有子文件夹、所有深层的文件全部加起来的总大小每个文件的大小就是它本身占用的字节数。输出结果里不会出现文件夹内部子文件夹的单独条目否则就变成了“递归列出所有文件”那不是这个脚本的目的。在实际场景里这种“只见森林不见树”的统计方式非常有用。拿我踩过的一个例子来说有一台Windows ServerE盘满了我怀疑是某个应用日志累积占用但那个目录下有几十个按天命名的日志文件夹如果一个个右键查属性再心算加法效率极低。用这个脚本跑一遍E:\app\logs下哪几个日期文件夹占了大头一眼就看到了。1.2 为什么选PowerShell而不是传统bat很多人一听到“Windows脚本”第一反应是.bat或者.cmd批处理。批处理文件处理简单字符串、启动程序确实方便但做文件大小统计相当别扭批处理本身没有原生的文件对象模型获取文件大小要靠%~z扩展变量获取文件夹总大小要靠两层循环配合dir /s的输出去解析文本。文本解析的毛病在于不同Windows语言环境下的dir输出格式不一样中文系统的“个文件”“字节”和英文系统的输出结构差异很大一个不小心就匹配错。PowerShell从诞生起就是面向对象的它把文件、文件夹都封装成了对象有Length属性、有PSIsContainer属性来区分文件和目录还可以直接用管道和Measure-Object做统计处理这类任务的体验完全是降维打击。所以在这个场景下我选择用PowerShell脚本后缀.ps1作为主方案。如果你确实只能在cmd环境里跑我也会在后面的扩展章节给一个简易的bat对照版但主推方案一定是PowerShell。2. 核心思路与算法设计2.1 文件大小与文件夹大小的本质区别写脚本之前先想清楚对象的差异。在Windows的文件系统里一个文件的大小就是它自身占用的字节数这个信息可以从文件对象直接拿到。但一个文件夹的大小并不是某个现成的属性它等于“这个文件夹下所有文件的字节数总和”。这个定义天然是递归的文件夹里有子文件夹子文件夹里还有子文件夹必须把所有层级都遍历到。所以在脚本里我设计了一个递归函数这是整个脚本的发动机。递归函数接收一个文件夹路径作为参数然后做三件事用Get-ChildItem -LiteralPath $FolderPath -Force取到这个文件夹下的所有直接子项。-Force参数很重要它会把隐藏文件、系统文件也一起统计进来不然统计结果会偏小。遍历每个子项如果是文件夹通过PSIsContainer判断就递归调用本函数把返回的大小累加如果是文件就直接累加它的Length属性。返回累加总大小。这里有个容易被忽略的边界空文件夹。递归到空文件夹时Get-ChildItem返回空集合函数不会进入foreach最终返回的累计大小就是0逻辑上没有任何问题。我在初版脚本里甚至故意用一个空文件夹做了测试确认输出是0MB而不是报错。2.2 大小单位换算与输出格式设计计算机里的文件大小是以字节Byte为单位的但直接显示一长串数字没人看得懂。常见的换算规则是1KB等于1024字节1MB等于1024KB1GB等于1024MB1TB等于1024GB这个换算基数是1024而不是1000和硬盘厂商用1000算出来的标称容量不一样属于老生常谈但总是有人搞混。我在脚本里把这个换算放进了输出阶段先把原始字节数除以1MB转成兆比值然后判断这个值如果大于等于1024就再除以1024显示成GB。显示时用{0:N2}格式化保留两位小数。举个例子一个文件占2147483648字节除以1MB后等于2048.00MB因为大于1024继续显示成2.00GB。这样大文件夹不会出现一串吓人的长数字小文件也能精确到小数点后两位算是一个平衡。输出格式我用的是Format-Table表格列包括三列类型文件夹/文件、名称、大小。最后再按大小从大到小排个序这样谁占空间最多直接就会出现在最上面省得自己一条条去比较。2.3 递归算文件夹大小时怎么避免踩坑递归方案看起来简单实际使用时有两个坑值得提前说。第一个坑是权限问题。在Windows上有些系统文件夹或者被加了权限限制的目录Get-ChildItem会直接抛“拒绝访问”的异常。如果脚本不做处理碰到一个无权访问的文件夹整个递归就会中断后面的统计全部作废。解决方式是在递归函数内部加try/catch异常时记录一条警告然后跳过这个文件夹继续统计其他内容。我在实际跑C盘的时候经常碰到类似C:\System Volume Information这种访问不了的目录加了容错之后脚本就能“绕过去”继续干活虽然统计结果略小于真实占用但至少能正常完成任务不至于一次崩溃。第二个坑是重解析点循环。Windows里有符号链接、目录联接junction这类机制比如C:\Documents and Settings在老系统里实际是指向C:\Users的链接。如果递归函数傻乎乎地顺着链接继续进去理论上可能形成死循环最后路径长到爆。好在PowerShell的Get-ChildItem默认不会追踪重解析点的下一层子目录我用普通的Get-ChildItem ls -Recurse风格递归时不会进入这些链接目标这等于无意中规避了风险。不过要是你将来用了 .NET 底层API或者加了-FollowSymlink之类的参数那就得自己判断要不要跳过重解析点了。3. 脚本拆解从骨架到完整实现3.1 参数定义与入口检查脚本给别的同事用第一步就要把输入参数定义清楚。我用param()块定义了一个必填参数-TargetPath表示要统计的根目录。这样脚本既能手动输入路径运行也能被其他程序调用参数化程度比写死路径高一个档次。入口部分还要做一个关键检查用户传来的路径到底存不存在、到底是不是文件夹。如果是一个不存在的路径直接提示错误退出如果用户传了一个文件的路径也提示“必须是目录路径”。检查完合法性再把真实路径转成完整的绝对路径避免后续拼接出错。这里有一个经验细节我在整个脚本里统一使用了-LiteralPath而不是-Path。这俩看起来差不多其实有个微小但致命的差异-Path会把路径里的方括号[ ]当作通配符来解析而-LiteralPath的意思就是“我给的路径是什么就是什么不要做任何通配解析”。Windows路径里完全可能包含方括号字符比如备份文件夹叫data[2024]用-Path就会莫名其妙地找不到文件换成-LiteralPath可以彻底避免这个坑。3.2 目录遍历与大小统计的核心函数核心递归函数如下我把它命名为Get-FolderSize。它接收一个文件夹路径返回值是该文件夹的总大小字节。里面用一个64位整数变量$total来做累加初始值是0。function Get-FolderSize { param( [string]$FolderPath ) $total 0L try { $items Get-ChildItem -LiteralPath $FolderPath -Force -ErrorAction Stop foreach ($item in $items) { if ($item.PSIsContainer) { # 文件夹递归计算子文件夹的总大小 $total Get-FolderSize -FolderPath $item.FullName } else { # 文件直接累加长度 $total $item.Length } } } catch { Write-Warning [跳过] $FolderPath : $($_.Exception.Message) } return $total }我特意用了-ErrorAction Stop配合try/catch默认情况下PowerShell遇到非终止错误只会打印红字然后继续不一定进入异常处理把错误级别提升到终止才能确保catch被触发记录警告。这一点不熟悉PowerShell的人容易漏你可以在自己的机器上试一下删掉-ErrorAction Stop用一个无权访问的目录做递归看看脚本是不是会“炸”得很安静。3.3 主流程遍历直属子项并汇总结果拿到递归函数之后主流程就简单了。先把目标路径下的所有直接子项取出来然后一个foreach循环处理判断每一项是不是文件夹再分别算出大小构造一个自定义对象收集起来。我采取的做法是先准备一个空数组$results ()每次循环用追加一个[PSCustomObject]。对PowerShell里数组追加的效率确实不算高如果路径下有十万个文件级别的子项性能会明显下降。但对于“统计直属第一层子项”这种场景子项数量通常最多几百个完全够用而且代码可读性最好。如果真遇到超大规模目录后面我会给一个用泛型List提升性能的替代写法。这部分代码里还有一个值得玩味的点构造对象时我直接算好大小文本又保留一列原始的“大小MB”数字这样既能排序也能直接给人看。$results () $children Get-ChildItem -LiteralPath $TargetPath -Force -ErrorAction SilentlyContinue foreach ($child in $children) { $name $child.Name if ($child.PSIsContainer) { $sizeBytes Get-FolderSize -FolderPath $child.FullName $sizeMB [Math]::Round($sizeBytes / 1MB, 2) $results [PSCustomObject]{ 类型 文件夹 名称 $name 大小MB $sizeMB 大小文本 if ($sizeMB -ge 1024) { {0:N2} GB -f ($sizeMB / 1024) } else { {0:N2} MB -f $sizeMB } } } else { $sizeMB [Math]::Round($child.Length / 1MB, 2) $results [PSCustomObject]{ 类型 文件 名称 $name 大小MB $sizeMB 大小文本 if ($sizeMB -ge 1024) { {0:N2} GB -f ($sizeMB / 1024) } else { {0:N2} MB -f $sizeMB } } } }最后用Sort-Object 大小MB -Descending排序再Format-Table -AutoSize输出。总大小通过Measure-Object -Property 大小MB -Sum计算这里有个坑如果目标路径下没有任何子项Measure-Object对空集合求和会返回$null直接输出就会显示为空。所以要做一个空值判断为空时把总和置为0。这个我放在后面常见问题里详细讲。3.4 最终版脚本全貌把上面几块拼起来加上参数定义和注释最终脚本完整如下# .SYNOPSIS 计算指定路径下所有直接文件/文件夹的大小 .DESCRIPTION 传入一个目录路径统计该路径下每个直接子项的大小。 文件按自身字节计算文件夹递归汇总内部所有文件和子文件夹。 .EXAMPLE powershell -ExecutionPolicy Bypass -File .\Get-ChildSize.ps1 -TargetPath D:\data # param( [Parameter(Mandatory$true)] [string]$TargetPath ) function Get-FolderSize { param([string]$FolderPath) $total 0L try { $items Get-ChildItem -LiteralPath $FolderPath -Force -ErrorAction Stop foreach ($item in $items) { if ($item.PSIsContainer) { $total Get-FolderSize -FolderPath $item.FullName } else { $total $item.Length } } } catch { Write-Warning [跳过] $FolderPath : $($_.Exception.Message) } return $total } $target Get-Item -LiteralPath $TargetPath -Force -ErrorAction Stop if (-not $target.PSIsContainer) { Write-Error 目标路径必须是目录 exit 1 } $results () $children Get-ChildItem -LiteralPath $target.FullName -Force -ErrorAction SilentlyContinue foreach ($child in $children) { if ($child.PSIsContainer) { $sizeBytes Get-FolderSize -FolderPath $child.FullName $sizeMB [Math]::Round($sizeBytes / 1MB, 2) $results [PSCustomObject]{ 类型 文件夹 名称 $child.Name 大小MB $sizeMB 大小文本 if ($sizeMB -ge 1024) { {0:N2} GB -f ($sizeMB / 1024) } else { {0:N2} MB -f $sizeMB } } } else { $sizeMB [Math]::Round($child.Length / 1MB, 2) $results [PSCustomObject]{ 类型 文件 名称 $child.Name 大小MB $sizeMB 大小文本 if ($sizeMB -ge 1024) { {0:N2} GB -f ($sizeMB / 1024) } else { {0:N2} MB -f $sizeMB } } } } $results | Sort-Object 大小MB -Descending | Format-Table 类型, 名称, 大小文本 -AutoSize $sum ($results | Measure-Object -Property 大小MB -Sum).Sum if ($null -eq $sum) { $sum 0 } Write-Host (总计: {0:N2} MB ({1:N2} GB) -f $sum, ($sum / 1024)) -ForegroundColor Green这个版本我实测过跑一个几百个子项的目录结果几乎瞬间就出来即使目录里有几万个文件最多也就等几秒钟。脚本的健壮性对日常使用来说是足够的。4. 运行与扩展不同打开方式和使用场景4.1 从命令行直接调用脚本写完之后运行方式要讲清楚。很多人第一次用PowerShell脚本都会撞上执行策略这个拦路虎报错信息是“无法加载脚本因为在此系统上禁止运行脚本”。这个问题本质上是Windows的默认安全策略。最简单的临时解决办法是启动PowerShell时直接指定-ExecutionPolicy Bypass这等于在当前会话里放行所有脚本不需要动系统注册表策略也不会永久降低系统安全性。完整的调用命令如下powershell -ExecutionPolicy Bypass -File .\Get-ChildSize.ps1 -TargetPath D:\data如果你已经在PowerShell环境里想临时改一次执行策略再跑可以用Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass-Scope Process表示只对当前窗口生效关掉窗口就恢复原状这种方式我一般更推荐因为不会留下永久修改。需要注意Set-ExecutionPolicy在部分策略环境里可能还是会被拦截那是公司域策略管得严这时候建议直接找管理员放开或者用第一种带参数的方式启动因为-ExecutionPolicy Bypass是在进程启动时就指定的优先级更高。脚本运行后的输出效果大致是这样的示意类型 名称 大小文本 ---- ---- -------- 文件夹 backup 2.35 GB 文件夹 logs 1.16 GB 文件 release.zip 812.45 MB 文件 readme.txt 0.02 MB 文件夹 temp 0.01 MB 总计: 4.55 GB如果看的是中文字段列名在Windows PowerShell控制台里可能会有点对齐问题但不影响阅读。4.2 导出CSV和批量处理多个路径Format-Table适合人眼直接看但如果你想把统计结果保存下来或者拿去做后续的上报告警可以导出成CSV。只需要在脚本末尾加上一行$results | Sort-Object 大小MB -Descending | Select-Object 类型, 名称, 大小MB, 大小文本 | Export-Csv -Path .\result.csv -NoTypeInformation -Encoding UTF8这里有几个细节值得说。第一是-NoTypeInformation必须加不然CSV文件里会多一行#TYPE Selected.PSCustomObject的元信息给后续Excel读取或者程序解析造成干扰。第二是编码在Windows PowerShell 5.1里-Encoding UTF8会输出带BOM的UTF-8文件Excel能正常识别这个行为在PowerShell 7里变成了不带BOM两者各有各的问题。我的经验是如果文件要交给Excel打开用Windows PowerShell 5.1版本没问题如果文件要交给Java、Python之类的程序解析无BOM的UTF-8更友好所以7.x反而是更好的选择。如果你要统计的不只是一个路径而是好几个目录比如C盘、D盘、E盘都想来一遍可以稍微改造一下主流程把-TargetPath改成一个字符串数组[string[]]$TargetPaths外面套一个foreach ($targetPath in $TargetPaths)循环每次循环统计一个路径并输出一段结果。这样一条命令就能把所有盘符的空间占用情况一次摸清楚。4.3 一个轻量级bat对照版附局限说明考虑到有些环境只能用批处理脚本我也写了一个bat版思路但这版本只能做最基础的统计效果打折不少。核心逻辑是用dir /a /b拿到直接的子项列表然后对每个子项判断是不是文件夹文件用%~zi变量拿大小文件夹就用dir /s /a结合find去文本解析总大小。echo off setlocal enabledelayedexpansion set target%~1 if %target% ( echo 用法: %~nx0 ^目录路径^ exit /b ) for /f delims %%i in (dir /a /b %target% 2^nul) do ( set fullpath%target%\%%i if exist !fullpath!\ ( echo [文件夹] %%i for /f tokens* %%s in (dir /s /a !fullpath! 2^nul ^| find 个文件) do echo %%s ) else ( echo [文件] %%~zi %%i ) ) endlocal需要提醒的是这个bat版有两个显而易见的硬伤第一dir /s的汇总文本在不同语言系统里的格式不一样中文系统里是“个文件 个目录”英文系统里是“File(s) Directory(s)”这行输出很脆第二它只打印了原始文本没有排序、没有单位换算、没有进行MB/GB格式化离一个“可以舒服使用的工具”还有很大距离。所以我更建议用PowerShell版bat版只作为极端情况下的应急方案。5. 常见问题与避坑记录5.1 脚本内容变成乱码怎么办PowerShell 5.1默认使用系统ANSI代码页读取脚本文件如果你用记事本把脚本保存成了UTF-8无BOM格式脚本里的中文注释和中文字段名在运行时就会变成“锟斤拷”。这个问题的根治办法是保存脚本时选择“UTF-8 with BOM”编码。具体操作在记事本另存为时编码下拉框选“UTF-8 with BOM”或者VS Code里右下角编码点开选择“UTF-8 with BOM”。如果已经存错了最简单的处理是重新打开后另存为一次。还有一种情况是控制台显示乱码而不是脚本本身乱码。脚本输出中文字段名后PowerShell窗口显示成了乱码这时候需要在控制台执行chcp 65001切换到UTF-8代码页或者执行[Console]::OutputEncoding [System.Text.Encoding]::UTF8大部分情况下能当场解决显示问题。5.2 权限不足、隐藏文件和“拒绝访问”运行脚本统计C盘或者别的受限目录时大概率会碰到两种现象。一种是某些文件夹递归时报“拒绝访问”这种情况下脚本会把对应的警告信息打出来统计继续往下走这个行为是正常的。另一种是某些文件夹明明存在但统计结果里“看不到”这时候十有八九是隐藏属性问题。所以我特别强调-Force参数如果没有它默认不会列出隐藏文件和系统文件。尤其是统计Windows用户目录这类地方缺少-Force会让结果小得超出真实情况。如果你是在做“全盘扫描哪个目录最占空间”这类事我的习惯是用管理员权限运行PowerShell控制台尽可能减少权限盲区。右键控制台图标选择“以管理员身份运行”即可。即使这样仍然会有个别系统保护目录访问不了不要为此焦虑那些目录通常也不是你清理的对象。5.3 文件夹里文件数量特别多统计很慢递归统计的核心制约因素是文件数量不是文件夹深度或者路径长度。一旦目录里有几十万个小文件每访问一个文件都要触发一次文件系统元数据读取耗时就上去了。这种情况我有三个建议第一个建议是接受现状统计总归要读元数据跑起来之后耐心等第二个建议是适当缩小范围先统计第一层子项中你认为最可疑的几个目录而不是一把梭第三个建议是换用更快的方式比如直接用robocopy的/L列表模式配合输出日志来测量目录大小或者改用点.NET API的枚举方式。PowerShell脚本本身也可以优化一下把数组改成System.Collections.Generic.List[object]减少反复复制数组的开销。$list [System.Collections.Generic.List[object]]::new() $list.Add([PSCustomObject]{ 类型 文件夹; 名称 $name; 大小MB $sizeMB })这个改动在子项数量不到一千时感知不到差别但在上万个对象的场景下提升非常明显。代码逻辑不变只是把结果容器换掉了。5.4 其他容易被忽略的细节点最后整理几个我实际使用中遇到过的小问题统一放在这里提醒你。Measure-Object -Sum会遇到空结果返回$null的问题。当目标目录是空目录时主循环压根没有收集到任何结果求和这一步就会“算不出来”。我给的处理是拿到求和值后判断$null就重置为0这样总大小显示不会是空的。$sum ($results | Measure-Object 大小MB -Sum).Sum if ($null -eq $sum) { $sum 0 }文件大小的“占用空间”和“文件大小”是两个概念。Windows文件系统上每个文件实际占用的磁盘空间还受簇大小影响我们脚本统计的.Length是文件逻辑大小不是文件在磁盘上实际划走的物理空间。对小文件来说物理占用通常会比逻辑大小多出零点几KB到几KB。如果你要排查的是“磁盘空间怎么没的”物理占用更接近真凶而脚本算出的逻辑大小则是“数据本身有多大”。绝大部分场景下两者误差不大但如果目录里全是一两KB的小文件心里要有个数统计结果会低于实际磁盘占用变化。路径参数带引号的问题。如果目录路径里包含空格比如D:\My Documents命令行传参时务必要用双引号把整个路径包起来不然会被拆成两个参数脚本就会因为路径不存在而报错。在PowerShell里直接调用脚本时-TargetPath D:\My Documents这个引号也不能省这是所有命令行工具的统一要求。此外如果路径非常长超过了Windows传统路径范围260个字符普通API就访问不了了。解决方案是启用Windows的长路径支持或改用\\?\开头的前缀路径。对于日常目录管理这个问题很少遇到但一旦遇到你可能会误以为是脚本Bug先检查路径长度就能定位。写到这儿我想起第一次给那台E盘爆满的服务器跑类似脚本时输出结果一刷出来全场最扎眼的就是一个每天都在写入、忘记配自动清理的日志目录占了E盘将近一半。后来我把这套脚本稍微改了一下加了个参数让它把结果输出成CSV再接上定时任务每天早上准时把空间报告发出来磁盘再也没出现过悄无声息爆满的情况。如果你也经常和数据目录、备份目录打交道建议先把这个脚本存起来顺手把CSV导出那行也留好等真正需要排查“磁盘空间到底哪去了”的时候你就知道它有多值得了。
返回列表