2026-10-06 · 喵窝资源铺
桌面、下载文件夹、微信自动接收目录——用半年就堆出上千个文件,名字还全是 IMG_2023(12).jpg、新建文档(3).docx。手动按日期分文件夹要人命,网盘的自动整理又不听你的。这篇给一份按文件修改时间归到「年/年-月/」两层目录的脚本,支持 move(搬走)和 copy(复制留原件)两种模式,代码完整实跑过,真实输出贴在下面。
.py / .sh 工具文件直接跳过——归档脚本别把自己和同事的脚本搬走;Thumbs.db、.DS_Store、desktop.ini 系统垃圾不归档也不删,原地留着;os.path.getmtime 换算成 2026/2026-09 两层目录,os.makedirs(..., exist_ok=True) 建好;-1、-2 后缀,绝不覆盖(见坑 1,这个判断不是锦上添花,是保命的);import os, sys, shutil, time
SRC = sys.argv[1] # 待归档目录
MODE = sys.argv[2] if len(sys.argv) > 2 else 'move' # move | copy
SKIP_EXT = {'.py', '.sh'} # 坑:归档脚本别把自己搬走
SKIP_NAME = {'Thumbs.db', '.DS_Store', 'desktop.ini'} # 坑:系统垃圾别归档
def bucket(ts):
t = time.localtime(ts)
return time.strftime('%Y', t), time.strftime('%Y-%m', t)
moved, skipped = [], []
if MODE not in ('move', 'copy'):
sys.exit('mode 只能是 move 或 copy')
for root, dirs, files in os.walk(SRC):
# 跳过已经生成出的 年/月 目录,防止二次运行扫自己产出
rel = os.path.relpath(root, SRC)
if rel != '.' and (len(rel) == 4 and rel[:4].isdigit()):
dirs[:] = []
continue
for fn in sorted(files):
p = os.path.join(root, fn)
ext = os.path.splitext(fn)[1].lower()
if ext in SKIP_EXT or fn in SKIP_NAME:
skipped.append((fn, '系统/工具文件不归档')); continue
y, m = bucket(os.path.getmtime(p))
dest_dir = os.path.join(SRC, y, m)
os.makedirs(dest_dir, exist_ok=True)
dest = os.path.join(dest_dir, fn)
if os.path.exists(dest):
base, e = os.path.splitext(fn)
i = 1
while os.path.exists(os.path.join(dest_dir, f'{base}-{i}{e}')):
i += 1
dest = os.path.join(dest_dir, f'{base}-{i}{e}')
(shutil.move if MODE == 'move' else shutil.copy2)(p, dest)
moved.append((os.path.relpath(p, SRC), os.path.relpath(dest, SRC)))
print(f'{MODE} {len(moved)} 个文件,跳过 {len(skipped)}')
for src, dst in moved: print(f' {src} -> {dst}')
for fn, why in skipped: print(f' [跳] {fn}:{why}')
用法:python3 sort_by_mtime.py 目录 move(或 copy)。拿不准就先用 copy 跑一遍——原件原地不动,月份目录里是归好档的副本,确认没丢东西再删原件。
造了 8 类样本:10 天前的照片、50 天前的报表、400 天前的扫描件(跨年)、顶层与子目录同名的两个 same.txt、撞名场景、一个 Thumbs.db 和一个 .py:
move 6 个文件,跳过 2 same-copy.txt -> 2026/2026-10/same-copy.txt same.txt -> 2026/2026-10/same.txt 合同扫描.pdf -> 2025/2025-08/合同扫描.pdf 合影.jpg -> 2026/2026-09/合影.jpg 报表.xlsx -> 2026/2026-08/报表.xlsx sub/same.txt -> 2026/2026-10/same-1.txt [跳] Thumbs.db:系统/工具文件不归档 [跳] clean.py:系统/工具文件不归档
逐个核对内容(防「搬了但内容坏了」):
| 文件 | 核对 | 结果 |
|---|---|---|
| 2026/2026-10/same.txt | 内容 = 顶层原版「content of same.txt」 | ✅ |
| 2026/2026-10/same-1.txt | 内容 = 子目录版「sub version」,撞名自动 -1 后缀 | ✅ |
| 2025/2025-08/合同扫描.pdf | 400 天前正确跨到去年 | ✅ |
| Thumbs.db / clean.py | 原地未动 | ✅ 跳过 |
连跑第二次:move 0 个文件——年月目录被显式跳过,不重扫自己产出。幂等通过。copy 模式另测 2 文件:归档后原件仍在原位,copy2 保留原 mtime。
os.walk 全目录扫一遍直接 shutil.move(p, dest)。实跑复现:顶层一个 same.txt(写 TOP VERSION)、子目录一个 same.txt(写 SUB VERSION),修改时间同月——两个都会被归到同一个月份目录,shutil.move 遇到目标已存在时直接覆盖且不报任何错。跑完月份目录里只剩 SUB VERSION,TOP VERSION 无声无息消失,回收站里都没有。修复:目标存在就换名(-1、-2 往后排),这不是可选优化,是必须。a.jpg 归到 2026/2026-09/ 后,第二次运行 os.walk 又扫进这个文件——它的 mtime 没变、目标路径计算结果相同,但「目标已存在」触发的是撞名逻辑,于是又生成 a-1.jpg。跑两遍目录里就有 a.jpg 和 a-1.jpg 两份。修复:walk 到纯 4 位数字的年份目录直接 dirs[:]=[] 剪枝。修完连跑第二次是 0 移动。photo_2020_trip.jpg,从微信/网盘另存下来后 mtime 就是下载那一刻——被归到 2026 年而不是 2020 年。微信接收的文件、邮件附件、网盘同步下来的文件全有这个问题。mtime 归档只保证「文件最后一次变动是什么时候」,想要真实日期得读图片 EXIF(拍摄时间)或文件名里的日期串,那是另一个脚本的事,别指望 mtime 一个字段包打天下。sub/ 之类目录留在原地,交给 find 目录 -type d -empty -delete 收尾。相关文章:全盘重复文件 MD5 检测清理 · 附件按文件名批量归档改名 · 乱码文件批量转 UTF-8 · 按扩展名分文件夹