硬盘清理工具是系统维护里最高频的需求之一。临时文件清理、重复文件查找、大目录定位,听起来很简单,但一旦这类小工具被批量生成出来,而且生成方式不是开发者从零手写的版本,风险点就完全不同了。标题里说的“都不是人写的”并不是贬义,它更像今天很多开发者的工作方式:把需求描述给 AI 编程助手,让它直接生成脚本。问题在于,硬盘清理工具的本质是“找出文件并删除”,删除一旦发生就很难回退。代码能不能跑是一回事,代码删错了能不能兜底是另一回事。
这篇文章会把“三连发”拆成三个最小场景:临时文件清理器、重复文件检测器、Top 大目录扫描器。重点不是展示某个 AI 工具多好用,而是回答三个问题:这类代码初稿有哪些通病,如何用工程手段补齐安全边界,以及生成式代码要经过哪些审查才能进入真实环境。
1. 先理解硬盘清理工具的代码,为什么需要一套安全护栏
很多人拿到磁盘快满的电脑,第一反应就是搜“硬盘清理工具”。但一个真正可用的清理脚本,并不是把“删文件”三个字翻译成代码那么简单。它本质上是一个由“枚举候选文件、判断是否可清理、执行移动或删除”组成三段式流程。三段式里的每一步都可能因为路径、权限、占用、后缀匹配宽度而产生不可逆后果。
1.1 硬盘清理的底层操作并不复杂,但不可逆
从代码层面看,硬盘清理主要做了三件事:
- 遍历目标目录,找出可能的候选文件。
- 对候选文件做规则过滤,例如后缀、大小、修改时间、文件名关键字。
- 对通过过滤的文件执行删除或归档操作。
这里的核心矛盾是:第二步过滤得再严格,也无法百分之百模拟人的判断。后缀名为.tmp的文件大部分可以清理,但可能有一个正在被某软件占用的临时文件;.bak文件通常是备份,但也可能承载着唯一一份配置历史;用户手动指定--root /home/user后,代码如果递归所有子目录,就可能碰到不该碰的项目工程目录。
删除操作天然不可逆。普通文件走回收站还能恢复,很多脚本直接用os.remove物理删除后,恢复只能依赖专业文件恢复工具,成功率很不稳定。
因此,清理工具的第一条安全原则不是“删得快”,而是“先别删”。一套稳妥的清理工具至少要包含候选清单、确认步骤、可恢复通道三部分。
1.2 AI 生成代码适合做什么,不适合直接做什么
把需求交给 AI 后,生成的结果通常语法正确、结构完整,也基本能覆盖用户描述的功能。这很容易让人放松警惕。
AI 比较擅长的是通用能力部分:
- 遍历目录的代码怎么写。
- 计算文件哈希的代码怎么写。
- 递归统计目录体积的代码怎么写。
- 命令行参数解析怎么写。
这些能力属于“常见套路”,模型训练数据里见得足够多,生成的代码可用性很高。
不适合直接照搬的是包含副作用的部分:
- 哪些目录绝对不应该进入删除范围。
- 符号链接是否会指向系统目录。
- 备份目录放在哪里,是否会再次被扫描到。
- 文件被占用或权限不足时,要不要中断整个脚本。
如果提示词只写“清理临时文件,删除超过 100MB 的文件”,AI 通常会给出最简单直接的实现。这个实现能运行,但不等于它理解了你真正想清理的范围。
可以把 AI 生成的代码当成“初稿资产”,而不能当成“生产最终品”。初稿负责把骨架搭出来,人负责补上不可逆操作的兜底逻辑。
1.3 三个工具的目标、输入与验收标准
为了让“三连发”足够具体,下面统一成三个命令行子命令。
| 工具 | 输入 | 输出 | 安全底线 |
|---|---|---|---|
| clean-temp | 根目录、后缀白名单、大小下限 | 待清理文件清单或移动结果 | 默认 dry-run,不直接物理删除 |
| find-dup | 根目录、最小文件大小 | 重复文件分组和节省空间估算 | 只输出报告,不自行删除 |
| top-dir | 根目录、Top N、最大深度 | 目录体积排行 | 对权限错误容错,不追踪符号链接 |
把三个工具放在一个工程里,而不是散成三份临时脚本,原因也很简单:清理代码需要共享同一套安全删除逻辑,否则每个脚本各写各的,很容易出现一个工具删得小心翼翼,另一个工具直接递归清空目录。
2. 三连发初稿的常见写法:能跑,但多数不敢直接执行
网上被反复生成的硬盘清理脚本,写法大多集中在几个固定模式里。这些模式足够简单,但离“可用且安全”还有明显距离。
2.1 工具一:临时文件清理初稿直接把目标目录整树删除
常见初稿写法是遍历目录里的所有内容,然后对整个文件或目录调用删除操作。
from pathlib import Path import shutil root = Path("./temp_target") for item in root.iterdir(): if item.is_dir(): shutil.rmtree(item) else: item.unlink()这段代码能跑通,但存在几个明显问题:
- 没有过滤规则,目录里的任何文件都会被动过。
- 对目录直接使用
shutil.rmtree,如果目录内部有嵌套数据,会被一次性全部删除。 - 没有 dry-run,执行前用户看不到到底删了哪些。
- 一旦删除时遇到文件占用或权限异常,脚本会中断,前面的操作也已经生效,难以恢复到中间状态。
更关键的是,这种代码读起来很“痛快”,执行起来却像一次不可回滚的批量操作。真实场景里应该先列出候选文件清单,逐条确认,再进入执行阶段。
2.2 工具二:重复文件扫描初稿对每个文件都做完整哈希
重复文件查找是典型的算法题,很多人第一反应是计算文件的 MD5 或 SHA-1,然后把哈希值相同的文件归为一组。
import hashlib from pathlib import Path hash_map = {} for file in Path(".").rglob("*"): if not file.is_file(): continue digest = hashlib.md5(file.read_bytes()).hexdigest() hash_map.setdefault(digest, []).append(file)这个初稿的问题非常具体:
file.read_bytes()会把整个文件一次性读进内存。遇到 4GB 的视频文件,内存会直接被占满。- 对每个文件都做完整文件读取,而文件数量多时,磁盘 IO 会成为瓶颈。
- 没有先用文件大小过滤。理论上,文件内容相同的文件大小必然相同。大小不同的文件不可能重复,完全没必要参与哈希计算。
- 没有处理
PermissionError和文件被占用的场景。
正确方案是“文件大小分组 + 分块哈希”的二段式判断,后面的实现会展开。
2.3 工具三:Top 目录扫描初稿没有深度限制和权限容错
大目录定位工具的逻辑是递归统计子目录体积,然后按体积从大到小排序。
import os def get_dir_size(path): total = 0 for entry in os.scandir(path): if entry.is_dir(follow_symlinks=False): total += get_dir_size(entry.path) else: total += entry.stat().st_size return total这个初稿有三个隐患:
- 无限递归。如果目录层级非常深,脚本会一直向下扫描,用户可能只是想看二级目录分布。
- 符号链接循环。即使
follow_symlinks=False避免了递归进入链接目录,有些场景下仍需要额外判断硬链接和重复挂载点。 - 权限错误没有捕获。扫描一个包含无权限子目录的根目录时,脚本会在中途抛异常,最终无法输出结果。
Top 目录工具并不需要全量精确统计每个子目录下的每一个文件。日常定位磁盘占用时,先限制扫描深度,再粗略估算顶层目录体积,已经能满足绝大多数场景。
2.4 初稿到可执行版本之间缺的是状态管理
三份初稿有一个共同短板:缺少状态管理。
一份脚本执行时,应该清楚自己处在哪个阶段:
- 枚举阶段:只收集文件信息。
- 计划阶段:输出将要处理的目标,文件数量、总体积。
- 执行阶段:已经确认需要移动或删除。
- 日志阶段:记录操作结果,方便回溯。
AI 生成的初稿通常把“枚举”和“执行”混在一段循环里,边遍历边删除。这种方式最危险,因为你在遍历过程中修改目录结构,很容易导致跳过文件、重复处理、遍历中断等意外行为。
因此,改造这三份代码的第一步,不是优化算法,而是把所有删除操作从“直接删”改成“先移动到备份目录”。
3. 先封装安全层:dry-run、软删除、路径逃逸检查
无论哪个子工具,只要涉及文件变更,都应该共享同一个安全层。设计原则可以用一句话概括:任何删除动作都要过“先备份、再确认、后清理”的流程。
3.1 把所有删除动作改成“移动到备份目录”
普通清理场景中,直接调用unlink()或rmtree()很危险。最简单有效的方案是软删除:在同一个盘符下创建一个备份目录,把筛选出的文件移动进去。这样用户还能恢复,系统也不会因为跨盘复制而长时间卡住。
import shutil from pathlib import Path from datetime import datetime class SoftDelete: def __init__(self, backup_root: str = "cleanup_backup"): self.backup_root = Path(backup_root) self.backup_root.mkdir(parents=True, exist_ok=True) def build_dest(self, src: Path) -> Path: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") candidate = self.backup_root / f"{timestamp}_{src.name}" index = 1 while candidate.exists(): candidate = self.backup_root / f"{timestamp}_{index}_{src.name}" index += 1 return candidate def move_to_backup(self, src: Path) -> Path: dest = self.build_dest(src) shutil.move(str(src), str(dest)) return dest注意,这里使用shutil.move而不是Path.rename,是因为Path.rename只能在同一文件系统内工作。如果备份目录和源目录不在同一个磁盘分区,shutil.move会先复制再删除源文件,虽然速度慢一点,但至少不会直接抛错。
移动完成后,磁盘上的文件仍然存在,只是从原位置转移到了备份目录。这符合“先给后悔机会”的设计原则。
3.2 路径与符号链接检查
AI 生成代码时,很少主动处理路径逃逸问题。如果用户传入的根目录是C:\Users\test\cleanup,里面有一个符号链接指向D:\重要资料,遍历代码一旦顺着符号链接进入目标目录,删除范围就被扩大了。
检查路径是否存在于根目录内部,需要使用resolve()获取真实路径,而不是比较字符串。
from pathlib import Path class PathPolicy: def __init__(self, root: Path): self.root = Path(root).resolve() def inside(self, target: Path) -> bool: target_resolved = Path(target).resolve() if target_resolved == self.root: return False return self.root in target_resolved.parents def is_symlink(self, target: Path) -> bool: return target.is_symlink()真实路径比较的核心逻辑是:先解析出目标文件在磁盘上的绝对真实位置,再看目标文件的父目录链里是否包含根目录。根目录内发生目录跳转时,例如/data/project/tmp中的tmp是符号链接,resolve()会把路径还原成符号链接指向的真实位置,从而发现它并不在project目录下。
3.3 命令行设计:默认不改文件
清理工具的命令行参数必须遵循一个原则:默认只输出计划,不执行变更。用户加上--apply之后,代码才执行真正的移动操作。
python disk_cleaner.py clean-temp --root ./demo --dry-run python disk_cleaner.py clean-temp --root ./demo --apply这种设计让清理工具和数据库的“事务确认”思路一致。第一次运行时,用户看到的是:
[dry-run] 发现文件: ./demo/cache/old.tmp, 大小: 1.0 MB [dry-run] 发现文件: ./demo/app.log, 大小: 2.0 MB [dry-run] 合计: 2 个文件, 3.0 MB确认无误后,再执行--apply。这样做能避免“命令一执行,文件立刻消失”的恐怖体验。
4. 三个工具的落地实现与运行验证
安全层确定后,三个子工具的实现就变成“收集候选文件”的问题。
4.1 clean-temp 子命令
临时文件清理器的核心逻辑是遍历根目录,按后缀白名单过滤,再按最小文件大小过滤。
import argparse from pathlib import Path def collect_candidates( root: Path, suffixes: tuple[str, ...], min_size: int ) -> list[Path]: root = Path(root).resolve() results = [] for file in root.rglob("*"): if not file.is_file(): continue if suffixes and file.suffix.lower() not in suffixes: continue if file.stat().st_size < min_size: continue results.append(file) return results为什么先判断is_file()?因为rglob("*")会同时拿到目录和普通文件,目录不应该进入候选清单。后续处理以文件为单位,避免递归删除目录。
后缀判断要使用lower()归一化。同一个文件扩展名在 Windows 和 Linux 上可能呈现出不同大小写,例如.TMP和.tmp,不处理会产生漏删。
min_size参数的作用是过滤掉过小文件。建议默认设置在 1MB 以上,否则清理工具会高频扫到大量几十字节的配置临时文件,输出清单很难阅读,而且容易误伤真正有用的运行时文件。
4.2 find-dup 子命令
重复文件检索采用二段式判断:
- 第一段:按文件大小分组。
- 第二段:只对同组内文件做分块哈希。
import hashlib from pathlib import Path from collections import defaultdict def file_md5(path: Path, chunk_size: int = 1024 * 1024) -> str: digest = hashlib.md5() with path.open("rb") as file_obj: while True: block = file_obj.read(chunk_size) if not block: break digest.update(block) return digest.hexdigest() def find_duplicates(root: Path, min_size: int = 0) -> dict[str, list[Path]]: by_size = defaultdict(list) root = Path(root).resolve() for file in root.rglob("*"): if not file.is_file(): continue try: size = file.stat().st_size except OSError: continue if size >= min_size: by_size[size].append(file) duplicate_groups = defaultdict(list) for files_with_same_size in by_size.values(): if len(files_with_same_size) < 2: continue for file_path in files_with_same_size: digest = file_md5(file_path) duplicate_groups[digest].append(file_path) return { digest: paths for digest, paths in duplicate_groups.items() if len(paths) > 1 }为什么要额外做try except OSError?文件可能刚好在访问前被删除或移动,也可能所在目录权限不足导致stat()失败。单文件失败不应该中断整个扫描,否则几千个文件里只要有少数几个异常,后续就没法继续了。
分块哈希把每次读取量限制在 1MB,无论文件多大,内存占用都保持稳定。对超大文件,这段逻辑只是读取更多轮次,不会把整个文件塞进内存。
4.3 top-dir 子命令
大目录扫描不需要追求绝对精确,更看重响应速度和结果可读性。因此这里用“最大深度 + 权限容错”的递归方式。
import os from pathlib import Path def dir_size(path, max_depth: int, current_depth: int = 0) -> int: total = 0 if current_depth > max_depth: return 0 try: with os.scandir(path) as entries: for entry in entries: if entry.is_dir(follow_symlinks=False): total += dir_size( entry.path, max_depth, current_depth + 1 ) elif entry.is_file(follow_symlinks=False): total += entry.stat().st_size except PermissionError: return 0 return total def top_directories(root: Path, top_n: int = 10, max_depth: int = 3) -> list[tuple[str, int]]: sorted_dirs = [] for path in Path(root).rglob("*"): if not path.is_dir(): continue if path.is_symlink(): continue size = dir_size(path, max_depth=max_depth) sorted_dirs.append((str(path), size)) sorted_dirs.sort(key=lambda item: item[1], reverse=True) return sorted_dirs[:top_n]使用follow_symlinks=False是为了不跟随符号链接。符号链接可能指向根目录之外,也可能形成目录循环,跟随会导致无限递归或越界扫描。
递归函数里,PermissionError 会被捕获并直接返回 0。这个处理思路是“缺一段统计不能毁掉整个结果”。生产环境还可以把无权限的具体路径写入日志,方便后续排查。
4.4 最小测试目录与预期输出
为了验证三个工具,可以先构造一个小型测试目录。
from pathlib import Path demo = Path("./demo") (demo / "cache").mkdir(parents=True, exist_ok=True) (demo / "cache" / "old.tmp").write_bytes(b"x" * (2 * 1024 * 1024)) (demo / "cache" / "image.bak").write_bytes(b"y" * (3 * 1024 * 1024)) (demo / "app.log").write_bytes(b"z" * (1024 * 1024)) (demo / "素材" / "video.mp4").parent.mkdir(parents=True, exist_ok=True) (demo / "素材" / "video.mkv").write_bytes(b"copy" * (1024 * 1024))运行clean-temp的 dry-run:
python disk_cleaner.py clean-temp --root ./demo --dry-run预期输出:
[dry-run] ./demo/cache/old.tmp, size=2.0 MB [dry-run] ./demo/app.log, size=1.0 MB [dry-run] 候选项: 2 files, total size=3.0 MBimage.bak因为后缀是bak且大小是 3MB,会出现在候选中;而video.mp4后缀不在默认白名单,所以不会出现。
运行find-dup:
python disk_cleaner.py find-dup --root ./demo --min-size 1如果两个video文件长度相同,代码会先按大小分组,再哈希比较。如果内容不同,则不会归进重复组。
最终确认安全清理时,可以先执行--apply查看移动结果,再从备份目录中确认文件完整性,最后再物理删除备份区。
注意:不要只验证工具能“跑起来”。真正要验证的是 dry-run 清单里的文件是否与预期一致、备份目录能否正常恢复、以及 backup 目录本身会不会被再次扫描。
5. 硬盘清理最容易踩的坑与排查顺序
硬盘清理工具运行时不报错,不代表一切正常。下面这些现象在真实环境中出现频率很高。
5.1 高频现象对照表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| dry-run 后没有任何输出 | 根目录路径错误、文件后缀不匹配、min-size 设置太大 | 检查根目录是否真实存在,打印 root.resolve() | 先用一个已知的测试文件验证 |
| 移动文件后磁盘空间没有变化 | 备份目录设在同一个磁盘分区,文件只是换了一个位置 | 查看备份目录大小、确认物理删除阶段未执行 | 真正释放空间需清理 backup 目录或使用外接硬盘备份 |
| 循环开始时刚遍历完文件又被删除 | 边遍历目录边删除,导致遍历结果不完整 | 检查代码里 rglob 和 unlink 是否在同一循环 | 先收集 candidates 列表,再单独执行软删除 |
| 清理过程遇到 PermissionError 中断 | 文件被占用或无权限,异常没有被捕获 | 查看报错堆栈指向哪个文件 | 在单文件处理上做 try except,跳过并记录 |
| 重复文件扫描特别慢 | 所有文件都做了完整哈希,没有先按文件大小过滤 | 观察 CPU 使用率和磁盘 IO | 使用“大小分组 + 分块哈希”二段式方案 |
| 脚本扫描范围越界 | 根目录内存在符号链接,指向范围外目录 | 打印 resolve() 后路径,检查备份目录真实位置 | 遍历时跳过符号链接 |
5.2 现象到根因的排查链路
硬盘清理类问题,排查顺序可以固定为“输入 → 路径 → 规则 → 权限 → 日志”。
第一步,确认输入。
清理命令敲下去之前,先确认根目录路径是不是真的存在。路径写错,后续任何逻辑都不会生效。
第二步,确认路径有没有被解析成预期位置。
在 Windows 上尤其要注意大小写、盘符和符号链接问题。代码中应使用resolve(),并在 dry-run 输出完整绝对路径,方便人工核对。
第三步,确认过滤规则。
临时文件清理必须看白名单后缀、文件大小下限和是否排除备份目录。文件没有进候选清单,大多是规则太松或太严。
第四步,确认权限和文件占用。
Windows 下常见的错误是文件正在被 Excel、Word、浏览器或系统服务占用。这类文件无法删除或移动,脚本如果直接抛出PermissionError,后面的文件就处理不了。
第五步,查看日志。
清理工具必须记录每个文件的操作结果。缺少日志时,误删文件后连“到底删了什么”都说不清。
5.3 清理同一盘上的 backup 目录为什么空间没有释放
这个问题很容易被人忽略。很多临时文件清理工具为了安全,先把目标文件移动到cleanup_backup备份目录。如果备份目录和文件原位置在同一个磁盘分区,则磁盘剩余空间不会变化。
例如用户想清理 C 盘,方案是“先把 C 盘文件移动到 C 盘 backup 目录”,这从物理空间角度没有任何效果。文件只是换了一个目录,仍然占用着 C 盘空间。
正确做法有两种:
- 把备份目录放在另一块磁盘,例如源文件在 C 盘,备份目录在 D 盘。
- 把软删除当成“临时中转站”,在人工确认备份文件有效后,再从 backup 目录执行物理删除。
第一种方式适合生产服务器和重要数据;第二种方式适合个人电脑的小规模整理。无论哪种方式,都要把“释放空间”和“文件归档”两个目标分开描述,否则用户执行完会误以为工具没有生效。
6. 让 AI 生成代码可用,需要在工程上补齐什么
回到开头的问题:既然三个工具都不是人写的,那能不能放心用?答案是:能,但前提是有一套明确的安全约束和人工审查流程。
6.1 给 AI 的提示词要把安全约束写进去
想让 AI 生成一份可用的硬盘清理脚本,提示词不能只写“帮我清理临时文件”。更稳定的做法是把功能和约束一起描述。
请生成一个 Python 命令行工具,功能如下: 1. 输入 root 路径,输出该目录下后缀为 .tmp、.log、.bak 且大小大于等于 1MB 的文件。 2. 默认执行 dry-run,只输出候选文件清单,禁止修改原文件。 3. 只有传入 --apply 后,才把候选文件移动到 backup_root 目录,不要直接物理删除。 4. backup_root 默认在 root 的同级目录,但不能被 scan 逻辑递归扫描到。 5. 遍历时跳过符号链接,避免目录越界。 6. 对每个文件单独捕获权限异常,避免单个失败中断整体任务。 7. 使用 pathlib,不使用第三方库。 8. 最后给出三个测试用例。这些约束不是为了限制 AI,而是为了把人类对“文件安全”的理解写进生成流程。AI 生成代码时更依赖提示词中的自然语言约束,约束越具体,代码越不会用暴力的方式实现删除。
6.2 人审清单和最小测试集
代码生成后不能直接进正式环境。可以按下面这份清单逐项过一遍:
- 代码中是否出现
unlink、rmtree、os.remove等直接删除调用。 - 如果出现物理删除,是否满足用户显式指定了不可逆模式。
- 遍历根目录是否使用
resolve()做真实路径锁定。 - 备份目录是否排除在扫描范围之外。
- 是否默认 dry-run,dry-run 模式下是否绝对没有任何写操作。
- 单文件权限异常时是否会中断整个任务。
- 是否输出完整操作日志,包含源路径、目标路径、时间戳。
- 是否提供了测试目录场景,而不是只在真实目录里试。
- 是否有显式参数控制文件后缀、文件大小和扫描深度。
最小测试集可以只包含三个用例:正常后缀文件、超大文件、无权限目录。正常后缀文件用于确认过滤规则,超大文件用于观察内存占用,无权限目录用于确认异常处理。
6.3 从学习环境升级到生产环境的加固项
个人电脑上跑clean-temp和学习环境里的跑法差异很大。生产环境还要额外考虑:
- 配置外置化,根目录、后缀白名单、备份目录、是否启用定时任务都放到配置文件里。
- 删除前导出 CSV 清单,包含文件路径、大小、最后修改时间、处理结果。
- 备份目录保留策略,例如保留 7 天后自动清理过期备份,或超过多少 GB 后触发告警。
- 运行账号最小权限,避免使用管理员账号执行清理任务。
- 对系统目录执行更严格的黑名单机制,例如根目录不能是
C:\Windows或 Linux 的/usr。 - 在 CI 环境增加代码扫描规则,禁止自动提交包含危险递归删除的代码。
这些项目不需要一次性全部实现,但代码进入自动清理流程前至少要有一个可用的日志和备份恢复通道。
6.4 最后说回这次“三连发”
“都不是人写的”不应该成为判断标准。判断一段代码能不能用于清理硬盘,标准只有一个:代码作者能不能为它产生的每个文件变更负责。AI 能写出不错的遍历和哈希逻辑