用 Python 写一个磁盘空间分析小工具:扫描大文件与 Top N 占用排行
一、痛点:磁盘又红了,到底是谁吃掉了空间?
相信很多同学都遇到过这种情况:电脑用着用着,C 盘或者某个数据盘突然飘红,系统提示空间不足。打开资源管理器一个文件夹一个文件夹点进去看,效率极低;系统自带的磁盘清理又不敢乱点,怕误删东西。
这时候,一个能快速扫描目录、按大小排出 Top N 的小工具就非常实用了。今天我们就只用 Python 标准库(零第三方依赖、单文件)写一个 disk_top.py:给定一个目录,它递归统计每个文件和子目录的占用,最后给出一份清晰的“空间占用排行榜”,帮你一眼定位“磁盘杀手”。
二、设计思路
- 遍历方式:用
os.scandir()而不是os.listdir()。scandir返回的条目自带文件类型判断和stat缓存,递归扫描大目录时系统调用更少,速度明显更快。 - 目录大小的定义:目录本身没有“大小”这个概念,我们定义目录大小 = 其下所有文件大小之和,递归累加得到。
- 健壮性:扫描系统盘时一定会撞到无权限的目录(比如 Windows 的
System Volume Information),捕获PermissionError/OSError后跳过并计数,而不是让程序直接崩溃。 - 符号链接:跳过软链接,避免循环引用导致无限递归。
- 输出:原始字节数对人不友好,写一个
format_size()转成 B / KB / MB / GB;最后按大小倒序输出 Top N,并支持--min-size过滤掉太小的文件。
三、完整代码
把下面这段代码保存为 disk_top.py,直接就能跑:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""disk_top.py:扫描目录,输出占用空间 Top N(仅 Python 标准库)"""
import argparse
import os
import re
import sys
def parse_size(text: str) -> int:
"""解析 '500KB' / '10MB' / '1GB' 这样的大小字符串为字节数"""
m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*([KMGT]?B)?\s*", text.strip(), re.I)
if not m:
raise ValueError(f"无法解析大小:{text!r}")
num, unit = float(m.group(1)), (m.group(2) or "B").upper()
factor = {"B": 1, "KB": 1024, "MB": 1024 ** 2, "GB": 1024 ** 3, "TB": 1024 ** 4}
return int(num * factor[unit])
def format_size(num: int) -> str:
"""把字节数转成人类可读的字符串"""
size = float(num)
for unit in ("B", "KB", "MB", "GB", "TB"):
if size < 1024 or unit == "TB":
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} PB"
class DiskScanner:
def __init__(self):
self.files = [] # 元素为 (size, path)
self.dir_sizes = {} # path -> 该目录下文件总字节数
self.skipped = 0 # 无权限跳过的目录数
def scan(self, root: str) -> int:
"""递归扫描 root,返回该目录下所有文件的总字节数"""
total = 0
try:
with os.scandir(root) as it:
entries = list(it)
except (PermissionError, OSError):
self.skipped += 1
return 0
for entry in entries:
try:
if entry.is_symlink():
continue # 跳过软链接,防止循环
if entry.is_dir(follow_symlinks=False):
sub = self.scan(entry.path)
self.dir_sizes[entry.path] = sub
total += sub
elif entry.is_file(follow_symlinks=False):
try:
size = entry.stat(follow_symlinks=False).st_size
except OSError:
continue
self.files.append((size, entry.path))
total += size
except OSError:
continue
return total
def main() -> None:
ap = argparse.ArgumentParser(description="扫描目录,输出空间占用 Top N")
ap.add_argument("path", help="要扫描的目录")
ap.add_argument("--top", type=int, default=20, help="输出前 N 名(默认 20)")
ap.add_argument("--min-size", default="1MB",
help="仅显示大于该值的文件,如 500KB / 10MB / 1GB")
args = ap.parse_args()
root = os.path.abspath(args.path)
if not os.path.isdir(root):
sys.exit(f"目录不存在:{root}")
try:
min_size = parse_size(args.min_size)
except ValueError as e:
sys.exit(str(e))
scanner = DiskScanner()
print(f"正在扫描 {root} ...")
total = scanner.scan(root)
big_files = [(s, p) for s, p in scanner.files if s >= min_size]
big_files.sort(reverse=True)
big_dirs = sorted(scanner.dir_sizes.items(), key=lambda kv: kv[1], reverse=True)
print(f"\n目录总占用:{format_size(total)}(跳过无权限目录 {scanner.skipped} 个)")
print(f"\n=== 最大的 {args.top} 个文件(≥{args.min_size})===")
for i, (size, path) in enumerate(big_files[:args.top], 1):
print(f"{i:>3}. {format_size(size):>10} {path}")
print(f"\n=== 最大的 {args.top} 个子目录 ===")
for i, (path, size) in enumerate(big_dirs[:args.top], 1):
print(f"{i:>3}. {format_size(size):>10} {path}")
if __name__ == "__main__":
main()
四、运行步骤
- 把上面的代码保存为
disk_top.py,放到桌面或任意顺手的位置。 - 打开终端,进入文件所在目录,运行:
# Windows:扫描 D 盘,列出前 20 个大于 50MB 的文件和目录
python disk_top.py D:\ --top 20 --min-size 50MB
# macOS / Linux:扫描下载目录
python3 disk_top.py ~/Downloads --top 15 --min-size 10MB
- 你会看到类似这样的输出:
正在扫描 D:\ ...
目录总占用:312.4 GB(跳过无权限目录 3 个)
=== 最大的 20 个文件(≥50MB)===
1. 4.2 GB D:\Games\xxx\data.pack
2. 2.8 GB D:\Backup\2026-09.iso
3. 1.1 GB D:\Videos\xxx.mp4
...
=== 最大的 20 个子目录 ===
1. 180.6 GB D:\Games
2. 66.3 GB D:\Backup
...
- 发现某个子目录特别大?把
path参数换成那个子目录再扫一层,逐层下钻,很快就能定位到“元凶”。确认无用之后再手动删除——删文件前务必二次确认,工具只负责定位,不负责删除。
五、三个值得留意的实现细节
1. 为什么用 os.scandir() 而不是 os.listdir()?
listdir 只返回名字,你想知道是文件还是目录、想取大小,还得再调一次 os.stat(),等于每个条目两次系统调用。scandir 返回的 DirEntry 自带 is_dir() / is_file() / stat() 缓存,扫描几十万文件的目录时差距非常明显。
2. 递归深度够吗?
Python 默认递归上限是 1000,正常文件系统的目录层级远远到不了这个数,所以直接递归是最清晰的写法。真遇到极端情况,可以把递归改写成显式栈(用一个 list 当栈来模拟),逻辑完全等价。
3. 权限与竞态
扫描过程中文件可能被其他程序删除或改名,stat 会抛 OSError;系统目录可能无权限,抛 PermissionError。这两处我们都做了捕获:单个文件失败就跳过,整个目录失败就计数后跳过,保证一次扫描一定能跑完、给出结果。
六、可以继续加的玩法
- 按扩展名聚合:统计
.mp4、.iso、.zip各占多少空间,清理时更有针对性。 - 导出 JSON:把结果
json.dump到文件,方便写脚本做二次分析或画图。 - 定期巡检:Windows 接任务计划程序、Linux 接 cron,每周自动生成一份磁盘占用报告,提前发现空间泄漏。
七、小结
这个小工具只有一百多行、零第三方依赖、Windows / macOS / Linux 通吃,核心就是三招:os.scandir 高效遍历、递归累加算目录大小、异常捕获保证跑完。思路可以复用到很多场景:日志目录膨胀分析、备份目录审计、项目 node_modules 瘦身前的摸底,都是同一个套路:先量化,再动手。
- 点赞
- 收藏
- 关注作者
评论(0)