1. 磁盘告警背后的真实场景:为什么就是需要一个按天数清理的脚本
先讲一个实际场景。某台测试服务器上跑着几个服务,日志文件每天都写,偶尔还要放一些临时备份包和导出的数据文件。一开始没人在意,直到某天监控弹出一条磁盘空间不足的告警,登录一看,日志目录已经占了三十多个G,备份目录里还躺着半年前的压缩包。手动清理花了大半天,删完之后没多久,新日志又慢慢把磁盘重新填满。问题不是"这一波没清干净",而是没有一个能按固定节奏自动删除过期文件的机制。
这时候,"win linux批处理删除指定N天前文件夹的文件"就成了一个很实际的运维需求。所谓批处理,本质上就是把一条删除命令包装成脚本,由系统定时触发,自动把目录下超过N天的文件清理掉。Windows侧常用的工具是forfiles,配合.bat脚本;Linux侧则几乎离不开find命令的-mtime参数。两者解决的问题完全一致,但语法和边界条件差异很大,不能直接平移。
为什么强调"按天数"而不是"按大小"?因为很多业务场景里,日志和临时文件的生成速度并不均匀。比如月初导出数据、月底做报表,文件可能几天内暴涨一波;又比如某个服务平时日志量很小,出问题时一小时就能写出好几个G。如果只按目录总大小来清理,容易误伤还在使用的近期文件;而按时间清理,规则简单、可预期,只要定好保留周期,脚本每次执行的结果都稳定可复现。按天清理天然适合日志、临时文件、过期备份、缓存文件这类"越旧越没用"的数据。
在展开两侧脚本之前,先说清楚适用范围。这套方案适合以下场景:业务产生的文件按日期自然累积,旧文件的价值随时间递减,删除之后不需要可回滚。比如应用日志、nginx访问日志、系统临时文件、定期导出的中间数据。如果文件本身需要长期留档审计,或者删除操作涉及合规追溯,那应该考虑归档和冷存储,而不是直接删。
另一个前提是:清理范围必须能界定清楚。脚本删的是"某个目录下的、修改时间早于N天前的、非目录类型的文件"。如果目录里既有重要配置又有临时文件,一个暴力删除脚本会很危险。所以下面所有实现里,我都坚持加上文件类型过滤、时间过滤、以及先模拟后执行的步骤,这些不是多余的防御,而是脚本能长期跑下去的底线。
2. Windows侧落地:forfiles批处理的参数拆解与可直接使用的脚本
2.1 forfiles命令和它的核心参数
Windows从XP时代开始就自带forfiles命令,专门用于对指定目录中符合条件的文件执行操作。和find、del的原始组合相比,forfiles最大的价值是内置了日期过滤,不需要自己写复杂的日期比较逻辑。这里先把它最常用的参数拆开看。
| 参数 | 作用 | 示例 |
|---|---|---|
| /P | 指定起始目录,默认是当前工作目录 | /P "C:\Logs\App" |
| /S | 递归搜索子目录 | 不加/S就只处理当前目录 |
| /M | 文件匹配模式,相当于通配符 | /M *.log 或 /M. |
| /D | 日期筛选,核心参数 | /D +30 表示30天前或更早 |
| /C | 对每个匹配文件执行的命令 | /C "cmd /c del /f /q @path" |
/D参数有三种写法需要理解透彻:+N表示"N天以前或更早",即删除至少N天前修改的文件;-N表示"N天以内",即保留最近N天,常用在反向筛选场景;0表示今天。实际清理时我们用+D。有个容易混淆的地方:Windows下forfiles判断的是文件的最后修改日期,不是创建日期也不是访问日期。日志文件一旦写入内容,修改时间就是"最后写入"的时间,对清理场景来说是合理的基准。
/C参数里可以使用几个内置变量:@file表示文件名,@path表示完整路径,@fdate表示文件日期,@isdir表示是否为目录。删除文件时,一个经典动作是先判断@isdir,避免把目录也删掉。因为forfiles的递归搜索模式下,/M *.*会同时匹配到目录本身,如果不加判断,del命令虽然删不了目录,但整个脚本的行为会变得不可控。
2.2 一套可直接落地的Windows清理脚本
下面这一份脚本是我在Windows Server环境下长期使用的基础模板。它做的事情很明确:检查目标目录是否存在,用forfiles递归查找N天前的所有文件并删除,同时把操作记录写入日志。
@echo off setlocal enabledelayedexpansion set "TARGET_DIR=C:\Logs\AppLogs" set "KEEP_DAYS=30" set "LOG_FILE=D:\Scripts\Logs\cleanup.log" if not exist "%TARGET_DIR%" ( echo [%date% %time%] target directory not exist: %TARGET_DIR% >> "%LOG_FILE%" exit /b 0 ) echo [%date% %time%] start cleanup in %TARGET_DIR% >> "%LOG_FILE%" forfiles /P "%TARGET_DIR%" /S /M *.* /D +%KEEP_DAYS% /C "cmd /c if @isdir==FALSE del /f /q @path" echo [%date% %time%] cleanup finished >> "%LOG_FILE%" endlocal几个细节说明一下。setlocal enabledelayedexpansion不是必选项,但在脚本里如果有变量嵌套或循环,建议加上,避免批处理的变量展开时机产生意外。if @isdir==FALSE这一行是防止误删目录的关键,forfiles的/C命令里写的是字面量@isdir,forfiles执行时会替换成TRUE或FALSE,大小写不需要担心。del命令加了/f强制删除只读文件,加了/q安静模式不弹确认框。日志记录用%date% %time%虽然格式受系统区域设置影响,但做运维排查时能看出大概时间就够用。
如果希望把日志按天分开,避免日志文件无限变大,可以在LOG_FILE里加入日期变量。批处理里直接拼接日期容易踩格式坑,一个稳妥办法是用wmic或PowerShell动态生成标准日期字符串。
for /f "tokens=2 delims==" %%I in ('wmic OS Get localdatetime /value') do set "dt=%%I" set "yesterday=%dt:~0,8%" set "LOG_FILE=D:\Scripts\Logs\cleanup_%yesterday%.log"这种方式生成的是YYYYMMDD格式,适合做日志文件的文件名。第一次用的时候可以先跑一次,打印出最终生成的LOG_FILE路径看看是否符合预期。
2.3 用PowerShell实现的同类脚本
如果你的服务器装了Windows Management Framework,用PowerShell做同样的事情会更顺手。尤其当目标目录数量多、需要更多条件判断时,PowerShell的管道和对象模型比批处理优雅得多。
$targetDir = "C:\Logs\AppLogs" $keepDays = 30 $cutoff = (Get-Date).AddDays(-$keepDays) Get-ChildItem -Path $targetDir -Recurse -File | Where-Object { $_.LastWriteTime -le $cutoff } | Remove-Item -Force -ErrorAction SilentlyContinueGet-ChildItem -File这个参数已经保证了只取文件,不会把文件夹对象传给Remove-Item。$cutoff用当前时间减去保留天数得到边界时间,任何LastWriteTime早于这个边界的文件都会被删除。这套逻辑更易读,也便于扩展:比如想排除某些特定文件名,在Where-Object里加条件就行。
PowerShell还可以把删除的文件信息追加到日志:
Get-ChildItem -Path $targetDir -Recurse -File | Where-Object { $_.LastWriteTime -le $cutoff } | ForEach-Object { Add-Content -Path "D:\Scripts\Logs\cleanup.log" -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') deleted $($_.FullName)" Remove-Item -Path $_.FullName -Force }2.4 Windows路径反斜杠、网络路径与权限问题
Windows侧常见的坑集中在三个地方。第一是路径结尾的反斜杠。forfiles对/P "C:\Logs\"和/P "C:\Logs"的处理存在差异,有时会在拼接路径时出现双反斜杠。稳妥做法是写路径时统一不带末尾反斜杠,再用引号包住。第二是权限,如果目标目录里有其他进程正在占用的文件,del /f会删除失败,脚本不会报错也不会中断,结果就是日志和实际效果对不上。建议脚本执行结束后,再用一个简单的统计指令验证文件数量变化是否符合预期。
第三是网络共享路径。forfiles对UNC路径的支持需要额外注意,不要把网络路径直接裸奔在任务计划里。如果必须清理共享目录,建议先把共享映射为本地盘符,或者用PowerShell配合New-PSDrive来访问,否则权限和路径解析很容易出问题。此外,%date%在中文Windows下会带"星期"字样,如果拼进文件名或日志里可能引入意外的字符,这也是我不建议直接用%date%做日期的原因。
3. Linux侧落地:find -mtime的正确打开方式与三种删除姿势
3.1 find的-mtime参数与基础写法
Linux下最常用的命令是find。它的-mtime参数按"文件内容最后被修改的时间"过滤,单位是天。-mtime +30表示修改时间距今超过30乘24小时,也就是31天以前;-mtime -30表示30天以内;-mtime 30表示恰好30天前的那24小时内。和Windows forfiles直观的"D +30"相比,find的+N边界需要仔细体会,很多人第一次用都会栽在这里。
一个基础的安全写法:
find /var/log/app -type f -mtime +30 -delete这条命令的意思:在/var/log/app目录下,找到所有类型为普通文件的条目,且修改时间超过30天,直接删除。-type f是不可省略的保护条件,没有它,find也会把目录当候选对象,-delete在删除目录时行为会变得危险,尤其是空目录会直接被删掉。除非你确实希望清理空目录,否则一律加上-type f。
如果需要限定文件后缀名,用-name条件:
find /var/log/app -type f -name "*.log" -mtime +30 -delete服务器里往往混着log、tmp、zip各种类型,加一个-name模式可以缩小清理范围。
3.2 三种删除方式对比:-delete、-exec rm、xargs
同样能实现删除,但不同写法在处理文件名空格、日志输出、批量执行上有差异。我把三种方式的特点列了出来。
| 方式 | 命令特点 | 适用场景 |
|---|---|---|
| -delete | 查找完成后直接删除,最简洁 | 文件名规范、不需要记录日志的小规模清理 |
| -exec rm -f {} ; | 每找到一个文件执行一次rm,可控制 | 需要按文件逐个处理、可追加日志,但性能较差 |
| -print0 + xargs -0 | 批量传参给rm,性能最好,安全处理空格 | 文件数量大、文件名可能含空格或特殊字符 |
-exec与xargs里最典型的坑是文件名带空格。如果直接写find ... | xargs rm,xargs默认按空白分隔,遇到my log file.log会被拆成多个参数,导致rm找不到文件。正确姿势是用-print0输出以空字符分隔的文件列表,xargs再用-0接收,这一组配合可以安全处理任意文件名。
find /var/log/app -type f -mtime +30 -print0 | xargs -0 rm -f如果文件数量非常庞大,比如几十万个小文件,-exec rm -f {} \;的性能瓶颈很明显,每删一个文件就要起一个rm进程。此时xargs批处理模式更合适,可以一次把大量文件路径传给rm,进程启动开销小很多。
3.3 先模拟后真删:清理脚本的安全测试方法
我写任何清理脚本的第一原则都是:先看再删。find命令提供了一个绝佳的测试方式——把删除动作去掉,只打印匹配到的文件列表。
find /var/log/app -type f -mtime +30 -print输出结果里的每一行都是一个将被删除的文件。先跑一次模拟,人工扫一眼文件列表,确认没有"看起来不该删"的文件,然后再把-print替换成-delete,或者重新执行加了删除动作的完整命令。这套习惯花不了多少时间,但能避免大多数误删事故。还有一个实用性更强的方法:先用交互式脚本,把删除文件逐个写到日志,再执行rm。
#!/bin/bash TARGET_DIR="/var/log/app" KEEP_DAYS=30 LOG_FILE="/var/log/cleanup.log" find "$TARGET_DIR" -type f -mtime +"$KEEP_DAYS" | while read -r file; do echo "$(date '+%Y-%m-%d %H:%M:%S') deleting $file" >> "$LOG_FILE" rm -f "$file" done这里用while read循环的好处是每删一个文件都能记日志。文件数量不大时完全够用。注意while read -r file配合find输出时,如果文件名本身包含换行符会出问题,不过日志文件名一般不会出现这种极端情况,真遇到再说。文件量大时就不建议这种逐行循环了,直接xargs配合日志重定向更高效。
3.4 软链接、受限目录与特殊目录的坑
find默认不跟随符号链接。也就是说,如果/var/log/app/link_dir是一个指向其他位置的软链接,find不会进入这个目录去搜索和删除,这是好事,避免误删链接指向的真实数据。但如果清理目标恰恰需要通过软链接进入另一个目录,需要在find加-L或显式指定路径,不过我不推荐在清理场景里这么做,风险大于收益。
删除空目录是另一个容易忽略的点。很多场景下,文件删完了,目录还留着,虽然不影响业务,但目录层级会越来越乱。如果想顺手清掉N天前产生的空目录,要加上-depth和-empty条件,并且用-mindepth 1避免把起始目录本身也当成空目录删掉。
find /var/log/app -type d -empty -mtime +30 -depth -delete注意-depth让find先处理子目录再处理父目录,这样删空目录时可以从里往外删干净。-mindepth 1的作用是排除起始目录本身,如果不加,find可能把/var/log/app当空目录删掉。这两条组合是我清理空目录时的标准配置。
4. 时间起点与误删防线:N天怎么界定,哪些文件必须手下留情
4.1 Windows和Linux在"N天前"定义上的差异
同样是"删除30天前的文件",Windows和Linux的判定边界其实并不完全一致。forfiles的/D +30按"日期"比较,匹配的是最后修改日期早于或等于当前日期往前推30天那天的文件,边界上刚好是30天整的文件也会被删。find的-mtime +30则按"24小时乘以天数"精确计算,文件修改时间距今大于30个24小时才匹配,所以严格来说,"30天零1秒之前"的文件才会被删除,刚满30天整的文件不会被删。
这个差异在大多数业务场景里不会产生明显影响,但你心里得有数。如果业务方要求"至少保留30天",Windows的forfiles可能把满30天的文件删掉,而Linux的find会多留24小时窗口。反过来,如果要求"只保留最近30天",两边都需要再调一天,比如Windows用/D +31,Linux用-mtime +31,避免边界处误判。
另一个需要注意的时间维度是文件的时间戳类型。find -mtime看内容修改时间;-atime看访问时间;-ctime看状态变更时间。Windows的forfiles默认看最后修改时间,PowerShell里可以用LastWriteTime。清理临时文件时,按修改时间最合理。如果某些业务文件的修改时间长期不变、但访问很频繁,按atime清理则更贴近"长期未使用"的定义。
4.2 需要排除保护的文件类型与做法
一套清理脚本如果只按时间一刀切,迟早会出事。我见过有人在备份目录里混放了需要长期保存的配置文件,结果被按天清理的脚本全部误删。防止误删有两种思路:一是把备份目录分成不同的子目录,按目录分别设定保留周期;二是在find命令里用! -name排除关键文件。
find /var/log/app -type f -mtime +30 ! -name "*.conf" ! -name "*.key" -delete用法可以组合多次! -name,也可以写一个包含排除模式的开关。Windows的forfiles同样可以配合嵌套if实现排除,但语法更绕,不如直接用PowerShell的Where-Object来得直观:
Get-ChildItem -Path $targetDir -Recurse -File | Where-Object { $_.LastWriteTime -le $cutoff -and $_.Extension -notin @(".conf", ".key", ".pem") } | Remove-Item -Force在代码里明确写出"不删什么",比把希望寄托在目录规划上可靠一些。如果目录结构本身就很混乱,最好的做法是先和业务方确认哪些子目录必须保留,然后把它们显式列进exclude配置。
4.3 空目录与特殊文件名的处理
前面提到Linux下可以用-empty -delete清理空目录,Windows下PowerShell也有对应写法:先按LastWriteTime过滤目录,再判断是否为空,最后删除。但空目录的删除有一个前置条件:目录里不能有被排除保护的文件。如果某个空目录在排除规则中明确要求保留,那它就不该被清理,所以目录清理策略要单独设计,别把"清理旧文件"和"清理旧目录"混在一个规则里。
特殊文件名方面,Windows的批处理对!和^等符号比较敏感,建议文件名里尽量不要出现这些字符,或者用PowerShell代替批处理。Linux下文件名里的空格、引号、括号只要用-print0和xargs -0就能稳定处理。真正让我头疼的是文件名里的换行符,一旦出现,连while read都会出问题,这种文件我一般直接手工处理,不放进自动化脚本。
5. 让清理自动化:Windows任务计划与Linux cron的配置细节
5.1 Windows任务计划:权限、起始于、以最高权限运行
脚本写好了,剩下的问题是怎么让它按时自动执行。Windows下用"任务计划程序"创建基本任务,这里有几个关键选项,配置错了脚本跑不起来或者跑得不对。
- "常规"选项卡里勾选"使用最高权限运行":清理动作经常需要删除其他账户创建的文件,权限不够会静默失败。
- "操作"选项卡里,程序填
cmd.exe或直接填批处理文件路径,但"起始于"目录必须填脚本所在目录。批处理文件里如果用了相对路径,起始于不对会找不到文件。 - "触发器"建议设为每周一次,同时把时间选在凌晨业务低峰期。日志文件往往在夜间还有写入,选太早可能删不掉刚刚轮转的文件,选太晚又可能赶上业务高峰。实际经验是凌晨2点到4点之间相对安全。
任务计划创建完成后,一定要手动"运行"一次,看任务结果是否成功,再去看日志文件确认有没有实际删除动作。很多任务看着显示"运行成功",实际因为权限或路径问题什么都没干。
另一个Windows下容易忽略的点是:如果脚本执行时间较长,任务计划默认的"如果任务运行时间超过X则停止"设置可能把任务中断。清理几十万个文件可能需要十几分钟,把这个设置改成"不停止"更稳妥。
5.2 Linux cron:PATH稀缺、日志重定向、避免重复运行
Linux侧的自动化首选cron,在root用户的crontab里加一行就行。
30 2 * * * /usr/local/sbin/clean_expired_files.sh >> /var/log/cleanup.log 2>&1意思是每天凌晨2点30分执行清理脚本,并把标准输出和错误输出都追加到日志文件。这条cron配置本身没什么坑,坑在脚本里。cron环境的PATH极简,只有/usr/bin:/bin,如果你在脚本里调用了find、date这些命令,建议在脚本开头先强制指定PATH。
#!/bin/bash export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin不加这句,脚本在终端手动执行时一切正常,放进cron就报"command not found",排查半天才发现是PATH问题。除PATH外,还需要考虑cron里没有完整的环境变量,比如LANG没设置可能导致date输出不是预期格式,但这属于细节问题,按需处理即可。
cron还有一个重复执行的问题。假如清理脚本因为某种原因卡住了,比如网络挂载目录无响应,下一次cron触发时又启动一个新的清理进程,两个进程同时删文件会互相影响。解决办法是在脚本开头加一个锁文件判断。
LOCK_FILE="/var/run/cleanup.lock" if [ -e "$LOCK_FILE" ]; then echo "$(date) previous cleanup still running, exit" >> "$LOG_FILE" exit 0 fi touch "$LOCK_FILE" trap 'rm -f "$LOCK_FILE"' EXITtrap保证脚本退出时把锁文件删除,即使中途报错也不会锁死后续任务。这套锁机制简单有效,我几乎在所有cron的清理脚本里都加了。
5.3 多目录批量配置:用一个脚本管理所有清理任务
生产环境往往不止一个目录需要清理。日志目录、备份目录、临时导出目录,每个目录的保留周期可能不同。与其为每个目录单独写一个脚本注册一个任务,不如做一个参数化脚本,用配置文件或变量数组驱动多个清理任务。
Windows批处理下可以这样组织:
set "KEEP_DAYS=30" for %%D in ("C:\Logs\App" "D:\Backup\Tmp" "E:\Data\Temp") do ( call :clean_dir "%%~D" ) goto :eof :clean_dir set "TARGET_DIR=%~1" if not exist "%TARGET_DIR%" goto :eof forfiles /P "%TARGET_DIR%" /S /M *.* /D +%KEEP_DAYS% /C "cmd /c if @isdir==FALSE del /f /q @path" goto :eofLinux下直接用数组:
DAYS=30 DIRS=( "/var/log/app" "/data/backup/tmp" "/opt/service/temp" ) for dir in "${DIRS[@]}"; do if [ -d "$dir" ]; then find "$dir" -type f -mtime +"$DAYS" -delete fi done列表形式的好处是后续加目录只需改一行。如果不同目录要不同的保留天数,把DAYS也变成目录名和天数关联的配置就行,比如用目录:天数的写法循环拆分,我这边就不展开写了。配置文件驱动的思路本质是一样的:脚本读配置,配置定义清理策略,人只维护配置,不碰逻辑。
6. 从单目录到全局:批量配置、现成轮子与我的实战建议
6.1 同样能“删旧文件”的logrotate与tmpwatch
聊完自己写脚本,回过头看,有些场景其实有更成熟的现成工具。Linux自带logrotate,专治日志文件轮转。它做的不是简单删除,而是按大小或时间把日志切片、压缩、留N份,老日志到达保留份数后才会被清理。如果你的目的是管好"日志文件",logrotate是比find批量删除更正规的方案,因为它还能处理压缩、权限、重启通知等一系列细节。
tmpwatch这个命令更贴近find删除的用途,专门清理目录下长时间未被访问的文件,常用于/tmp目录的维护。用法类似tmpwatch -m 48 /tmp,表示删除/tmp下超过48小时未被修改的文件。CentOS/RHEL系列自带这个命令,Ubuntu系则是tmpreaper。这些工具好不好用,取决于发行版和具体场景,但如果你的服务器本身就是主流发行版,先看看有没有现成机制,再决定写不写脚本,这个习惯能省不少事。
Windows侧对应的现成方案是系统自带的"存储感知"(Storage Sense),不过它主要面向Windows客户端的用户配置,不适合服务器场景。服务器上更常用的还是任务计划加forfiles或PowerShell脚本,没有比这更通用的做法了。
6.2 一键式双平台脚本的设计思路
如果有整套环境同时包含Windows和Linux服务器,可以设计一套统一调用入口的清理方案,而不是在每个平台上维护完全不同的逻辑。比如采用"配置驱动"的思路:每个目标目录写一行配置,包含路径、保留天数、是否递归、是否跳过子目录,同一个小工具在Windows和Linux下各自解析这套配置,执行各自的删除策略。
Windows侧用PowerShell读取CSV格式的配置,Linux侧用bash读取同一份CSV或环境变量文件。这样业务上只需要改一份配置,Windows和Linux的脚本都跟着生效。架构上不复杂,关键是避免每台服务器上脚本逻辑越来越散。我见过一个环境里每个团队各写各的清理脚本,参数五花八门,最后运维根本分不清哪个目录谁在管,这种场景下统一配置的价值就体现出来了。
当然,统一的代价是两边的脚本需要各维护一份,并且要保持行为一致。对小规模环境来说,先各自用最简单的方式跑通,比一上来就设计一套分布式统一清理系统更实际。自动化是逐步生长的,不是一步到位的。
6.3 我的几个实用建议与收尾经验
最后结合我自己的实战经验,给几个具体建议。
第一,清理脚本的第一版本一定要先手工跑,确认匹配的文件列表符合预期后再交给计划任务。这一步不能省。我自己第一次写forfiles批处理时,曾经因为忘记加@isdir==FALSE判断,脚本把子目录也当作删除对象处理,虽然最终没有造成实际损失,但排查过程很耗时间。从那以后,任何删除类脚本都先做"只打印不删除"的演练。
第二,日志记录比删除本身更重要。脚本每次执行时,把删除时间、目录、文件数记下来。一旦出事,第一件事就是查日志,确认脚本到底删了什么。没有日志的清理脚本等于裸奔。如果日志量太大,可以只记录删除数量而不记录每个文件名,但至少要能查到"哪次执行删了多少个文件"。
第三,避免"精准删除"变成"暴力删除"。任何脚本里都别写裸的rm -rf,任何forfiles命令都别裸用/M * /D +0这样的极端组合。保持最小化权限、显式指定目录、显式指定文件类型、显式说明保留天数,这四个显式是安全底线。
再分享一个我踩过的真实坑:某公司的日志清理脚本在Linux上运行正常,后来迁移到一台新服务器,crontab配置照旧,但脚本一直不执行。排查半天,发现新服务器的crontab里默认环境变量没有包含脚本依赖的某个工具路径,脚本开头也没有强制设置PATH。加上export PATH=...之后,问题立刻消失。这个坑极其常见,也极其隐蔽。现在我的所有cron清理脚本,开头永远是#!/bin/bash加一行完整的PATH声明。
清理脚本这个事,看着简单,实际做起来到处是边界细节。但只要把时间基准、文件类型过滤、权限、日志、调度这五件事理清楚,它就能长期稳定地替你守着磁盘空间。真正让我觉得值得的,不是脚本本身有多精巧,而是"磁盘满了才发现"的那种焦虑,变成了每天凌晨自动发生的一次安静清理。