ChatGPT 5 Plusに
JPEGファイルのファイル名を"yyyy-mm-dd HH.MM.SS.jpg" のように変更するスクリプトが欲しいです。日付はJPEGファイルの中に書き込まれているEXIF情報を参照してください。また、同一日時のファイルができてしまう場合は、末尾に通し番号をつけるようにしてください。
というプロンプトを突っ込んだら、
以下のPythonスクリプトで対応できます。EXIFの
DateTimeOriginal
→DateTime
→DateTimeDigitized
の順に参照し、YYYY-MM-DD HH.MM.SS.jpg
へ改名します。重複時は_01
,_02
, … を付与します。EXIFが無い場合はファイルの更新時刻を使用します
と言いながら以下のようなPythonプログラムを返してきました。
ディレクトリ内にある全てのJPEGファイルを一発で名称変更してくれました。そんなに難しい内容ではないし、すでに多くの人間が書いたプログラムでしょうが、初手から修正なしで問題なく動く(ように見える)のすごいですね……。
#!/usr/bin/env python3 # rename_jpegs_by_exif.py import argparse import os import sys from datetime import datetime from pathlib import Path try: from PIL import Image, ExifTags # pip install Pillow except ImportError: print("Pillow が必要です: pip install Pillow", file=sys.stderr) sys.exit(1) # EXIFタグ名→IDの逆引き TAG_ID = {v: k for k, v in ExifTags.TAGS.items()} PREFERRED_TAGS = [ "DateTimeOriginal", # 撮影日時 "DateTime", # 画像の更新日時 "DateTimeDigitized", # デジタイズ日時 ] def read_exif_datetime(path: Path) -> datetime | None: try: with Image.open(path) as im: exif = im.getexif() if not exif: return None # 優先順で日時文字列を取得 dt_str = None for name in PREFERRED_TAGS: tag_id = TAG_ID.get(name) if tag_id in exif and exif.get(tag_id): dt_str = exif.get(tag_id) break if not dt_str: return None # 典型フォーマット "YYYY:MM:DD HH:MM:SS" # 稀にバイト列のことがあるので文字列化 if isinstance(dt_str, bytes): dt_str = dt_str.decode(errors="ignore") dt_str = dt_str.strip() # サブ秒が別タグ(37521: SubSecTimeOriginal)に入っている場合がある subsec = exif.get(37521) or exif.get(37522) or exif.get(37520) # Original / Digitized / Image if isinstance(subsec, bytes): subsec = subsec.decode(errors="ignore") # パース(コロン区切りの年月日に対応) base = datetime.strptime(dt_str[:19], "%Y:%m:%d %H:%M:%S") # サブ秒は使わない(重複は連番で解決) return base except Exception: return None def mtime_fallback(path: Path) -> datetime: ts = path.stat().st_mtime return datetime.fromtimestamp(ts) def format_dt(dt: datetime) -> str: # "YYYY-MM-DD HH.MM.SS" 形式 return f"{dt.year:04d}-{dt.month:02d}-{dt.day:02d} {dt.hour:02d}.{dt.minute:02d}.{dt.second:02d}" def next_available_name(target_dir: Path, base: str, ext: str, used: set[str]) -> Path: # 重複時は _01, _02, ... を付与 candidate = f"{base}{ext}" i = 1 while (target_dir / candidate).exists() or candidate in used: candidate = f"{base}_{i:02d}{ext}" i += 1 used.add(candidate) return target_dir / candidate def main(): ap = argparse.ArgumentParser(description="JPEGをEXIF日時でリネーム") ap.add_argument("path", nargs="?", default=".", help="対象ディレクトリ(既定: カレント)") ap.add_argument("-n", "--dry-run", action="store_true", help="実際にはリネームせず計画のみ表示") ap.add_argument("-r", "--recursive", action="store_true", help="サブディレクトリも処理") args = ap.parse_args() root = Path(args.path).expanduser().resolve() if not root.exists(): print("指定パスが存在しません", file=sys.stderr) sys.exit(1) patterns = ("*.jpg", "*.jpeg", "*.JPG", "*.JPEG") files = [] if args.recursive: for ptn in patterns: files.extend(root.rglob(ptn)) else: for ptn in patterns: files.extend(root.glob(ptn)) if not files: print("対象JPEGが見つかりません") return used_names = set() # この実行内での重複回避 for src in sorted(files): # 拡張子は .jpg に正規化 ext = ".jpg" dt = read_exif_datetime(src) if dt is None: dt = mtime_fallback(src) base = format_dt(dt) dst = next_available_name(src.parent, base, ext, used_names) if src.name == dst.name: # すでに目的の名前 print(f"SKIP: {src.name}") continue print(f"{src.name} -> {dst.name}") if not args.dry_run: try: src.rename(dst) except FileExistsError: # 競合が発生したときの最終保険 dst = next_available_name(src.parent, base, ext, used_names) src.rename(dst) if __name__ == "__main__": main()