C盘又飘红了可你根本说不清是哪个目录吃掉了空间。这种无力感几乎是每个Windows用户的共同记忆。资源管理器为什么指望不上资源管理器是为浏览文件设计的不是为分析磁盘占用设计的。当你想搞清楚空间去哪了它会在每个环节让你失望。逐目录查看。右键单击某个文件夹选属性资源管理器慢吞吞遍历完后只给一个数字。想对比多个目录重复这个动作吧。找不到大文件。可以切换到按大小排序但只对当前目录有效。要找出整个磁盘上最大的文件得逐层进入每个子目录切换排序方式。深度不可控。要么只看直接子项要么在属性面板里一口气扫到底没有只看前3层这种折中选项。没有报告导出。所有信息只能截屏保存无法生成文本报告更别提记录每个文件的完整路径。多盘符无力。机器上有C、D、E多个盘符时想看整台机器的空间分布只能逐个盘符手动操作。权限错误含糊。遇到无法访问的系统目录弹出一串难以理解的提示你不知道哪些文件被跳过、影响多大。困境与对策资源管理器的每一道坎脚本都给了对应的解法。困境一只能逐目录查看无法横向对比。对策支持一次扫描多个路径。用-p指定多个路径脚本会分别扫描并汇总输出不传参数时自动检测所有可用盘符全盘扫描。命令python disk_usage.py -p C:\ D:\就能同时看两个盘。实现上通过遍历所有有效路径把每个路径的统计结果累加到全局总量上。困境二找不到整个磁盘上最大的文件。对策扫描过程中维护一个最大文件候选列表最终输出Top N。用--top 50可以列出前50个最大文件及其完整路径。找大文件不再需要层层点击。困境三递归深度不可控要么太浅要么扫到底。对策用-d参数限制递归层数默认5层。-d 2快速看顶层分布-d 10看深层细节。实现上在os.walk遍历时根据路径分隔符数量计算当前深度达到上限就清空待访问子目录列表阻止下钻。困境四没有报告导出能力。对策统计报告既打印到终端也同步写入日志文件。默认在脚本同目录生成同名的.log文件把扫描到的每个文件完整路径都记下来事后分析或转发都方便。用--log可以指定日志路径。困境五多盘符场景无力。对策脚本启动时会遍历A到Z所有盘符检测可用盘一条命令就能扫遍整台机器。多路径结果合并到同一份报告里目录统计合并、最大文件列表合并后重新排序截取前N个。困境六权限错误处理含糊。对策文件访问用异常捕获包裹遇到OSError或PermissionError直接跳过当前文件继续扫描下一个不会因为个别权限问题中断整个流程。完整源码将以下内容保存为disk_usage.py即可直接运行仅依赖Python 3标准库无需安装任何第三方包。建议配合后面的工作原理章节阅读每个函数的职责会更容易理解。#!/usr/bin/env python3 磁盘文件大小统计工具 统计指定路径下的文件大小分布支持最大递归深度控制和多路径扫描。 importosimportsysimportargparseimportstringimportloggingfrompathlibimportPathdefget_all_drives():获取Windows系统所有可用盘符drives[]forletterinstring.ascii_uppercase:drivef{letter}:\\ifos.path.exists(drive):drives.append(drive)returndrivesdefsetup_logger(log_path):设置日志记录器将扫描的文件路径写入日志文件loggerlogging.getLogger(disk_usage)logger.setLevel(logging.INFO)# 文件处理器追加模式fhlogging.FileHandler(log_path,modew,encodingutf-8)fh.setLevel(logging.INFO)fh.setFormatter(logging.Formatter(%(message)s))logger.addHandler(fh)returnlogger,log_pathdefscan_directory(start_path,max_depth5,loggerNone): 扫描目录并返回统计信息 返回: total_size: 总大小(字节) file_count: 文件总数 dir_stats: 目录统计信息 {dir_path: {size, count}} largest_files: 最大的N个文件 [(path, size)] total_size0file_count0dir_stats{}largest_files[]# 用于跟踪前N大文件的最小阈值优化性能min_top_file_size0start_pathos.path.abspath(start_path)start_depthstart_path.rstrip(os.sep).count(os.sep)forroot,dirs,filesinos.walk(start_path,followlinksFalse):# 计算当前深度current_depthroot.rstrip(os.sep).count(os.sep)-start_depth# 如果达到最大深度阻止进一步递归ifcurrent_depthmax_depth:dirs.clear()# 处理当前目录的文件dir_file_count0dir_total_size0forfileinfiles:try:file_pathos.path.join(root,file)statos.stat(file_path)file_sizestat.st_size total_sizefile_size file_count1dir_file_count1dir_total_sizefile_size# 记录最大文件 (保持最多100个提高性能)iffile_sizemin_top_file_size:largest_files.append((file_path,file_size))iflen(largest_files)100:largest_files.sort(keylambdax:x[1],reverseTrue)largest_files.pop()min_top_file_sizelargest_files[-1][1]iflargest_fileselse0# 记录到日志iflogger:logger.info(file_path)except(OSError,PermissionError):continue# 记录目录统计仅统计有文件的目录ifdir_file_count0:dir_stats[root]{size:dir_total_size,count:dir_file_count,depth:current_depth}# 排序最大文件largest_files.sort(keylambdax:x[1],reverseTrue)returntotal_size,file_count,dir_stats,largest_filesdefformat_size(size_bytes):格式化字节大小为可读字符串forunitin[B,KB,MB,GB,TB]:ifabs(size_bytes)1024.0:returnf{size_bytes:.2f}{unit}size_bytes/1024.0returnf{size_bytes:.2f}PBdefwrite_report(f,start_path,total_size,file_count,dir_stats,largest_files,max_depth,top_n20,no_top_filesFalse):将统计报告写入文件或打印到终端# 计算所有目录的公共前缀用于缩短路径显示all_dirslist(dir_stats.keys())dir_commonifall_dirs:try:iflen(all_dirs)1:dir_commonos.path.commonpath(all_dirs)else:dir_commonos.path.dirname(all_dirs[0])exceptValueError:passprint(f\n{*65},filef)print(f 磁盘文件大小统计报告,filef)print(f{*65},filef)print(f 扫描路径:{start_path},filef)print(f 最大深度:{max_depth}层,filef)print(f 文件总数:{file_count:,},filef)print(f 总大小 :{format_size(total_size)},filef)print(f{*65},filef)ifnotdir_stats:print(f\n --- 未扫描到任何文件 ---,filef)returnprint(f\n 目录大小分布 (Top{top_n}):,filef)print(f{目录:45}{文件数:8}{大小:10},filef)print(f{-*45}{-*8}{-*10},filef)sorted_dirssorted(dir_stats.items(),keylambdax:x[1][size],reverseTrue)fordir_path,statinsorted_dirs[:top_n]:displaydir_pathifdir_commonanddir_path.startswith(dir_common):reldir_path[len(dir_common):].lstrip(\\).lstrip(/)ifrel:displayreliflen(display)42:display...display[-39:]print(f{display:45}{stat[count]:8,}{format_size(stat[size]):10},filef)iflargest_filesandnotno_top_files:visible_nmin(top_n,len(largest_files))print(f\n 最大的文件 (Top{visible_n}):,filef)print(f{:3}{路径:50}{大小:10},filef)print(f{-*3}{-*50}{-*10},filef)try:common_prefixos.path.commonpath([f[0]forfinlargest_files[:top_n]])exceptValueError:common_prefixfori,(path,size)inenumerate(largest_files[:top_n],1):displaypath[len(common_prefix):].lstrip(\\).lstrip(/)ifpath.startswith(common_prefix)andcommon_prefixelsepathiflen(display)50:display...display[-47:]print(f{i:2}.{display:50}{format_size(size):8},filef)defmain():parserargparse.ArgumentParser(description磁盘文件大小统计工具 - 递归扫描目录并统计文件大小分布,formatter_classargparse.RawDescriptionHelpFormatter,epilogr 示例: %(prog)s # 扫描所有盘符,最大深度5层 %(prog)s -p C:\Users # 扫描指定路径 %(prog)s -d 3 # 最大递归深度为3层 %(prog)s -p D:\Projects -d 10 # 扫描D:\Projects,允许10层 %(prog)s -p C:\ D:\ -d 2 # 扫描C盘和D盘根目录,深度2层 %(prog)s --top 50 # 显示前50个最大目录/文件 )parser.add_argument(-p,--path,nargs,metavar路径,help开始扫描路径可指定多个。默认为所有盘符根路径)parser.add_argument(-d,--depth,typeint,default5,metavarN,help最大递归层数默认: 5)parser.add_argument(--top,typeint,default20,metavarN,help显示前N个最大目录和文件默认: 20)parser.add_argument(--no-top-files,actionstore_true,help不显示最大文件列表)parser.add_argument(--log,metavar文件,help日志文件路径默认: 脚本目录/脚本名.log)argsparser.parse_args()# 参数校验ifargs.depth0:print(错误: 递归深度不能为负数)sys.exit(1)ifargs.top1:print(错误: --top 参数必须 1)sys.exit(1)# 确定扫描路径ifargs.path:paths[os.path.abspath(p)forpinargs.path]else:print(正在检测可用盘符...)pathsget_all_drives()ifnotpaths:print(错误: 未检测到任何盘符)sys.exit(1)print(f发现{len(paths)}个盘符:{, .join(p[0]forpinpaths)})# 验证路径valid_paths[]forpinpaths:ifos.path.exists(p):valid_paths.append(p)else:print(f警告: 路径不存在, 跳过:{p})ifnotvalid_paths:print(错误: 没有有效的扫描路径)sys.exit(1)max_depthargs.depth# 设置日志ifargs.log:log_pathos.path.abspath(args.log)else:script_pathPath(sys.argv[0])log_pathscript_path.with_suffix(.log)logger,log_pathsetup_logger(log_path)# 分路径统计all_dir_stats{}all_largest_files[]grand_total_size0grand_file_count0forvpinvalid_paths:print(f\n[{vp}] 正在扫描 (深度{max_depth}层)...)total_size,file_count,dir_stats,largest_filesscan_directory(vp,max_depth,logger)grand_total_sizetotal_size grand_file_countfile_count all_dir_stats.update(dir_stats)all_largest_files.extend(largest_files)iflen(valid_paths)1:print(f -{format_size(total_size)},{file_count:,}个文件)# 合并并排序最大文件all_largest_files.sort(keylambdax:x[1],reverseTrue)all_largest_filesall_largest_files[:args.top]# 生成起始路径显示文本iflen(valid_paths)1:start_displayvalid_paths[0]else:start_display, .join(valid_paths[:3])iflen(valid_paths)3:start_displayf 等{len(valid_paths)}个路径# 如果多路径先打印多路径汇总print(f\n{*65})print(f 多路径扫描汇总)print(f{*65})forvpinvalid_paths:print(f {vp})write_report(sys.stdout,start_display,grand_total_size,grand_file_count,all_dir_stats,all_largest_files,max_depth,args.top,no_top_filesargs.no_top_files)# 统计报告也写入日志文件withopen(log_path,a,encodingutf-8)aslf:write_report(lf,start_display,grand_total_size,grand_file_count,all_dir_stats,all_largest_files,max_depth,args.top,no_top_filesargs.no_top_files)# 显示日志文件位置print(f\n 日志文件:{log_path})if__name____main__:main()工作原理从命令到报告脚本的整体流程可以归纳为三个阶段解析与准备、递归扫描、汇总与输出。解析与准备阶段。脚本启动后用 argparse 解析命令行参数校验递归深度不能为负、Top N必须大于等于1。然后确定扫描路径用户指定了就用指定的没指定就遍历所有盘符检测可用盘。最后初始化日志记录器以覆盖写入模式打开日志文件。递归扫描阶段。核心函数scan_directory用os.walk遍历目录树每进入一层目录就根据路径分隔符数量计算当前深度达到上限就清空待访问子目录列表阻止下钻。对每个文件调用os.stat获取大小累加到全局总数和当前目录的本地统计中同时把文件路径写入日志。这里有两个关键设计值得注意。一是Top N文件的高效维护。脚本不是扫描完再全量排序而是边扫描边维护一个最多100个的候选列表并记录当前列表中的最小值作为阈值。只有大于等于阈值的文件才进入候选列表列表超长时排序后弹出最小的那个。这种做法避免了对海量文件做全量排序的开销。二是异常跳过。文件访问用try/except包裹遇到权限问题直接跳过继续保证扫描不会因个别文件中断。汇总与输出阶段。如果指定了多个路径脚本会把所有路径的结果合并总大小和文件数累加目录统计合并到同一字典最大文件列表合并后重新排序截取前N个。write_report函数计算所有目录的公共前缀用于缩短显示路径再按大小排序输出目录榜单和最大文件榜单。报告既打印到终端也追加写入日志文件末尾。运行效果把脚本保存为disk_usage.py在终端里运行python disk_usage.py -p D:\Projects -d 3你会得到一份类似这样的报告 磁盘文件大小统计报告 扫描路径: D:\Projects 最大深度: 3 层 文件总数: 12,345 总大小 : 23.45 GB 目录大小分布 (Top 20): 目录 文件数 大小 --------------------------------------------- -------- ---------- node_modules 234 5.23 GB .git/objects 567 2.10 GB dist 89 1.45 GB src/components 156 0.87 GB ... 最大的文件 (Top 20): 路径 大小 --- -------------------------------------------------- -------- 1. node_modules/large-lib/index.js 120.45 MB 2. .git/objects/pack/pack-abc.pack 98.32 MB 3. dist/bundle.js 45.10 MB ...哪几个目录吃掉了大部分空间、最大的文件藏在哪、总共扫了多少文件一眼看清。参数速查脚本所有参数集中说明如下组合使用即可应对不同场景。-p或--path指定扫描路径可一次传多个空格分隔。不传时默认扫描所有可用盘符。-d或--depth最大递归层数默认5。设为0表示只扫描起始目录下的直接子项。--top报告中显示前N个最大目录和最大文件默认20。--no-top-files只显示目录榜单不显示最大文件列表。扫描海量文件时可以加快输出。--log自定义日志文件路径。不传时默认使用脚本同目录下的同名.log文件。小结资源管理器适合浏览文件不适合分析磁盘占用。这个脚本把我的磁盘空间都被什么吃掉了这个问题从一次次右键属性的笨拙操作简化成一条命令就能拿到答案。深度可控、多路径扫描、Top N排序、日志归档每一项都对应着资源管理器做不到或者做得很别扭的事情。下一次磁盘飘红之前不妨先跑一遍这个脚本让数据告诉你答案。