ARTICLE DETAIL

资讯详情

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

用PowerShell批量统计子文件夹大小:从入门到进阶实战指南

用PowerShell批量统计子文件夹大小:从入门到进阶实战指南 上个月同事小陈把C盘塞到只剩1GB来问我能不能看看到底什么东西在吃空间。我说你直接右键C盘属性看已用空间不就行了他说不是想知道整个盘多大而是想知道到底是哪几个子文件夹最占地方。这个问题用PowerShell批量统计子文件夹大小来解决其实是最省事的不用装任何额外软件命令往终端里一贴几十个目录的体积按从大到小排出来隐藏目录和系统目录也不会漏。今天这篇就把这条命令从入门到进阶拆开讲包含排序、排除目录、定时报表、提速思路以及我实际使用中踩过的坑。适合刚接触PowerShell的桌面运维、开发同学也包括想彻底搞清楚磁盘空间去哪了的普通Windows用户。1. 为什么统计子文件夹大小会成为一个实际问题1.1 一个真实的磁盘告警现场磁盘空间告警在办公环境里大概是运维收到频率最高的工单类型。系统更新失败、数据库备份无法落盘、Outlook邮件收不下来、软件莫名卡死……最后查下来多半是某个分区被塞满了。用户最直接的诉求往往是我不知道该删什么你帮我看看。这种排查在资源管理器里极其痛苦。你点开此电脑对C盘右键看属性只能看到已用空间和剩余空间的总数完全没有细分。于是你得从C:\Users往下钻先看每个用户名文件夹的大小发现某用户很大再点进AppDataAppData里又有Local、Roaming、LocalLow三兄弟Local下面躺着几十个软件目录光看名字根本无法判断哪个能删。等你终于定位到一个可疑目录旁边的系统更新可能已经因为磁盘空间不足失败第二次了。我实际的习惯是直接跑PowerShell把C:\Users下的子目录全部按体积排出来。输出结果通常是几个明显的尖峰一个巨大的AppData\Local缓存目录或者某个软件的数据目录。两分钟内就能确定排查方向。这件事让我养成了一个判断在Windows环境里凡是涉及“文件夹体积”这种需要横向批量对比的问题优先考虑PowerShell而不是GUI。1.2 资源管理器、第三方工具与PowerShell的横向对比能力资源管理器第三方工具如TreeSize/WizTreePowerShell批量列出多个子目录体积不支持只能单个看属性支持支持按体积排序、过滤、自定义规则不支持部分支持完全支持逻辑可编程是否需要安装不需要需要安装且部分功能要授权不需要系统自带自动化与定时任务不支持部分工具支持命令行看版本原生支持脚本可复用数据后续处理只能看最多截图可导出少量格式输出是对象可直接排序/导出/告警学习成本低中等中等但一本万利这张表列完你会发现PowerShell不是完成这项任务里最快的但它是“零依赖”和“可自动化”这两项同时满足的唯一选择。个人电脑上装WizTree没问题速度还快得多因为它直接读NTFS的MFT主文件表几秒就能扫完整个盘但反过来它需要管理员权限和NTFS卷作为前提办公网络里装第三方软件还要走审批。PowerShell慢一点胜在随处可用输出结果还能继续交给后续命令做排序、导出和报警。真正用起来之后不会想回到一层层点开文件夹的方式。2. 先跑通一条命令再看它算的数准不准2.1 最小可用命令与输出解读第一次跑通这个命令我建议先保持最小化不加花里胡哨的功能否则很难判断是管道逻辑错了还是参数写错了。先看这条最基础的Get-ChildItem -Path C:\ -Directory | ForEach-Object { $size (Get-ChildItem -Path $_.FullName -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum [PSCustomObject]{ Folder $_.FullName SizeBytes $size } }这段脚本做的事情很简单先拿Get-ChildItem列出根目录下所有一级子文件夹然后对每一个文件夹递归取到里面所有文件再从文件的Length属性里把字节数累加起来最后生成一个带Folder和SizeBytes两个字段的对象。脚本跑完输出大致长这样Folder SizeBytes ------ --------- C:\Users 15923489351 C:\Windows 18902345678 C:\Program Files 3451234567注意这里还没有排序没有单位换算也没有排除任何目录。先把核心逻辑跑通确认管道没问题再逐步加需求。如果你在这一步就报错大概率是单条命令的语法问题拆分排查比整体排查容易得多。2.2 关键参数的行为剖析上面那条命令里有好几个参数每一个都有它存在的理由漏掉任意一个都会让结果失真或者输出变脏。我整理了一张表参数作用不加会怎样-Directory只取目录对象根目录下的一堆文件也会混进循环严重干扰统计-Recurse递归进入所有子目录只统计第一层文件结果明显偏小-File只保留文件对象Measure-Object会对目录也尝试读Length属性Warning刷屏-Force包含隐藏和系统对象回收站、隐藏缓存目录全部漏掉结果系统性偏小-ErrorAction SilentlyContinue跳过无权限目录红色报错刷屏还可能中断整个脚本这里我最想强调-Force。很多人第一次跑统计时觉得结果比磁盘属性小得多一查往往就是忘了加它。Windows里占空间的隐藏大头不少$Recycle.Bin、System Volume Information、各种软件的隐藏缓存目录。没有-Force这些都不参与统计得出来的报表自然没法用。2.3 逻辑大小与实际磁盘占用的差异第一次全盘统计时容易遇到一个疑惑把各个子文件夹大小加起来怎么和磁盘属性里的已用空间对不上这很正常。Measure-Object求和用的是文件Length属性也就是文件的逻辑大小。而NTFS卷实际占用的物理空间还要受稀疏文件、文件压缩、重复数据删除和硬链接等因素影响。举个例子一个200MB的数据库备份文件如果存放在NTFS压缩目录里物理占用可能只有120MB但Length属性还是200MB。又比如某个文件被硬链接到两个目录两个目录统计时都会把它算一次总和就会虚增。所以这个脚本的定位要明确它是用来“排序查找大目录”的不是用来“计算磁盘实际占用”的。后续你真打算清理以清理前后磁盘属性的变化为准。理解了这个统计口径你就不会拿结果去和厂商文档里的容量参数对质避免很多无谓的困惑。3. 批量统计最容易翻车的三类问题3.1 权限不足时的容错与错误收集扫描C:\Users这种目录时碰不到权限的目录非常常见另一个用户的Profile、System Volume Information、各种系统保护目录。第一次跑脚本时看到成片红色错误容易误以为命令写错了。其实不是是Windows的权限模型在告诉你这里进不去。最简单的处理是给Get-ChildItem加-ErrorAction SilentlyContinue。但完全静默也有坏处你不知道哪里被跳过了万一漏掉的目录恰好是罪魁祸首报表就失真了。更好的做法是把错误收集到变量里统计结束后单独输出。推荐这样写$allErrors () Get-ChildItem -Path $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue -ErrorVariable allErrors注意-ErrorVariable前面的号。PowerShell的变量赋值默认是覆盖但ErrorVariable支持追加模式不加的话每处理一个目录都会把之前收集到的错误数组清空重来最后只剩最后一个目录的错误。这个细节我踩过一次排查了好一会儿才意识到是变量覆盖问题。统计完再单独把$allErrors输出到日志文件既保证脚本不中断又保留完整线索。3.2 符号链接和系统目录会悄悄骗过统计Windows里有两种常见的“虚拟目录”目录联接Junction和符号链接Symbolic Link。它们在资源管理器里看起来和普通文件夹没有区别但实际指向的是另一个位置。最典型的就是C:\Users\某个用户\AppData\Local\Application Data这种内部联接以及不少软件在安装时创建的链接目录。这里的关键问题是递归统计时到底要不要跟随链接。Windows PowerShell 5.1里Get-ChildItem -Recurse对链接目录的处理方式并不统一有些会跟随有些会直接跳过并警告“目录不是空的”统计结果忽大忽小。到了PowerShell 7官方增加了-FollowSymlink参数你可以显式决定行为。我的建议是统计时不要跟随链接。原因是链接指向的物理位置经常不在当前盘符范围内。比如某个链接指向D:\你统计C:\时如果跟着进去会把D盘的内容算进C盘结果严重失真。除非你有特定锚定需求否则默认不跟随把链接目录本身当作一个独立条目盘点即可。另外不跟随也能天然避免循环链接导致的死循环。3.3 中文路径与乱码的正确处理姿势中文Windows下最容易遇到的问题不是统计结果不对而是脚本里中文路径显示成乱码。Windows PowerShell 5.1默认按照系统区域设置的代码页解析脚本文件。如果你用一个保存为UTF-8无BOM的脚本文本里面所有中文字符都会解析成乱码目录名对不上自然统计不到。解决办法有两个一是把脚本文件保存为UTF-8 with BOM5.1能正确识别BOM并按UTF-8解码二是直接用PowerShell 7新版默认UTF-8基本不会出现这类问题。如果只是在控制台里看输出出现乱码还可以临时调整控制台输出编码[Console]::OutputEncoding [System.Text.Encoding]::UTF8不过这个设置只对当前会话有效放到profile.ps1里可以一劳永逸但脚本如果要分发到多台机器我不建议依赖每台机器的profile统一脚本文件保存为UTF-8 with BOM更稳妥。这个习惯我从那之后一直保持基本没再被中文路径乱码困扰过。4. 把临时命令升级成可交付的报表脚本4.1 排序、单位换算与Top N表格输出基础命令跑通之后你很快就会觉得那串十几位的字节数很碍眼。我们需要Sort-Object按字节数排序再通过Select-Object只取前20名输出时格式化单位。这里推荐用.NET字符串格式化而不是把数字格式化成数值再输出$results | Sort-Object SizeBytes -Descending | Select-Object -First 20 | Format-Table FolderName, {NSizeGB;E{{0:N2} -f ($_.SizeBytes / 1GB)}}, FileCount -AutoSize注意表达式块E{...}里的写法。冒号前面的N是列名后面字符串{0:N2}是.NET格式化说明符表示保留两位小数并使用千分位分隔。如果你改用[math]::Round($size/1GB,2)虽然结果差不多但输出的数字类型依然偏宽表格容易被撑变形。另外如果你后续还要对这个结果做进一步排序不要在这里就格式化成字符串应该先保持数值字段排序完毕最后输出时再格式化。4.2 黑名单目录与缓存优化既然是批量统计子文件夹大小大概率有不想统计的目录。回收站、系统还原点、Windows系统目录这些要么是系统接口要么清理意义不大。常见黑名单写法$exclude ($Recycle.Bin, System Volume Information, Windows) Get-ChildItem -Path $root -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin $exclude }注意$Recycle.Bin带美元符号在双引号里会被当成变量展开所以这里必须用单引号包住字符串否则你会得到一个空字符串黑名单直接失效。这个坑很小但一旦失效回收站目录的体积会混进报表Excel透视表里看半天才发现多了一个莫名目录。紧接着要提一个性能细节。很多人会在一个循环块里写两次Get-ChildItem一次算总和一次数文件个数等于每个目录都被递归扫描两遍大数据量下耗时接近翻倍。正确做法是把文件列表先缓存进变量一次扫描满足两个需求$files Get-ChildItem -Path $_.FullName -Recurse -File -Force -ErrorAction SilentlyContinue $bytes ($files | Measure-Object -Property Length -Sum).Sum [PSCustomObject]{ FolderName $_.Name SizeBytes $bytes FileCount $files.Count }这里需要权衡内存。如果一个子目录下有上百万个小文件将文件对象全部缓存会占用可观内存普通办公环境几十个一级目录、每个目录几万文件缓存完全没问题。巨型目录场景我建议回到管道逐对象聚合的写法多扫描一遍换取内存稳定。4.3 CSV导出与Excel打开乱码的分水岭报表脚本最后一步通常是导出CSV。这里有一个跨版本行为差异值得单独说。Windows PowerShell 5.1的Export-Csv -Encoding UTF8会补上BOMExcel打开中文列名没问题而PowerShell 7默认的UTF8不带BOMExcel打开时中文列名会变成乱码因为Excel默认用系统代码页去猜文件编码。所以用PowerShell 7跑脚本时导出参数要写成Export-Csv -Path folder-size-report.csv -NoTypeInformation -Encoding utf8BOM如果你不清楚目标机器上是哪个版本最省事的方法是把整个分发脚本固定用Windows PowerShell 5.1跑。它能识别带BOM的UTF8脚本文件也能输出Excel友好CSV行为最可预测。我自己维护的生产脚本就是统一在5.1环境下运行避免团队里有人用7、有人用5.1导致文件格式不一致。4.4 挂到任务计划里定时生成报告命令行批量统计的价值一半体现在可以定时跑。我用任务计划程序把脚本挂成每周日凌晨两点自动执行周一上班直接看报表。注册命令示例$action New-ScheduledTaskAction -Execute powershell.exe -Argument -NoProfile -ExecutionPolicy Bypass -File D:\Scripts\folder-size-report.ps1 $trigger New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 02:00 Register-ScheduledTask -TaskName FolderSizeWeeklyReport -Action $action -Trigger $trigger这里有两个容易忽略的点脚本路径要写绝对路径不要依赖任务计划程序的工作目录如果脚本本身需要参数比如-RootPath D:\Data把这些参数拼进-Argument字符串里即可。输出文件名建议带时间戳避免每次覆盖同一个文件这样长期积累下来就是一条完整的历史趋势数据。5. 目录变多之后的提速和增量思路5.1 单线程扫描的瓶颈在哪当目录数量从几十个变成上百个扫描时间会显著拉长。这里要先搞清楚瓶颈在什么地方。统计子文件夹大小的核心操作是对每个文件迭代并读取Length属性。真正的开销不在计算而在文件系统IO。几千几万个文件在磁盘上分散分布逐个打开元数据本身就慢机械硬盘上寻道时间占比尤其高。PowerShell单线程串行扫描时只能一个目录树一个目录树地走。只要其中一个目录里躺着几十万个文件整个脚本就被拖住了。CPU占用率往往很低因为大部分时间都在等IO返回。理解了这一点你才能判断什么时候适合加并行。5.2 ForEach-Object -Parallel并行实测PowerShell 7的ForEach-Object新增了-Parallel参数可以把每个子文件夹的扫描任务丢到多个线程里并行执行。示例$root D:\Data Get-ChildItem -Path $root -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object -Parallel { $files Get-ChildItem -Path $_ -Recurse -File -Force -ErrorAction SilentlyContinue [PSCustomObject]{ Folder $_.FullName SizeBytes ($files | Measure-Object -Property Length -Sum).Sum FileCount $files.Count } } -ThrottleLimit 8实测效果非常依赖存储类型。本地SSD上ThrottleLimit取4到8提速比较明显原来十几分钟的扫描可能压缩到四五分钟。但机械硬盘不建议并行并发IO会在磁盘上反复抢占磁头总耗时有时比串行还长。网络共享目录就更不用说瓶颈通常在被访问的服务器端本地再加线程也没有收益。这一点我在不同硬件上测过几轮结论相对稳定。还要提醒一个并行块的变量坑在-Parallel内部使用外部变量需要$using:语法比如$using:root。如果你是从串行版本直接改写的容易出现变量作用域问题脚本运行时报“变量未定义”排查起来比较耗时间。5.3 从历史CSV对比到USN日志的进阶方向全量扫描每周跑一次时间长了会发现大量重复工作。更好的思路是保存历史快照从第二次开始先和上次的结果做对比筛选出体积变化明显的目录再有针对性地深入分析。$historical Import-Csv folder-size-history.csv $current Import-Csv folder-size-current.csv Compare-Object $historical $current -Property Folder, SizeBytes这个简单方案已经能覆盖绝大多数日常场景。更激进的做法是读取NTFS的USN变更日志系统自己记录了哪些文件在什么时间发生过变化理论上可以按增量扫描。但这个方案需要管理员权限对不同Windows版本兼容性也有要求普通维护场景我建议先用历史CSV对比方案稳定直观且不会引入额外权限风险。5.4 一个简单的旁路方案robocopy单目录核查如果你的需求收窄成“我就想知道这一个巨大目录到底多大”用robocopy比纯PowerShell更省心。robocopy是Windows自带的文件复制程序但可以用只扫描不复制模式统计体积robocopy D:\Data NULL /L /S /NJH /BYTES /NFL /NDL /NC /FP /LOG:D:\Data-size.log Get-Content D:\Data-size.log | Select-Object -Last 8这里/L表示只列出不复制NULL是虚拟目标/BYTES让输出以字节为单位日志末尾部分会有总字节数。它的执行效率通常比纯PowerShell管道高因为robocopy本身是原生实现的。不过robocopy一次只能统计一个根目录没法批量汇总多个子文件夹大小所以它更适合做单目录复核批量报表还是交给前面的PowerShell脚本。6. 我的维护经验脚本写完只是开始6.1 统计频率和历史归档的意义脚本跑通之后最重要的决定是多久跑一次。太频繁没有意义普通办公环境的目录体积变化以天为单位一天一次都算奢侈。我个人建议每周或每月跑一次。真正的价值在于趋势如果某周AppData\Local涨了10GB你就能在问题发生前主动清理而不是等系统弹窗告警。归档命名很关键。哪怕你现在用不到历史数据也要养成文件名带日期时间的习惯$stamp Get-Date -Format yyyyMMdd-HHmmss $out folder-size-$stamp.csv不归档的脚本三个月后想回看磁盘增长曲线会发现文件早已被覆盖。归档之后用Excel拖一个数据透视表按目录列求和一眼就能看出哪个目录在持续膨胀这种数据洞察比单次报表值钱得多。6.2 拿到大目录之后怎么定位元凶统计结果会告诉你哪个目录大但不会告诉你为什么大。我的经验是先看目录里有没有明显的缓存、临时文件和安装包。很多软件会把安装包缓存在AppData\Local\Package Cache这类位置体积动辄几个GB删掉不影响软件运行但能释放大量空间。还可以顺手查一下有哪些超过500MB的大文件尤其注意旧下载目录Get-ChildItem -Path C:\Users -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Length -gt 500MB } | Sort-Object Length -Descending | Select-Object -First 30 FullName, {NSizeGB;E{{0:N2} -f ($_.Length / 1GB)}}这里用的其实和批量统计完全相同的遍历管道只是把输出从目录换成了文件。同一个思想可以套到很多场景找出最近30天没访问过的大文件、找出所有超过1GB的视频文件等等。PowerShell的方便之处就在这里管道逻辑一旦跑通换个条件就是新工具。6.3 执行策略、脚本签名与日志留存脚本文件发给同事或扔到其他机器执行时最容易撞上的是执行策略。Windows默认的Restricted策略不允许运行.ps1脚本双击脚本大概率只会用记事本打开。两个选择# 临时执行不影响系统设置 powershell.exe -NoProfile -ExecutionPolicy Bypass -File D:\Scripts\folder-size.ps1 # 或者对当前用户放开远程签名限制 Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned办公环境如果对执行策略有统一管控建议优先使用Bypass参数方式运行副作用最小。另外脚本里的日志、输出路径尽量使用ASCII路径中文路径在部分旧版任务计划和第三方终端里仍存在乱码风险。脚本维护要顺手路径设计不能太随意这是我在分发脚本时多次踩坑后总结出来的规矩。说回开头那个同事他后来把这套脚本贴到了共享目录全组都学会了用PowerShell批量统计子文件夹大小。我觉得这才是一个小型工具的最终状态不是写一次用一次而是能被人拿去复现、按需改参数。你第一次跑通时可以直接把命令粘到终端里找感觉等要定时跑了再按第四节的完整脚本思路组织。如果统计结果哪里不对劲先想权限、链接、编码这三个最容易出问题的点大概率能少折腾半小时。
返回列表