先别急着找清理软件。上个月我正跑着 docker build,系统突然弹了个 no space left on device,构建直接失败;再一看 C 盘,只剩 800MB。挨个翻目录才发现,系统临时文件、Windows 更新缓存、WER 日志、各种开发工具的缓存,零零碎碎加起来快 30GB。从那天起,我决定给自己写一个临时文件自动化工具。
这个工具做的事情很明确:自动扫描并删除系统临时文件、Windows 更新缓存、软件日志、无效缓存和回收站冗余文件,同时牢牢守住个人文档、照片、安装软件这些真正重要的数据;清理结束后,把释放了多少空间列成清单给你。它适合程序员的开发机、运维手上长期不重启的服务器,也适合任何觉得 C 盘突然不够用的普通用户,前提是你愿意看明白脚本做了什么。
下面就是我这次做的完整记录,包含设计思路、核心代码、计划任务接入方式,以及一些只有踩过坑才会知道的细节。
1. 需求拆解与整体设计思路
1.1 磁盘空间都去哪了:一份开发者视角的“垃圾地图”
大多数开发者对自己 C 盘空间被什么吃掉,其实是没概念的。你以为装了几个大型软件就没了,实际上真正的大头往往是散落在各个角落的临时文件和缓存。我把一台用了两年的开发机翻了一遍,典型的东西有这么几类:
| 目录 | 里面是什么 | 典型大小 | 能不能清 |
|---|---|---|---|
%TEMP%(用户临时目录) | 编辑器临时文件、安装包解压残留、各类程序运行中间产物 | 2GB ~ 10GB | 能,按时间过滤后删 |
C:\Windows\Temp | 系统级临时文件,部分旧安装程序的残留 | 1GB ~ 5GB | 能,部分需管理员权限 |
C:\Windows\SoftwareDistribution\Download | Windows 更新下载缓存,安装完就没用了 | 1GB ~ 8GB | 能,直接删内容 |
C:\ProgramData\Microsoft\Windows\WER | Windows 错误报告、崩溃转储 | 几百MB ~ 2GB | 能,基本都是无用日志 |
%LOCALAPPDATA%\Microsoft\Windows\INetCache | IE/Edge 传统缓存 | 1GB ~ 4GB | 能 |
| 各种开发者工具缓存 | 微信开发者工具编译缓存、VS Code 缓存、Chrome 缓存、npm/pip 缓存 | 5GB ~ 20GB | 需要甄别,不能一刀切 |
这里我特别想提一下开发者工具。微信开发者工具默认缓存路径在%LOCALAPPDATA%\微信开发者工具下,一次完整的小程序项目调试就能积累上 G 的编译缓存;Chrome DevTools 用久了,CacheStorage、Code Cache这些目录也会悄悄膨胀。很多同类清理软件根本不知道这些目录的存在,或者不敢动它们,所以清理效果非常有限。这也是我决定自己写工具的核心原因:开发者机器的垃圾,只有开发者自己最清楚。
1.2 工具的功能边界:哪些该删,哪些打死都不能碰
这个工具的安全底线非常明确:个人文档、照片、视频、音乐、已安装软件的可执行文件和配置、正在使用的工程文件,一律不在扫描范围之内。
为了实现这个底线,我定了规矩:
- 只清“显式列入清单”的目录,不做全盘搜索式清理。全盘扫
*.tmp看起来很厉害,但 Photoshop 的暂存文件、IDE 的 autosave、数据库的临时表空间都可能混在里面,删掉就是事故。 - 所有目标目录都走白名单机制,路径必须能被明确枚举到;不在清单里的路径即使看起来像垃圾,也宁可不删。
- 凡是位于“个人数据区”(桌面、文档、图片、下载、音乐、视频)下的内容,默认被排除。哪怕这些目录里有一个
.tmp文件,也不删,因为那可能是你刚下载到一半的资料。 - 删除操作必须满足“修改时间超过 N 天”的门槛,默认 7 天。正在活跃使用的文件会自动被年龄条件挡在门外。
这个边界的本质是:用“位置 + 特征 + 时间”三个过滤器做交集,而不是拿一把大刀把所有看起来像垃圾的东西都砍了。宁可漏掉一些可以删的文件,也不误删任何一个可能的有效数据。
1.3 为什么我不直接用现成清理软件
市面上清理软件很多,我一个都没用,不是觉得它们没用,而是有三个绕不过去的问题。
第一是黑盒。大部分清理工具扫出来的“垃圾”列表,不会告诉你每个文件具体是什么、属于哪个软件、删了之后会有什么连锁反应。遇到开发者目录,它们要么不认识直接漏掉,要么当成普通缓存给你清了,但 npm cache 其实可以靠官方命令安全清理,直接删目录也没问题;可有些软件的自定义模板目录如果被误删,下次打开直接回到出厂状态。黑盒操作不能让我放心。
第二是隐私。让我把自己的整体文档目录交给一个第三方软件扫描,我是不愿意的。它可能很规矩,但没必要冒这个险。
第三是无法自动化、无法审计。定时清理、输出结构化报告、和运维监控对接,这些需求在现成软件里基本都不好实现。自己做一个小脚本,所有行为都在代码里,任何人审一遍就知道它删了什么,这比任何宣传文案都可靠。
就这样,需求变清晰了:一个用 Python 写的、白名单机制、带 dry-run 预览、能生成报告的临时文件自动化工具。
2. 核心技术与方案选型
2.1 用 Python 而不是批处理:安全性和可维护性更重要
网上能搜到一堆win11删除临时文件bat的脚本,几行del /s /q就能把指定目录清了,看起来简单粗暴。坦白讲,我最早的第一版就是这个思路,但用了两周就放弃了。原因是批处理在“安全校验”和“统计反馈”上实在太弱:
- 没有安全的路径校验,
del /s配合一个变量不小心拼错的路径,可能把不该删的目录扫进去。 - 没有文件年龄过滤,做不到“只删 7 天前的文件”。
- 没有结构化统计,删完了你不知道释放了多少、失败了几个。
- 跨平台、扩展、写单元测试都很别扭。
Python 只需要标准库就能覆盖全部需求:pathlib处理路径、os做遍历、shutil处理清理、argparse接收参数、json导出报告。再加上 Python 3.8 以上几乎是现代开发机的标配,不引入任何第三方依赖,把.py文件丢进去就能跑。
最终我的方案是“Bat 做壳,Python 做核”:一个 5 行的.bat文件负责被任务计划程序调用,真正逻辑全部放在 Python 脚本里。这样既能在 Windows 下无感启动,又能保住安全校验和统计报告能力。
2.2 三层过滤器:位置、特征、时间
工具的安全性,核心在三层过滤器,缺一不可:
位置过滤器。要清理什么,全在CLEAN_TARGETS字典里列清楚。脚本遍历时不会跳出这些目录,每个被处理的文件最终解析出来的绝对路径,必须仍然位于允许清理的目标目录之下。同样重要的还有PROTECTED_DIRS保护名单,扫描过程中一旦发现路径落在保护名单里,立即跳过。
特征过滤器。在指定的缓存目录中,不区分扩展名直接按年龄删,是可以的;但在偏杂乱的临时目录里,我会额外跳过一些明显有“活”特征的目录名,比如node_modules、.git、ServiceWorker,避免正在用的工具链被打断。当然也可以不跳,但对我这种经常做前端构建的人来说,把这些目录从清理范围里拿走,能少很多麻烦。
时间过滤器。这一点最关键。代码里用st_mtime判断最后修改时间,只有超过min_age_days(默认 7 天)的文件才会进入删除候选。这能自动挡住绝大多数正在使用的文件,因为活跃文件通常都会被频繁修改。如果有人坚持要当天清理,也可以用--min-age 0强制放行,但我不建议你这么做,除非你刚刚关掉了所有可能占用临时文件的程序。
三层过滤器叠在一起,才真正把“保留个人文档、照片、安装软件等重要数据”从口号变成了工程实现。
2.3 清理报告:让每次操作都留下可审计的痕迹
自动化工具和手动删除最大的区别,是它必须可追溯。所以我的脚本在“删除前”和“删除后”都会做统计:
- 清理前:计算每个目标目录下有多少文件符合条件、体积多大。
- 清理中:记录成功删除了多少文件、释放了多少字节、因占用或权限失败的有哪些。
- 清理后:按目录汇总输出一张表格,并可选择导出 JSON 报告。
JSON 报告很实用,我后来把它接到了日志系统里,每周自动清理完就能看到磁盘“健康状态”的变化曲线。如果清理出现大量失败文件,也可以从报告里定位到具体路径,排查是哪个进程在占用。
3. 从零实现一个可落地的清理工具
3.1 先摸底:把目标目录的大小量化出来
写工具第一步不是写删除逻辑,而是先摸清自己机器上这些目录到底有多大。我直接用一段几行的 Python 脚本把候选目录都统计了一遍:
import os from pathlib import Path candidates = [ Path(os.environ.get("TEMP", r"C:\Windows\Temp")), Path(r"C:\Windows\Temp"), Path(r"C:\Windows\SoftwareDistribution\Download"), Path(r"C:\ProgramData\Microsoft\Windows\WER"), ] def dir_size(path): total = 0 for base, _, files in os.walk(path): for name in files: fp = Path(base) / name try: total += fp.stat().st_size except OSError: pass return total for p in candidates: if p.exists(): print(f"{p}: {dir_size(p) / 1024 / 1024:.2f} MB")这一步强烈建议做一下。很多开发者以为自己的空间被微信开发者工具缓存吃了,一跑统计发现其实是 Windows 更新缓存占了大头。先搞清楚问题在哪,再决定清理范围,效果完全不同。
3.2 核心实现:扫描、统计、删除、报告一条龙
下面这份脚本是我的简化版,完整保留了安全设计和报告逻辑。你可以直接存成clean_temp.py使用。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import argparse import json import os import subprocess import time from pathlib import Path # ---------- 配置区:按需修改 ---------- TEMP_DIRS = [ Path(os.environ.get("TEMP", r"C:\Windows\Temp")), Path(r"C:\Windows\Temp"), ] UPDATE_DIRS = [ Path(r"C:\Windows\SoftwareDistribution\Download"), ] LOG_DIRS = [ Path(r"C:\ProgramData\Microsoft\Windows\WER"), ] BROWSER_CACHE_DIRS = [ Path(os.environ.get("LOCALAPPDATA", r"C:\Users\Default\AppData\Local")) / "Microsoft" / "Windows" / "INetCache", ] CLEAN_TARGETS = { "系统临时文件": TEMP_DIRS, "Windows更新缓存": UPDATE_DIRS, "软件日志": LOG_DIRS, "浏览器缓存": BROWSER_CACHE_DIRS, } PROTECTED_DIRS = [ Path(os.environ.get("USERPROFILE", r"C:\Users\Default")) / "Desktop", Path(os.environ.get("USERPROFILE", r"C:\Users\Default")) / "Documents", Path(os.environ.get("USERPROFILE", r"C:\Users\Default")) / "Downloads", Path(os.environ.get("USERPROFILE", r"C:\Users\Default")) / "Pictures", Path(os.environ.get("USERPROFILE", r"C:\Users\Default")) / "Music", Path(os.environ.get("USERPROFILE", r"C:\Users\Default")) / "Videos", ] SKIP_NAMES = {"node_modules", ".git", "ServiceWorker", "TempState"} MIN_AGE_DAYS = 7 # ---------- 工具函数 ---------- def is_protected(path: Path) -> bool: p = path.resolve() for base in PROTECTED_DIRS: try: p.relative_to(base.resolve()) return True except ValueError: continue return False def is_old_enough(path: Path, min_age_days: int) -> bool: try: mtime = path.stat().st_mtime except OSError: return False return (time.time() - mtime) >= min_age_days * 86400 def clean_dir(root: Path, min_age_days: int, dry_run: bool = False): """递归清理目录内符合条件的文件,不会删除 root 本身。""" stats = {"deleted": 0, "bytes_freed": 0, "failed": 0, "skipped": 0} if not root.exists(): return stats try: for item in root.iterdir(): if item.name in SKIP_NAMES: stats["skipped"] += 1 continue if is_protected(item): stats["skipped"] += 1 continue if item.is_dir(): sub_stats = clean_dir(item, min_age_days, dry_run) for k in stats: stats[k] += sub_stats[k] elif item.is_file() or item.is_symlink(): if not is_old_enough(item, min_age_days): stats["skipped"] += 1 continue try: size = item.stat().st_size except OSError: size = 0 if dry_run: stats["deleted"] += 1 stats["bytes_freed"] += size continue try: item.unlink() stats["deleted"] += 1 stats["bytes_freed"] += size except (PermissionError, OSError): stats["failed"] += 1 except (PermissionError, OSError): stats["failed"] += 1 return stats def clear_recycle_bin() -> bool: try: subprocess.run( ["powershell", "-NoProfile", "-Command", "Clear-RecycleBin -Force -ErrorAction SilentlyContinue"], check=False, capture_output=True, timeout=60, ) return True except Exception: return False # ---------- 主流程 ---------- def main(): parser = argparse.ArgumentParser(description="开发者临时文件自动化清理工具") parser.add_argument("--dry-run", action="store_true", help="只统计不删除") parser.add_argument("--yes", action="store_true", help="跳过二次确认") parser.add_argument("--min-age", type=int, default=MIN_AGE_DAYS, help="只删修改时间超过该天数的文件") parser.add_argument("--recycle-bin", action="store_true", help="同时清空回收站") parser.add_argument("--json-report", type=str, help="导出 JSON 报告到指定文件") args = parser.parse_args() total_deleted = total_freed = total_failed = 0 report = [] print(f"运行模式: {'只预览(dry-run)' if args.dry_run else '正式清理'}") print(f"年龄门槛: 修改时间超过 {args.min_age} 天的文件\n") for category, dirs in CLEAN_TARGETS.items(): for d in dirs: if not d.exists(): continue stats = clean_dir(d, args.min_age, dry_run=args.dry_run) freed_mb = stats["bytes_freed"] / 1024 / 1024 print(f"[{category}] {d}") print(f" 可删/已删 {stats['deleted']} 个文件,释放 {freed_mb:.2f} MB," f"跳过 {stats['skipped']} 个,失败 {stats['failed']} 个") total_deleted += stats["deleted"] total_freed += stats["bytes_freed"] total_failed += stats["failed"] report.append({ "category": category, "dir": str(d), "deleted": stats["deleted"], "bytes_freed": stats["bytes_freed"], "failed": stats["failed"], }) if args.recycle_bin: if not args.dry_run: ok = clear_recycle_bin() print(f"回收站清理: {'成功' if ok else '失败或为空'}") else: print("回收站清理: 已跳过(dry-run模式)") if not args.dry_run and not args.yes: confirm = input("确认执行上面列出文件的删除操作? 输入 yes 继续: ") if confirm.strip().lower() != "yes": print("已取消") return print(f"\n清理结果: {total_deleted} 个文件,共释放 {total_freed / 1024 / 1024:.2f} MB," f"失败 {total_failed} 个") if args.json_report: with open(args.json_report, "w", encoding="utf-8") as f: json.dump({"total_deleted": total_deleted, "total_freed": total_freed, "items": report}, f, ensure_ascii=False, indent=2) if __name__ == "__main__": main()这个脚本有几个设计细节需要解释一下。
clean_dir是递归处理目录内子项的,但不会删除root本身。对C:\Windows\SoftwareDistribution\Download这样的目录,我们只清理它里面的东西,目录结构保留,避免因为某个服务还在引用目录句柄导致清理失败或后续异常。另外我没有急着删除空目录,因为空目录不占空间,留着也不会影响后续使用,还能减少一些软件重新创建目录的麻烦。
SKIP_NAMES里我放了node_modules和.git。虽然理论上它们出现在这些临时目录里的可能性很低,但一旦出现,基本都是开发中的工作产物,不该由清理工具顺手带走。这个集合可以根据个人需求增减,比如你不想保留某类浏览器插件缓存,也可以在这里加名字;反过来,如果你确认某个目录里全是垃圾,也可以把它从集合中移除。
回收站清理用的是 PowerShell 的Clear-RecycleBin,在 Python 里通过subprocess调用。我特意加了-ErrorAction SilentlyContinue,这样即使回收站为空,也不会报错打断执行。注意这个功能默认是关闭的,只有显式传--recycle-bin才会执行,原因后面会细说。
3.3 首次运行:先 dry-run,再正式清理
我第一次跑这个工具,用的命令是:
python clean_temp.py --dry-run --min-age 7输出大概长这样:
运行模式: 只预览(dry-run) 年龄门槛: 修改时间超过 7 天的文件 [系统临时文件] C:\Users\admin\AppData\Local\Temp 可删 3845 个文件,释放 1234.56 MB,跳过 128 个,失败 0 个 [Windows更新缓存] C:\Windows\SoftwareDistribution\Download 可删 42 个文件,释放 1876.21 MB,跳过 0 个,失败 0 个 [软件日志] C:\ProgramData\Microsoft\Windows\WER 可删 156 个文件,释放 342.10 MB,跳过 5 个,失败 0 个 [浏览器缓存] C:\Users\admin\AppData\Local\Microsoft\Windows\INetCache 可删 890 个文件,释放 210.43 MB,跳过 76 个,失败 0 个 清理结果: 4933 个文件,共释放 3663.30 MB,失败 0 个这时候其实什么都还没删,列出的都是“符合删除条件”的文件。我建议第一次使用一定把这个流程走一遍:先看清楚哪些目录贡献最大、哪些文件会被选中,心里有数之后再用--yes去执行正式清理。这种“先预览后执行”的思路,同样适合接进自动化流程,每天跑一次预览并把结果存日志,只有磁盘超过某个阈值时才触发真正的清理,调度上会更安全。
3.4 接入计划任务,实现真正的无人值守自动化
脚本有了,接下来就是接入 Windows 任务计划程序。我用一个 5 行的批处理文件做壳,把 Python 的调用和日志重定向都封装起来。
run_cleaner.bat内容如下:
@echo off cd /d C:\tools\ python clean_temp.py --yes --min-age 7 --json-report C:\tools\clean_report.json >> C:\tools\clean_history.log 2>&1注意--json-report每次会覆盖同一个文件,如果你要保留历史记录,可以在 Python 里给文件名拼上日期,比如report_20250101.json。
然后用schtasks注册每周任务:
schtasks /Create /TN "DevTempCleaner" /TR "C:\tools\run_cleaner.bat" /SC WEEKLY /D SUNDAY /ST 03:00 /RU SYSTEM /RL HIGHEST /F这里有几个点需要重点说明。
第一,任务跑在凌晨 3 点,是因为这个时间开发机大概率没有人在用,被占用的文件最少,清理成功率最高。我试过在下午 4 点跑,结果一半以上的文件都因为被 IDE、浏览器、更新服务占用而失败,体验很差。
第二,/RU SYSTEM意味着以系统权限运行,能拿到比管理员更高的权限,可以清理更多系统目录。伴随高权限的是更高风险,所以我在任务计划里先跑了两周 dry-run,确认输出无误后才改成正式清理模式。
第三,日志重定向很重要。>> C:\tools\clean_history.log会把每次运行的结果追加到同一个日志文件,一旦之后发现磁盘异常,可以回查这个日志定位问题。
4. 常见问题与排查技巧实录
4.1 文件被占用删不掉,怎么办
走进度条的时候最烦的就是一串PermissionError,清理结果里显示一堆“失败”。我遇到的情况分三类:
- 微信开发者工具或 VS Code 开着,相关的临时文件还在被进程盯住。
- Windows 更新服务正在后台下载或安装,
SoftwareDistribution\Download目录里的文件被锁。 - 浏览器没关干净,
INetCache里的部分缓存文件被后台进程占用。
我的处理方法是:不硬删,先跳过,等进程结束再补一轮。具体到脚本里,失败的文件会记录在 JSON 报告里。之后如果想专门补删,可以用 Process Explorer 搜索文件路径对应的句柄,关掉占用的进程后单独对那个目录再跑一次清理。
另外,早晨刚开机的时候跑清理,成功率最高。因为启动项还没全部加载,更新服务也还没进入工作状态。
4.2 误删风险:三层保险千万别省
清理工具最重要的不是效率,是安全。我建议所有人在正式自动化之前,把这三层保险都做满:
- 一层保险:位置白名单 +
PROTECTED_DIRS。这已经在代码里强制实现,不需要你操作,但你要自己审一遍配置,确认保护目录覆盖了你所有重要的文件夹。 - 二层保险:年龄过滤。默认 7 天是经验值。对于临时目录来说,7 天没有动过的文件,基本可以确定不会再用了。
- 三层保险:先用 dry-run 观察。连续跑两轮 dry-run,看输出结果有没有出现你眼熟的路径,确认没有异常再加
--yes。
我早期第一版脚本没有做年龄过滤,结果把 IDE 正在写的一个 autosave 临时文件删了,虽然当时功能没崩溃,但丢失了几次撤销记录,从那以后我把年龄过滤写死成了默认值。这种教训,遇到一次就够了。
4.3 清理后空间没有明显释放,问题出在哪
有段时间我清理完发现只释放了 2GB,但磁盘可用空间没怎么涨,排查下来有三个原因最典型。
第一个原因是正在运行的进程持有文件,大量文件被跳过,计划任务里可以看到failed数量居高不下。解决办法前面说过,调整运行时间,或者分批清理。
第二个原因是磁盘空间的大头根本不是临时文件,而是休眠文件hiberfil.sys和虚拟内存pagefile.sys。这两个文件动辄 4GB 到 16GB,不属于临时文件,我的工具不会碰它们。如果你确认不依赖休眠功能,在管理员命令行执行powercfg /h off可以直接关闭休眠并回收对应空间;虚拟内存则建议保持系统管理,不要轻易动。
第三个原因是开发环境自带的巨型缓存,比如node_modules、npm cache、Docker 镜像。这些应该用各自的官方命令处理,而不是靠临时文件工具:npm cache clean --force、pnpm store prune、docker system prune -a。我的脚本特意把这些目录放进SKIP_NAMES,就是防止开发者乱删导致构建环境损坏。
4.4 权限不足与“管理员模式”的正确打开方式
普通用户权限跑这个脚本,能清理掉%TEMP%下绝大多数文件和大部分浏览器缓存,但在C:\Windows\Temp、SoftwareDistribution\Download目录会遇到权限拒绝。两个解决路径:
- 直接右键以管理员身份运行
run_cleaner.bat,适合手动清理。 - 用前面
schtasks创建/RU SYSTEM的计划任务,适合无人值守。
我个人的建议是:开发机上日常清理用普通权限就够了,系统临时目录和更新缓存可以留到固定维护时间用管理员权限跑一次。频繁用 SYSTEM 权限跑清理脚本,万一脚本有 bug,权限越高坏的影响越大。
最后再分享两个小技巧
这个脚本我实际用了快两个月,稳定性不错。我最常用的是以下两个扩展思路,供你参考。
第一,JSON 报告可以接进自己的监控或日志系统。每周任务跑完,把clean_report.json里的total_freed字段提取出来,你就能看到磁盘空间释放的历史趋势;如果某周释放量突然大幅下降,往往意味着有些进程异常占用文件,值得去检查。
第二,把 dry-run 变成日常“健康巡检”。我在周日凌晨的计划任务里跑了两个任务:一个--dry-run输出报告但不删,一个--yes在 dry-run 成功后才执行正式清理。这样即使脚本配置将来被改动,自动化流程里也永远有一道预览闸门挡在前面。让自己的清理工具先憋一周,再用一周,你会发现“自动化”不是越快越好,而是越稳越好。