☰
PowerShell脚本批量统计子文件夹大小,轻松排查C盘空间占用
2026/10/11 23:26:02 网站建设 项目流程

我把PowerShell这个标题相关的所有细节都整理好了,内容围绕“批量统计子文件夹大小”展开,从原理、脚本、实操到排坑全程覆盖,写完大概6000多字,可以直接拿去发。


Windows下想批量统计子文件夹大小,我第一个想到的就是PowerShell。它随系统自带、脚本可复用、能一次性输出所有子目录的占用情况,解决“右键属性一个个点”这种原始低效的痛点。这篇文章适合运维、开发、普通办公用户看,尤其是经常发现C盘莫名其妙满掉,又不想装第三方磁盘分析工具的人。PowerShell脚本本身不复杂,我会把原理、代码、坑一次性讲透,让你不仅能跑通,还能按自己的需求改。

1. 为什么推荐用PowerShell做批量统计

1.1 传统方式到底有多痛

很多人遇到“哪个文件夹占空间大”这个问题,第一反应是打开资源管理器,对着子文件夹一个个右键看属性。文件夹少还好,要是目录下面有二三十个子文件夹,一个个点过去、等属性弹窗加载完、再抄下数字,整个过程少说也要十几分钟。更麻烦的是,Windows资源管理器的“属性”窗口一次只能看一个文件夹,没法排序、没法导出、也没法对比。

有人会想到用CMD的dir命令,但dir本身也不直接给文件夹大小,dir /s /a 输出的是一大堆文件列表,最后才给你一个总计字节数。想统计多个子文件夹就得写for循环嵌套,CMD的语法写起来又绕又容易错,批处理变量延迟展开那一套能把人逼疯。

还有人会选择下载第三方工具,比如TreeSize、WinDirStat、du命令之类。这些工具确实好用,但问题在于:公司电脑未必允许安装软件,下载工具本身也有安全风险,而且很多工具是图形界面,没法自动化和定时执行。你只是想“每周看一次目录占用情况”,每次都手动打开工具点一下,反而更麻烦。

1.2 PowerShell方案的三个核心优势

PowerShell做批量统计子文件夹大小,强在三个地方。

第一是零依赖。Windows 7及以上系统出厂自带Windows PowerShell 5.1,不需要装任何额外软件,也没有下载和执行第三方的风险。哪怕是最小化的服务器系统(Server Core),PowerShell也是默认组件。这意味着你可以把脚本丢到任何一台Windows机器上直接跑。

第二是对象化输出。PowerShell不像CMD那样只能处理纯文本,它的命令输出的是结构化对象,可以直接排序、筛选、分组、导出CSV。统计完结果顺手就能变成表格或Excel文件,而不是一堆需要人工再加工的字符串。

第三是可组合、可复用。你可以把统计逻辑封装成函数,存到PowerShell配置文件(profile)里,以后在任何目录直接调用一条命令就能得到结果。也可以挂到计划任务里每周自动跑一次,把结果写成报告,完全不需要人工介入。这一点对于运维场景特别实用。

1.3 动手前先确认环境

在写脚本之前,建议先确认三件事,能省掉后续各种莫名其妙的问题。

首先是版本。系统自带的Windows PowerShell 5.1和可独立安装的PowerShell 7.x(pwsh)在日常命令语法上基本通用,本文脚本两个版本都能跑。但你如果装了PowerShell 7,记得用 pwsh 命令启动,而不是在普通窗口里敲powershell,两者是不同的程序。

其次是打开方式。最简单的:Win + X 菜单里选“Windows PowerShell (管理员)”,或者直接按 Win 键输入 powershell 回车。还有个小技巧,在资源管理器地址栏输入 powershell 并回车,会在当前目录直接打开PowerShell窗口,统计哪个目录就先进哪个目录,非常方便。如果只是查询文件夹大小,普通权限就够,不用非开管理员;但涉及到执行策略修改、读取别人家目录或者系统保护目录时,才需要管理员权限。

最后是执行策略。默认情况下PowerShell禁止运行.ps1脚本,但不影响你在窗口里手动敲命令。如果你想把脚本存成文件来跑,第一次会看到“因为在此系统中禁止执行脚本”的报错。那时候执行以下命令即可,只影响当前用户:

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

RemoteSigned的意思是:本地创建的脚本可以运行,从网上下载的脚本必须带数字签名。这是官方推荐的安全策略,不建议直接改成Unrestricted。

2. 核心命令与原理解析

2.1 统计一条命令就能读懂

PowerShell统计文件夹大小,本质上就两个核心cmdlet配合:Get-ChildItem负责递归列出所有文件,Measure-Object负责把所有文件的Length属性求和。

Get-ChildItem -Path "D:\Data" -Recurse -File | Measure-Object -Property Length -Sum

这条命令看起来短,但里面每个参数都值得说清楚。-Recurse表示递归遍历所有子目录,-File表示只看文件不看目录(避免把文件夹对象也算进去),-Property Length指定要对文件的Length属性做聚合计算,-Sum表示我们要的是总和而不是计数。跑完会输出一个对象,里面有Count和Sum两个核心字段:Count是文件数量,Sum是所有文件的字节数总和。

这里有个新手容易踩的坑:Measure-Object输出的Sum属性,单位是字节(Byte)。一个稍微大点的目录随便就是几十个GB,直接看原始数字完全不直观。所以实际使用中要先换算成GB、MB或者TB,这一点后面脚本里会专门处理。

2.2 为什么统计结果可能对不上

假设你统计C:\Users\你的用户名\Downloads这个目录,跑出来的结果比右键属性里看到的少了几百MB。不用慌,大概率是隐藏文件的问题。

Get-ChildItem在默认情况下不会返回隐藏文件和系统文件,而资源管理器的属性对话框是把它们算进去的。比如Downloads目录里可能有个AppData软链接、隐藏的desktop.ini,或者系统级的回收站文件,这些都会被PowerShell默认漏掉。解决办法很直接:加上-Force参数。

Get-ChildItem -Path "D:\Data" -Recurse -File -Force

-Force在PowerShell里表示强行包含隐藏项。统计文件夹占用空间时,建议一律加上这个参数,不然结果会偏低。

另外还有一种情况会导致统计结果比属性对话框“多”,那就是符号链接和硬链接。如果一个目录里放了指向别处的目录符号链接(比如很多网盘客户端的“同步文件夹”快捷方式),Get-ChildItem -Recurse会顺着链接一层层钻进去,把链接目标里的文件也统计进来。这算不算“这个文件夹的大小”?从占用磁盘的角度看不算,从“在这个目录里能看到哪些内容”的角度看算。所以要看你的业务需求。如果你只想统计真实占用的空间,需要在递归时排除ReparsePoint(重解析点),代码里可以这样写:

Get-ChildItem -Path "D:\Data" -Recurse -File -Force | Where-Object { $_.Attributes -notmatch 'ReparsePoint' }

2.3 两种实现思路,效率差距巨大

前面说的单命令统计只能统计整个目录的总大小,但标题是“批量统计子文件夹大小”,所以我们要的是:指定一个根目录,把根目录下每个一级子目录的大小分别算出来。

实现思路有两种,我强烈建议你理解它们之间的区别,因为它直接决定脚本在大目录下是“能用”还是“卡死”。

第一种思路是“循环递归”。用Get-ChildItem先拿到根目录下所有一级子目录,然后对每一个子目录分别做一次递归统计:

$folders = Get-ChildItem -Path $root -Directory foreach ($folder in $folders) { $size = (Get-ChildItem -Path $folder.FullName -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum }

这种写法逻辑清晰、容易理解,但性能很差。假设根目录下面有20个子目录,每个子目录里的文件总量是100万个,那么这个脚本实际上会把同一批文件翻来覆去地递归20次,每次都从头扫描一遍磁盘。磁盘IO是最大的瓶颈,文件越多越明显。数据量大时,这种写法能把几分钟的事情拖到几十分钟甚至更久。

第二种思路是“一次遍历,按路径分组”。先对整个根目录做一次完整的递归,把所有文件对象拿回来,然后按“这个文件属于哪个一级子目录”进行分组统计:

$files = Get-ChildItem -Path $root -Recurse -File -Force -ErrorAction SilentlyContinue $files | Group-Object { ($_.FullName.Substring($root.Length + 1) -split '\\')[0] } | ForEach-Object { # $_ 是每个分组,$_.Name 是分组依据(一级子目录名) # $_.Group 是组内所有文件对象 }

这样整个根目录只递归扫描一次,之后的所有统计都是内存里的对象运算,速度提升非常明显。实际测试中,几万个文件的场景下两种方式差别可能不到一两秒,但到了几十万甚至上百万文件的场景,差距就是“等到怀疑人生”和“三十秒出结果”的区别。

当然,第二种思路也有缺点——如果你要统计的不是“一级子目录”,而是“所有层级的每个独立文件夹”,那一次遍历分组就不合适了,因为分组依据会变得非常复杂。对于最常见的使用场景(根目录下哪些文件夹占空间最大,好决定删哪些),一级子目录就够了。如果要统计所有层级,我建议直接用第一种思路,只是别把它用在超大目录上。

2.4 一个真正的性能核弹:直接调.NET API

如果你对速度有极致要求,比如要统计的目录有几百万文件,PowerShell的cmdlet仍然会显得啰嗦。Get-ChildItem每次枚举文件都要构造完整的FileInfo对象,这个对象的信息太全了,反而成了负担。微软在.NET里提供的System.IO.Directory类可以绕过PowerShell的部分开销,用纯.NET方法直接枚举:

[System.IO.Directory]::EnumerateFiles($path, '*', [System.IO.SearchOption]::AllDirectories)

EnumerateFiles是惰性求值,边遍历边返回路径字符串,配合foreach循环累加文件长度会快很多。但代价是代码可读性下降,且错误处理要自己搞,权限报错不会自动吞掉,你得包一层try/catch。我的建议是:一般场景用Get-ChildItem就好,别为了那点性能把代码搞复杂;只有当你确实感觉到“这个脚本跑得太慢,慢到不能接受”的时候,再用.NET方式优化。运维行业有个原则:先让功能跑起来,再让速度提上去。

3. 可直接抄作业的完整脚本

3.1 先想清楚你要哪种统计结果

很多人在网上抄脚本跑出来不对,问题往往不是脚本本身,而是需求没说清楚。统计子文件夹大小至少有两种形态:

A. 只要根目录下第一层子文件夹各自的大小,通常用来快速定位“哪个大目录该清理”。

B. 要目录树中所有层级的每个文件夹各自的大小,通常用来做全面的空间分析报告。

A用“一次遍历分组”,B用“循环递归统计”。下面两个脚本分别是这两种形态的实现,你可以直接复制改成自己的路径和需求。

3.2 基础版:逐个递归统计每个子目录

这个版本逻辑直观,适合目录层级不深、文件量不大的场景。我在脚本里加了空值保护,因为如果一个文件夹里没有任何文件,Measure-Object的Sum会是$null,直接参与数学运算会报错。

$root = "D:\Data" # 改成你要统计的根目录 $folders = Get-ChildItem -Path $root -Directory -Force $result = foreach ($folder in $folders) { $size = (Get-ChildItem -Path $folder.FullName -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum if ($null -eq $size) { $size = 0 } [PSCustomObject]@{ 文件夹名 = $folder.Name 完整路径 = $folder.FullName 大小GB = [math]::Round($size / 1GB, 2) 文件数 = $folder | Get-ChildItem -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count } } $result | Sort-Object 大小GB -Descending | Format-Table -AutoSize

注意最后一行,用Format-Table -AutoSize可以让表格自适应宽度,避免长路径被截断显示成省略号。如果你想把结果导出成文件,把最后一行换成:

$result | Sort-Object 大小GB -Descending | Export-Csv -Path "目录大小统计.csv" -NoTypeInformation -Encoding UTF8

-Encoding UTF8在Windows PowerShell 5.1下是带BOM的UTF-8,Excel打开中文不会乱码。如果你用的是PowerShell 7,默认的UTF8不带BOM,Excel打开中文虽然有可能会乱码,但那属于Excel的历史遗留问题,可以在导入时手动选择UTF-8编码。

需要说明的是,这个基础脚本有一个明显的性能缺陷:为了统计“文件数”我又对每个文件夹做了一次完整的递归遍历,等于整体耗时翻倍。如果文件数不是你关心的指标,就把脚本里“文件数”那一行删掉,只保留大小统计。

3.3 优化版:一次遍历按顶层目录分组

如果你的根目录文件量大、子目录多,强烈建议用这个版本。核心逻辑已经说过了:一次性拿回所有文件,Group-Object按一级子目录归属分组,再对每个分组求和。整个磁盘只扫描一遍。

$root = "D:\Data" $allFiles = Get-ChildItem -Path $root -Recurse -File -Force -ErrorAction SilentlyContinue $result = $allFiles | Group-Object { ($_.FullName.Substring($root.Length + 1) -split '\\')[0] } | ForEach-Object { $bytes = ($_.Group | Measure-Object -Property Length -Sum).Sum if ($null -eq $bytes) { $bytes = 0 } [PSCustomObject]@{ 子目录名 = $_.Name 大小GB = [math]::Round($bytes / 1GB, 2) 大小MB = [math]::Round($bytes / 1MB, 2) 文件数 = $_.Count } } $result | Sort-Object 大小GB -Descending | Format-Table -AutoSize

字符串分割那行是理解这个脚本的关键。一个Windows路径长这样:D:\Data\ProjectA\report\2024\summary.xlsx。$_.FullName.Substring($root.Length + 1)把前缀的D:\Data\去掉,变成ProjectA\report\2024\summary.xlsx,再按反斜杠\分割成数组,取第[0]个元素,得到ProjectA。这就把文件归类到了它所属的第一层子目录下。注意路径中如果有空格也没关系,因为我们是按字符切,不涉及命令解析。

这里有一个边界情况要提醒:根目录下的某些文件是直接放在根目录本身、不在任何子文件夹里的(比如D:\Data\readme.txt)。它们分割后的第一个元素是readme.txt,会被当做一个“子目录名”。如果你希望根目录直接下的散文件单独归为“(根目录)”,可以多加一层判断,判断文件路径里是否还包含反斜杠。但对于绝大多数清理场景,这种散文件数量很少,不处理影响不大。

3.4 人味儿十足的格式化函数

统计结果如果用字节自己看,一个是数字太长;每次输出一串“28526471235”也没法一眼感知大小。我习惯在脚本里放一个大小格式化函数,直接输出“12.35 GB”这种人类友好的格式:

function Format-Size { param([double]$bytes) if ($bytes -ge 1TB) { return '{0:N2} TB' -f ($bytes / 1TB) } if ($bytes -ge 1GB) { return '{0:N2} GB' -f ($bytes / 1GB) } if ($bytes -ge 1MB) { return '{0:N2} MB' -f ($bytes / 1MB) } if ($bytes -ge 1KB) { return '{0:N2} KB' -f ($bytes / 1KB) } return '{0} B' -f $bytes }

然后在上面两个脚本的[PSCustomObject]里加上一列“可读大小 = Format-Size -bytes $bytes”,输出的表格一眼就能看懂。这个函数我经常存进PowerShell profile里,任何终端随时可以调用,属于那种“写一次吃一年”的通用工具。

3.5 扩展:Robocopy其实也算一条路

PowerShell不是唯一的选择,Windows自带的robocopy也能统计目录大小,而且速度不慢。虽然robocopy的主要用途是文件复制,但它有一个“列目录但不复制”的模式。你可以对每个子文件夹执行这样的命令:

robocopy <子文件夹路径> <任意空目录> /L /S /NJH /NJS /BYTES

/L是“只列出不复制”,/S递归子目录,/NJH和/NJS关掉头尾信息,/BYTES让输出的统计信息按字节显示。命令跑完,robocopy会在末尾输出类似“Bytes : 1.234 g”这样的统计行。如果你会用脚本循环每个子文件夹,再解析那行文本,也能得到结果。

但我个人不推荐用它做主力方案,原因有两个。一是robocopy的输出是给人类看的文本,不是结构化的对象,你要从一堆报告行里正则匹配提取数字,脚本写起来既笨拙又容易受本地化语言影响。二是robocopy统计的是“复制任务需要处理的数据量”,它可能会因为文件属性、重解析点、dedup等机制和你的预期有偏差。PowerShell的对象管道明显更干净。robocopy这条方案的价值在于:如果哪天PowerShell的Get-ChildItem因为系统权限问题连枚举都做不到,robocopy反而能靠管理员权限硬扛过去。知道有这条路就行。

4. 常见问题与排查技巧实录

4.1 统计出来的数字和右键属性“对不上”

这是后台被问最多的问题。原因通常有三个,解决办法也各有对应。

第一个是隐藏文件漏统计,加-Force即可。第二个是符号链接被递归穿透,导致比预期大,用Where-Object { $_.Attributes -notmatch 'ReparsePoint' }排除。第三个是权限不足,某些子目录(比如系统卷信息、其他用户配置文件)根本打不开,Get-ChildItem会报一堆红色错误。这时要在统计命令末尾加-ErrorAction SilentlyContinue,让脚本跳过访问不了的目录继续往后跑,同时你要接受结果会比真实值略小这个事实。如果你怀疑某个关键目录被跳过,可以先单独对该目录执行一次不带SilentlyContinue的Get-ChildItem,看清楚报错再决定要不要用管理员身份重跑。

4.2 文件量太大,脚本跑了一个小时还没结束

这是典型的“循环递归”写法导致的性能问题。如果已经用了优化版“一次遍历分组”,还是慢,那瓶颈就不在脚本逻辑,而在磁盘IO本身。特别大的目录(几十万文件)首次统计时,操作系统没有对应的文件索引缓存,只能实打实扫一遍磁盘。这时候几个建议:一是用机械硬盘的话,尽量让脚本在磁盘空闲时段跑;二是加上进度提示,至少知道卡在哪儿,而不是干瞪眼等;三是如果统计对象是备份目录或归档目录,考虑用文件系统级别的备份报告替代实时扫描。加个最简单的进度提示可以这样写:

$count = 0 foreach ($file in $allFiles) { $count++ if ($count % 1000 -eq 0) { Write-Host "已处理 $count 个文件..." } }

实际改的时候把整个统计逻辑放在循环里一次搞定,不要一边遍历一边又去套后台递归,那会雪上加霜。

4.3 中文文件夹名显示成乱码

PowerShell窗口默认可能不是UTF-8编码,中文会显示成各种方块或问号。如果你只是想把统计结果输出到CSV,那只要在Export-Csv里指定-Encoding UTF8(5.1版是带BOM的UTF8,7.x要加上编码名称),用Excel打开一般都没问题。如果你想在窗口里直接看中文列表,有两个办法:一是换Windows Terminal,它的默认编码就是UTF-8,Windows 11上已经自带了;二是先执行chcp 65001切换控制台代码页,再重新打开PowerShell窗口。这里有个坑:在PowerShell里执行chcp 65001会把当前窗口的代码页切成UTF-8,但这个改动到自己所在的窗口就重置了,别指望一劳永逸。

如果脚本文件本身是以UTF-8无BOM格式保存的,并且里面有中文路径,PowerShell 5.1读取时可能把中文字符识别成乱码,导致路径找不到。保存.ps1文件时,建议在VSCode或Notepad++里显式选择“UTF-8 with BOM”编码。PowerShell 7则没有这个问题,因为7.x默认就走UTF-8。

4.4 PowerShell窗口的文字复制粘贴技巧

这个看起来是小事,但很多人卡在这。PowerShell 5.1窗口默认启用“快速编辑模式”,你直接用鼠标左键拖选文字,然后按回车或者右键,就能拷贝到剪贴板。粘贴时直接在窗口里点右键即可。也就是说,选中即复制,右键即粘贴,不需要像CMD那样先打开菜单找“标记”。如果你用的是Windows Terminal,复制是Ctrl+Shift+C,粘贴是Ctrl+Shift+V,注意不是普通的Ctrl+C/V,因为Ctrl+C在终端里是中断命令的意思。刚上手的人很容易犯这个错误:辛辛苦苦选中一堆文字,按了一下Ctrl+C,结果命令全被取消了。

4.5 执行策略、版本差异和安全意识

有人脚本跑不起来,是因为Open PowerShell时用的是命令行窗口直接粘贴一段Set-ExecutionPolicy命令。如果你连“是否要更改为执行策略”的提示都没看到,大概率是没开管理员权限。执行策略的默认级别Trusted和实际生效级别是分用户的:当前用户设置过的会覆盖本机默认设置。所以排查的时候,先看当前会话的设置:

Get-ExecutionPolicy -List

输出会列出MachinePolicy、UserPolicy、Process、CurrentUser、LocalMachine几个作用域,CurrentUser生效优先级很高。解决方式就是前面提到的Set-ExecutionPolicy RemoteSigned -Scope CurrentUser。

最后提醒一句:任何脚本都建议你自己看懂每行在做什么再执行,尤其是网上抄来的。PowerShell的执行策略本质上是“防止你双击了一个来路不明的脚本就运行”,而不是“保证你运行了微软代码就一定安全”。统计目录大小的脚本本身不可能有恶意逻辑,但你要养成的习惯是:先打开.ps1文件读一遍,再用编辑器或PowerShell ISE逐步调试,最后才整段跑。

结尾:几个实用经验

我自己用这套脚本统计过几次服务器的共享盘,最大的感受是:统计不重要,重要的是统计完怎么处理。我每次都会顺手在输出里加上文件数和“最后修改时间距今超过N天的文件总大小”这两个衍生维度。比如同样占10GB的目录,一个全是最近在用的项目文件,另一个三年没动过的归档文件夹,清理优先级的判断完全不同。既然PowerShell已经拿回了所有文件对象,额外算这两个指标只是多几行代码的事。

还有一个小技巧:把前面优化版脚本的根目录参数改成$env:TEMP或者`$env:LOCALAPPDATA\Temp`,就能快速看系统临时目录的占用;改成$env:USERPROFILE\Downloads就能看下载文件夹有多大。当你觉得C盘莫名其妙满掉,又不想装任何软件时,这套脚本加一个CSV导出,就是最轻量的“应急磁盘分析工具”。走一遍,结果一目了然,该清哪个目录立刻就有数了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询