用 Python 写一个磁盘空间分析小工具:扫描大文件与 Top N 占用排行

举报
yd_240716394 发表于 2026/10/30 23:54:44 2026/10/30
【摘要】 一、痛点:磁盘又红了,到底是谁吃掉了空间?相信很多同学都遇到过这种情况:电脑用着用着,C 盘或者某个数据盘突然飘红,系统提示空间不足。打开资源管理器一个文件夹一个文件夹点进去看,效率极低;系统自带的磁盘清理又不敢乱点,怕误删东西。这时候,一个能快速扫描目录、按大小排出 Top N 的小工具就非常实用了。今天我们就只用 Python 标准库(零第三方依赖、单文件)写一个 disk_top.p...

一、痛点:磁盘又红了,到底是谁吃掉了空间?

相信很多同学都遇到过这种情况:电脑用着用着,C 盘或者某个数据盘突然飘红,系统提示空间不足。打开资源管理器一个文件夹一个文件夹点进去看,效率极低;系统自带的磁盘清理又不敢乱点,怕误删东西。

这时候,一个能快速扫描目录、按大小排出 Top N 的小工具就非常实用了。今天我们就只用 Python 标准库(零第三方依赖、单文件)写一个 disk_top.py:给定一个目录,它递归统计每个文件和子目录的占用,最后给出一份清晰的“空间占用排行榜”,帮你一眼定位“磁盘杀手”。

二、设计思路

  1. 遍历方式:用 os.scandir() 而不是 os.listdir()。scandir 返回的条目自带文件类型判断和 stat 缓存,递归扫描大目录时系统调用更少,速度明显更快。
  2. 目录大小的定义:目录本身没有“大小”这个概念,我们定义目录大小 = 其下所有文件大小之和,递归累加得到。
  3. 健壮性:扫描系统盘时一定会撞到无权限的目录(比如 Windows 的 System Volume Information),捕获 PermissionError / OSError 后跳过并计数,而不是让程序直接崩溃。
  4. 符号链接:跳过软链接,避免循环引用导致无限递归。
  5. 输出:原始字节数对人不友好,写一个 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()

四、运行步骤

  1. 把上面的代码保存为 disk_top.py,放到桌面或任意顺手的位置。
  2. 打开终端,进入文件所在目录,运行:
# 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
  1. 你会看到类似这样的输出:
正在扫描 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
...
  1. 发现某个子目录特别大?把 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 瘦身前的摸底,都是同一个套路:先量化,再动手。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。