python Exifとワガママ | 60歳を迎えて、思うこと。

60歳を迎えて、思うこと。

いつの間にか、「60」という年月が経ちました。
残り少ないか多いか?わかりませんが。
じじぃ~の「ひとりごと」を細々と続けられれば。。。

python Exifとワガママ

ぶひぶひ
どんどん

すまほ
やら
たぶれっと



いっぱい あると

ファイル名が

気になって
ショウガ
ない

IMG_20260726_011906.jpg

だったり

PXL_20260726_011906.jpg

だったり

IMG_1234.JPG

だったり

はっきり 言って


気持ち悪い;;



【Gemini】様に

作ってぇ~~~



丸投げ!


# ==== 【Gemini】様

# ====Rev.C
# MOTOROLA特殊 Exif対処
# 再帰的処理追加
# ====
# 脱線大歓迎です!むしろ実用性を考えると、サブフォルダも含めて一括処理できた方が圧倒的に便利ですよね。
# 結論から言うと、**完全に可能**です!
# Pythonの `os.walk()` という標準機能を使えば、`S:\_Exif` の直下だけでなく、その中にある**すべてのサブフォルダ(`S:\_Exif\iPad_air` など)を自動で再帰的に探索(ガサ入れ)して全ファイルを拾い出す**ことができます。
# 階層構造は無視して、見つかった対象ファイルをすべて平坦に `S:\_Exif-Rename` 直下へ集約してコピーします。

# ## 修正箇所の解説
# 変更箇所はすごくシンプルで、メイン処理の `os.listdir()`(直下のみ検索)を `os.walk()`(全サブフォルダ探索)に差し替えるだけです。
# すでに組み込んである同名ファイル自動リネーム(重複防止機能 `_1`, `_2`)がそのまま働くため、別々のサブフォルダにたまたま同じファイル名があったとしても上書きされず、安全に処理されます!

# ## 全サブフォルダ対応版コード(完全版)
# 以下のコードに差し替えて実行してみてください!
# ```python


import os
import shutil
import re
from datetime import datetime
from PIL import Image
import pillow_heif

# HEIC形式の読み込みを許可
pillow_heif.register_heif_opener()

# hachoir (動画メタデータ解析用)
from hachoir.metadata import extractMetadata
from hachoir.parser import createParser

# ★絶対死守★ スクリプトのディレクトリをカレントディレクトリに変更
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
print(f"Current directory: {os.getcwd()}")

# --- 設定項目 ---
SRC_DIR = r"S:\_Exif"
DEST_DIR = r"S:\_Exif-Rename"
LOG_FILE = r"L:\pypy\Exif情報-Rename\Exif情報-Rename-Log.txt"

# 対象とする拡張子(すべて小文字で定義)
TARGET_EXTS = {".jpg", ".jpeg", ".png", ".heic", ".mp4", ".mov"}

# 機種名のマッピング(Exifのカメラモデル名 -> ファイル名に使用する略称)
MODEL_MAP = {
    "iPhone 16e": "16e",
    "iPad Pro (12.9-inch) (3rd generation)": "iPadPro",
    "iPad Air (5th generation)": "iPadAir",
    "Redmi Note 10 Pro": "Note", # Redmi Note 10 Pro
    "M2101K6R": "Note", # Redmi Note 10 Pro
    "M2101K6G": "Note", # Redmi Note 10 Pro
    "Pixel 10a": "10a",
    "SM-A233C": "A23", # Galaxy A23 5G
    
    
    # --- motorola用(表記揺れをすべてカバー) ---
    "moto g24": "g24",
    "moto g24 ": "g24",
    "motog24": "g24",
    "motorola moto g24": "g24",
}

def log_message(log_fp, msg):
    """コンソール表示とログファイル書き込みを同時に行う"""
    print(msg)
    log_fp.write(msg + "\n")

def get_image_exif(file_path):
    """画像から撮影日時、秒以下フラグメント、カメラ機種名を取得"""
    filename = os.path.basename(file_path)
    
    try:
        with Image.open(file_path) as img:
            exif = img.getexif()
            
            model = ""
            date_str = ""
            subsec = ""

            if exif:
                # ==== Rev.B 追加 Null文字対策
                # model = exif.get(0x0110, "").strip()
                # \x00 (ヌル文字) を消してから strip() する
                model = str(exif.get(0x0110, "")).replace("\x00", "").strip()

                sub_exif = exif.get_ifd(0x8769)
                date_str = sub_exif.get(36867) or sub_exif.get(36868) or exif.get(306)
                subsec = sub_exif.get(37521) or sub_exif.get(37522) or ""

            # --- 【救済処理】motorola等のExif消失ファイル対策 ---
            if not date_str:
                match = re.search(r'(\d{8})_(\d{6})(\d{3})?', filename)
                if match:
                    ymd = match.group(1)
                    hms = match.group(2)
                    subsec = match.group(3) or ""
                    date_str = f"{ymd[:4]}:{ymd[4:6]}:{ymd[6:8]} {hms[:2]}:{hms[2:4]}:{hms[4:6]}"
                    
                    # 救済ルートでも MODEL_MAP を通るように生名のフリをする
                    if not model and ("_SR" in filename or "_HDR" in filename or "_MFNR" in filename):
                        model = "moto g24"

            if not model or not date_str:
                return None, None, None

            dt = datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
            return dt, str(subsec).strip(), model

    except Exception:
        return None, None, None

def get_video_metadata(file_path):
    """動画(MP4/MOV)から作成日時と機種名を取得"""
    try:
        parser = createParser(file_path)
        if not parser:
            return None, None, None
        
        with parser:
            metadata = extractMetadata(parser)
            if not metadata:
                return None, None, None
            
            dt = metadata.get("creation_date")
            model = metadata.get("camera_model") or metadata.get("producer") or ""
            return dt, "", str(model).strip()
    except Exception:
        return None, None, None

def main():
    os.makedirs(DEST_DIR, exist_ok=True)
    os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)

    with open(LOG_FILE, "a", encoding="utf-8") as log_fp:
        log_message(log_fp, f"\n=== 処理開始: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===")
        
        if not os.path.exists(SRC_DIR):
            log_message(log_fp, f"[ERROR] 処理対象フォルダが存在しません: {SRC_DIR}")
            return

        success_count = 0
        skip_count = 0

        # ★【変更点】os.walk を使って全サブフォルダを再帰的に探索!
        for root, dirs, files in os.walk(SRC_DIR):
            for filename in files:
                src_path = os.path.join(root, filename)

                ext = os.path.splitext(filename)[1].lower()
                if ext not in TARGET_EXTS:
                    log_message(log_fp, f"[SKIP] 対象外の拡張子: {filename}")
                    skip_count += 1
                    continue

                # 画像または動画からメタデータ取得
                if ext in {".mp4", ".mov"}:
                    dt, subsec, model = get_video_metadata(src_path)
                else:
                    dt, subsec, model = get_image_exif(src_path)

                # Exif/メタデータに撮影日時や機種名がない場合はスキップ
                if not dt or not model:
                    log_message(log_fp, f"[SKIP] Exif情報不足/なし: {filename}")
                    skip_count += 1
                    continue

                # 機種名の変換(MODEL_MAPに登録があれば置換、なければExifの生の文字)
                model_prefix = MODEL_MAP.get(model, model)
                
                # 特殊文字を除去してファイル名に使える文字にする
                model_prefix = "".join(c for c in model_prefix if c.isalnum() or c in ("_", "-"))

                # 日時文字列の生成
                date_part = dt.strftime("%Y%m%d_%H%M%S")
                
                # 新しいファイル名の組み立て
                new_filename = f"{model_prefix}_{date_part}{subsec}{ext}"
                dest_path = os.path.join(DEST_DIR, new_filename)

                # 同名ファイルが存在する場合の重複防止処理(_1, _2 を付与)
                counter = 1
                base_dest_path = os.path.join(DEST_DIR, f"{model_prefix}_{date_part}{subsec}")
                while os.path.exists(dest_path):
                    dest_path = f"{base_dest_path}_{counter}{ext}"
                    counter += 1

                # ファイルの安全コピー&リネーム実行
                try:
                    shutil.copy2(src_path, dest_path)
                    # ログには元のサブフォルダパスも含めて分かりやすく表示
                    rel_path = os.path.relpath(src_path, SRC_DIR)
                    log_message(log_fp, f"[SUCCESS] {rel_path} -> {os.path.basename(dest_path)}")
                    success_count += 1
                except Exception as e:
                    log_message(log_fp, f"[ERROR] コピー失敗 {filename}: {e}")
                    skip_count += 1

        log_message(log_fp, f"=== 処理完了: 成功 {success_count} 件 / スキップ {skip_count} 件 ===")

if __name__ == "__main__":
    main()


 ==============================
# これで `S:\_Exif\iPad_air` やその他の深いサブフォルダにある写真も、根こそぎ拾ってリネームできるようになりました!
# ==============================
まあ いいんでないのw

じじぃ~