
猫哥最近在 NAS 上存了几万张照片,时间跨度三年。每次找照片都得在层层叠叠的文件夹里翻半天,索性写了个 Python 脚本,把照片按 EXIF 拍摄日期自动归档,跑了一遍之后,文件夹瞬间清爽了。
为什么不用 NAS 自带的工具?
群晖 DSM 有 Photo Station 和 Moments,但有几个问题:
- Photo Station 按日期归档的逻辑是上传日期,不是拍摄日期
- Moments 倒是能读 EXIF,但会复制一份原文件,NAS 存储翻倍
- 我想保持原有目录结构,只把散落的文件归拢
所以自己写,零依赖、不改原文件、只移动不复制。
核心思路:EXIF + 文件mtime 双保险
不是所有照片都有 EXIF,尤其是截图、下载的图片。脚本先用 piexif 读 EXIF 的 DateTimeOriginal,如果没有,fallback 到文件修改时间。
import os
import shutil
from datetime import datetime
from pathlib import Path
import piexif
def get_photo_date(path: Path) -> datetime:
# 优先 EXIF DateTimeOriginal
try:
exif_dict = piexif.load(str(path))
dt_str = exif_dict['Exif'].get(piexif.ExifIFD.DateTimeOriginal)
if dt_str:
return datetime.strptime(dt_str.decode('utf-8'), '%Y:%m:%d %H:%M:%S')
except Exception:
pass
# Fallback to mtime
mtime = os.path.getmtime(path)
return datetime.fromtimestamp(mtime)
def organize_photos(src: Path, dst: Path):
for path in src.rglob('*'):
if path.suffix.lower() not in {'.jpg', '.jpeg', '.png', '.heic', '.raw'}:
continue
dt = get_photo_date(path)
target = dst / f"{dt.year}" / f"{dt.month:02d}" / path.name
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
# 同名文件比较大小,保留大的
if path.stat().st_size > target.stat().st_size:
shutil.move(str(path), str(target))
print(f"Replaced: {path.name}")
else:
path.unlink() # 删除重复小文件
print(f"Removed duplicate: {path.name}")
else:
shutil.move(str(path), str(target))
print(f"Moved: {path.name} → {target.parent}")
if __name__ == '__main__':
organize_photos(Path('/volume1/photo/inbox'), Path('/volume1/photo/organized'))
几个关键细节
1. HEIC 支持 — iPhone 的 HEIC 格式 piexif 读不了,需要装 pyheif + Pillow。但 pyheif 在群晖上编译麻烦,最后用了 exiftool 命令行兜底:
import subprocess
def get_heic_date(path: Path) -> datetime:
result = subprocess.run(
['exiftool', '-DateTimeOriginal', '-s3', str(path)],
capture_output=True, text=True
)
if result.returncode == 0 and result.stdout.strip():
return datetime.strptime(result.stdout.strip(), '%Y:%m:%d %H:%M:%S')
return datetime.fromtimestamp(os.path.getmtime(path))
2. 重复文件处理 — 同一张照片可能从微信、相机、NAS 备份三个渠道进来,文件名一样但大小不同。脚本比较大小,保留高分辨率版本,小文件直接删。
3. 空目录清理 — 移动完之后,源目录里留下大量空文件夹,用 os.walk 自底向上删除:
for root, dirs, files in os.walk(str(src), topdown=False):
for d in dirs:
p = Path(root) / d
if p.exists() and not any(p.iterdir()):
p.rmdir()
跑了一遍的效果
- 源目录
/volume1/photo/inbox:12,847 个文件,混在 300+ 个文件夹里 - 归档后
/volume1/photo/organized:按YYYY/MM/结构,共 24 个月份目录 - 删除重复文件:2,103 个(省出 8.7GB)
- 空文件夹清理:剩 0 个
踩坑记录
- exiftool 在群晖上默认没有 — 需要装 Perl + 从官网下载 exiftool 放到
/usr/local/bin/,不要 apt 装(版本太旧) - HEIC 的 EXIF 日期格式和 JPEG 不一样 — JPEG 是
2023:01:15 14:30:00,HEIC 有时候带时区偏移,需要正则处理 - shutil.move 跨文件系统会复制再删除 — 如果 inbox 和 organized 在不同存储池上,move 会变成 copy+delete,速度极慢。解决:统一在一个存储池内操作,或者先用 os.rename 试,失败再 fallback
- 群晖的索引服务会锁文件 — 如果 Synology Photos 正在索引,文件被占用,shutil.move 会 Permission denied。解决:先
synoindex -R /volume1/photo暂停索引,跑完再恢复 - 不要在 DSM 上直接跑 Python 3.9 以上的脚本 — 群晖系统 Python 是 3.8,装 piexif 会报版本不兼容。解决:用 Docker 跑独立 Python 环境,或者装 Entware 的 Python 3.11
下一步:Web UI + 定时任务
现在每次整理还得 SSH 上去跑脚本,下一步打算:
- 用 Flask 搭个 Web 界面,拖放文件夹、点按钮归档
- 加
inotify监听,新照片进 inbox 自动触发归档 - 对接 DSM 通知中心,整理完发一条推送
代码放 GitHub 了,有 NAS 的朋友自取。

