跳至正文

用 Python 把 NAS 上的照片自动按日期归档,再也不用手动整理了

NAS照片归档

猫哥最近在 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 的朋友自取。

Python脚本运行

归档效果

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注