给定时清理脚本加一个安全网:Python 里的 dry-run、日志与异常兜底
几乎每个开发者都写过清理脚本:删掉过期的日志、压缩包、临时缓存。脚本本身只有十几行,真正难的是让它在无人值守的情况下也不闯祸——删错一个目录,损失可能比它省下的磁盘空间大得多。本文把我给清理脚本加的几层“安全网”整理出来,全部只用 Python 标准库,不依赖任何第三方包。
一、先想清楚三件事再动手
动手写删除逻辑之前,先在纸上回答三个问题:
- 删什么:只删某个明确的目录下的某几类文件,例如
logs/*.log和tmp/*.zip,不要用模糊的通配去扫整个家目录。 - 留多久:按修改时间算保留期,比如 30 天。时间口径要写进日志里,否则事后没法复核。
- 出错怎么办:单个文件删不掉时,是跳过继续,还是立刻停下?我的选择是:跳过并记录,最后汇总退出码,让调用方知道“这次并不干净”。
这三点定下来,脚本的结构就自然有了:先收集候选文件,再逐个判断,最后统一汇报。
二、默认 dry-run:先看清单,再真正删
清理脚本最危险的默认值,是“一运行就删”。我习惯把删除做成需要显式开启的动作:默认只打印将要删除的清单,只有加上 --apply 才会真正执行。
import argparse
parser = argparse.ArgumentParser(description="清理过期文件")
parser.add_argument("--target", required=True, help="要清理的目录")
parser.add_argument("--days", type=int, default=30, help="保留天数")
parser.add_argument("--apply", action="store_true", help="真正执行删除,不加则只预览")
args = parser.parse_args()
这样每次改完保留规则,都可以先跑一次预览,核对清单里有没有不该出现的文件名。定时任务里第一次上线,也先用预览模式跑一周,确认清单稳定后再打开 --apply。
三、只删自己认得的文件
光靠后缀名还不够。我的做法是三重限定:
- 路径限定:只处理
--target目录本身,不跟随符号链接往外走,遇到链接直接跳过。 - 类型限定:只处理普通文件,目录、套接字、设备文件一律不动。
- 时间限定:用文件的修改时间
st_mtime和当前时间比较,超过保留期才进入候选。
from pathlib import Path
from datetime import datetime, timedelta, timezone
def candidates(target: Path, days: int):
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
for item in target.iterdir():
if item.is_symlink() or not item.is_file():
continue
mtime = datetime.fromtimestamp(item.stat().st_mtime, tz=timezone.utc)
if mtime < cutoff:
yield item
注意这里用带时区的 datetime 做比较,避免本地时区和夏令时带来的边界误差。保留期的边界值(恰好 30 天整的文件)要留出余量:宁可多留一天,也不要在边界上误删。
四、日志要能讲清楚一件事的来龙去脉
清理脚本的日志不是写给人看的热闹,而是事后复核的证据。每条记录至少包含四样东西:时间、文件路径、文件大小、处理结果。
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.FileHandler("cleanup.log", encoding="utf-8"),
logging.StreamHandler()],
)
跑完之后再写一行汇总:“本次扫描 128 个文件,候选 17 个,成功删除 16 个,失败 1 个,释放约 320 MB”。有了这行汇总,下次排查时不用逐行翻日志,先看汇总就能定位到哪一天开始出问题。
五、异常不要悄悄吞掉,也不要炸掉整批
删除操作最常见的失败是权限不足和文件正被占用。正确的做法是分开处理:
- 可预期的失败(权限、文件不存在):记一条警告,继续处理下一个文件。
- 不可预期的失败(磁盘错误、路径突然消失):同样先记录下来,但要在汇总里抬高退出码。
- 绝不能做的事:用空的
except: pass把所有异常都吞掉,等于主动关掉报警器。
failures = []
for item in candidates(target, args.days):
try:
if args.apply:
item.unlink()
logging.info("已删除 %s", item)
else:
logging.info("[预览] 将删除 %s", item)
except OSError as exc:
failures.append(item)
logging.warning("删除失败 %s:%s", item, exc)
if failures:
logging.warning("本次有 %d 个文件删除失败", len(failures))
raise SystemExit(2)
退出码 2 是一个约定:调用方(定时任务或监控)看到非零退出码就知道要人工看一眼,而不是默认“一切正常”。
六、上线前的复核清单
每次把清理脚本交给定时任务之前,我会按这张清单过一遍:
- 预览模式跑一次,清单里没有系统目录、没有正在用的配置文件。
- 保留期是从业务上定的天数,而不是随手写的数字。
- 日志文件本身不在被清理的目录里,否则等于自己删自己的证据。
- 定时任务的执行用户只有目标目录的写权限,没有对外的写权限。
- 第一次真正删除选择业务低峰期,并且有人在场。
小结
清理脚本的价值不在于删得多快,而在于删得可控。默认 dry-run、路径与类型双重限定、带汇总的日志、以及不吞异常的退出码,这四层网叠加之后,脚本就算半夜自己跑,我也能在第二天早上从日志里把每一笔账对清楚。工具可以很简单,纪律不能省——这大概是所有无人值守脚本共同的道理。
- 点赞
- 收藏
- 关注作者
评论(0)