磁盘又满了?用 Python 写个安全清理脚本:先演练、再真删,专治日志爆盘

磁盘又满了?用 Python 写个安全清理脚本:先演练、再真删,专治日志爆盘

先给第一次来的读者:这篇解决什么问题

VPS(Virtual Private Server,虚拟专用服务器)就是你租在机房里的一台远程小电脑,按月或按年付费,上面跑着博客、代理服务和各种脚本。磁盘满是最常见的"慢性病":服务每天写日志,几个月下来不知不觉就把硬盘塞满,轻则新日志写不进去、服务报错,重则连系统更新和备份都失败。

最危险的清理方式是凭感觉删:rm -rf 一行敲下去,删错数据库或配置文件就只能靠备份抢救——前提是你有备份。这篇给你一个思路相反的脚本:默认只演练不真删,先把"最大的文件是哪些、哪些日志过期了"列清楚,你确认无误后加一个 --apply 参数才真正执行。全文代码只用 Python 标准库,不用装任何第三方包,我在本地真实跑通过,输出原样贴在下面。

前置知识:一台能 SSH 登录的 Linux 服务器、会用命令行、Python 3.10 以上。清理之前先读一遍备份篇:重要数据先有备份,再谈删除。

清理前先定三条规矩

  1. 只删日志类文件:脚本只认 .log、.out、.gz 后缀,数据库、配置文件、网站数据一律不动。最大的文件不一定是能删的——下面实测里最大的就是一个非日志文件,脚本原样放过了它。
  2. 默认演练,显式确认才真删:不加 --apply 时只打印清单;真正删除必须是你主动敲下这个参数。定时任务里也要先跑一周演练,确认清单合理再上 --apply。
  3. 禁区目录直接拒绝:/、/etc、/usr、/home、/root 这类目录传进来,脚本直接返回错误,不执行任何删除。系统自带的日志目录(如 /var/log)请用系统工具 logrotate 管理,别拿这个脚本去扫。

完整代码

把下面代码存为 disk_cleaner.py:

#!/usr/bin/env python3
"""磁盘清理脚本:列出大文件 + 删除过期日志,默认只演练不真删。
用法:python3 disk_cleaner.py <目录> [--days 30] [--apply]
只用标准库,Python 3.10+。
"""
import sys
from datetime import datetime, timedelta
from pathlib import Path

# ---------- 安全护栏 ----------
FORBIDDEN = {"/", "/etc", "/usr", "/bin", "/sbin", "/boot", "/home", "/root"}
LOG_SUFFIXES = {".log", ".out", ".gz"}  # 只删这些后缀,数据库、配置文件一律不动


def human(n):
    for u in ("B", "KB", "MB", "GB"):
        if n < 1024:
            return f"{n:.1f}{u}"
        n /= 1024
    return f"{n:.1f}TB"


def main():
    if len(sys.argv) < 2:
        print("用法:python3 disk_cleaner.py <目录> [--days 30] [--apply]")
        return 2
    target = Path(sys.argv[1]).resolve()
    days = 30
    apply = False
    args = sys.argv[2:]
    if "--days" in args:
        days = int(args[args.index("--days") + 1])
    if "--apply" in args:
        apply = True
    if str(target) in FORBIDDEN or not target.is_dir():
        print(f"[拒绝] 不安全的目标目录:{target}")
        return 2
    cutoff = datetime.now() - timedelta(days=days)
    files = [p for p in target.rglob("*") if p.is_file()]
    files.sort(key=lambda p: p.stat().st_size, reverse=True)
    print(f"扫描 {target}:共 {len(files)} 个文件")
    print("最大的 5 个文件:")
    for p in files[:5]:
        print(f"  {human(p.stat().st_size):>8}  {p.relative_to(target)}")
    expired = [p for p in files
               if p.suffix in LOG_SUFFIXES
               and datetime.fromtimestamp(p.stat().st_mtime) < cutoff]
    freed = sum(p.stat().st_size for p in expired)
    print(f"超过 {days} 天未动的日志类文件:{len(expired)} 个,"
          f"共 {human(freed)}")
    for p in expired:
        print(f"  {'删除' if apply else '将删除'} {p.relative_to(target)}")
    if not apply:
        print("[演练模式] 未删除任何文件,加 --apply 才真正执行")
        return 0
    for p in expired:
        p.unlink()
    print(f"[完成] 已删除 {len(expired)} 个文件,释放约 {human(freed)}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

用到的标准库只有 pathlib(目录遍历)和 datetime(过期判断),Python 官方文档可查版本。

真实运行输出

测试目录里放了 3 个文件:40 天没动过的 old-app.log、2 天前更新的 new-app.log、以及一个 8KB 的非日志文件 big-data.bin。先跑演练模式:

扫描 /tmp/clean-test/logs:共 3 个文件
最大的 5 个文件:
     7.8KB  big-data.bin
     4.9KB  old-app.log
     2.0KB  new-app.log
超过 30 天未动的日志类文件:1 个,共 4.9KB
  将删除 old-app.log
[演练模式] 未删除任何文件,加 --apply 才真正执行

注意两点:最大的 big-data.bin 因为不是日志后缀,没有进入删除清单;2 天前的 new-app.log 因为没超过 30 天,也没动。确认清单无误后加 --apply 再跑一遍,输出最后一行变成 [完成] 已删除 1 个文件,释放约 4.9KB,事后检查目录里只剩 big-data.bin 和 new-app.log——删且只删了该删的。

三步用起来

第 1 步:把脚本存到服务器,例如 /usr/local/bin/disk_cleaner.py,先对你应用的日志目录跑演练:

python3 /usr/local/bin/disk_cleaner.py /var/log/你的应用 --days 30

把路径换成你自己的应用日志目录,不要直接指向 /var/log 或备份目录。

第 2 步:看演练清单,重点看"最大的 5 个文件"那一节——如果榜首是数据库或上传文件,说明磁盘压力不在日志,删日志治标不治本,得去处理真正的大文件。

第 3 步:清单合理后,再挂定时任务,每周日凌晨 3 点跑一次真删:

0 3 * * 0 python3 /usr/local/bin/disk_cleaner.py /var/log/你的应用 --days 30 --apply >> /var/log/disk-cleaner.log 2>&1

两个常见坑

  • 正在被写入的日志删了还会长回来:如果某个日志文件每天都在变大,说明对应的服务日志级别或轮转没配好,删只是缓刑。先给服务配好日志轮转,再用这个脚本兜底。
  • 别把备份目录当清理目标:备份文件常带 .gz 后缀,正好撞上脚本的删除规则。清理目录和备份目录必须分开,这是比脚本本身更重要的规矩。

和本系列其他文章的关系

这篇是"发现问题—解决问题"链条里的一环:先用巡检脚本发现磁盘告警,再用本篇清理过期日志,清理前按备份篇确认重要数据已有备份。三步走完,磁盘爆满这类问题基本就不会半夜找上门了。