2024/09/06

AppleScriptでEXIFを参照し撮影日毎のフォルダを作り写真をコピーする

AppleScriptを使って、選択したファイルを撮影日ごとにフォルダに整理する方法を紹介します。今回は、Finderで選択したファイルをEXIF情報から撮影日を取得し、日付ごとのフォルダに自動で振り分けるAppleScriptを使います。
※要ExifTool


以前書いたものとほぼ同じです。
EXIFを元に撮影日名フォルダにまとめるAppleScript - ぽんハウス BLOG
実はPythonで書き直すために書いていたのですが、このAppleScriptのみのバージョンと速度があまり変わらなかったので、使うのはこちらにしました。

AppleScriptのコード

以下のスクリプトでは、Finderで選択したファイルの撮影日を取得し、それに基づいてファイルを指定したフォルダにコピーします。フォルダ名は「YYMMDD」形式で作成され、同じ日付のファイルは一つのフォルダにまとめられます。

tell application "Finder"

-- Finderで選択されたファイルを取得

set fileObject to selection as alias list

-- ファイルが選択されているか確認

if fileObject is {} then

log "ファイルが選択されていません。"

return

end if

-- コピー先フォルダを選択

set targetDir to (choose folder with prompt "コピー先フォルダを選択してください")'s POSIX path

-- 処理全体の開始時間を記録(保存先フォルダを選択した後)

set totalStartTime to current date

-- すべての選択されたファイルのPOSIXパスを取得してリストに格納

set filePaths to {}

repeat with obj in fileObject

set end of filePaths to quoted form of (POSIX path of obj)

end repeat

-- ファイルパスをスペースで区切って結合

set filesString to ""

repeat with aFile in filePaths

set filesString to filesString & aFile & " "

end repeat

-- exiftoolの開始時間を記録

set exifStartTime to current date

-- exiftoolで全ファイルの撮影日(DateTimeOriginal)をまとめて取得

set exifResult to do shell script "/opt/homebrew/bin/exiftool -T -DateTimeOriginal " & filesString

-- exiftoolの処理終了時間を記録

set exifEndTime to current date

set exifDuration to (exifEndTime - exifStartTime)

log "EXIFデータ取得にかかった時間: " & exifDuration & " "

-- 結果を行単位で分割

set exifList to paragraphs of exifResult

-- 選択されたファイルリストと対応するEXIF日付リストを処理

repeat with i from 1 to count of filePaths

set filePath to item i of filePaths

set obj to quoted form of filePath

set CDR to item i of exifList -- 対応するEXIFデータの行

-- EXIFデータが空でないか確認

if CDR is not "" then

try

-- 取得した日付のフォーマットが "YYYY:MM:DD" の場合、":" を除去

set AppleScript's text item delimiters to ":"

set CDRList to text items of CDR

set fullDate to item 1 of CDRList & item 2 of CDRList & item 3 of CDRList -- "YYYYMMDD" 形式に結合

set AppleScript's text item delimiters to ""

-- 頭の2 ("20") を削除し、次の6桁だけを変数に格納 (YYMMDD形式)

set yymmdd to text 3 through 8 of fullDate

-- フォルダ作成の開始時間を記録

set folderStartTime to current date

-- ターゲットフォルダ作成

set destinationFolderPath to targetDir & "/" & yymmdd

do shell script "mkdir -p " & quoted form of destinationFolderPath

-- フォルダ作成の終了時間を記録

set folderEndTime to current date

set folderDuration to (folderEndTime - folderStartTime)

log "フォルダ作成にかかった時間: " & folderDuration & " "

-- ファイルコピーの開始時間を記録

set copyStartTime to current date

-- ファイルを指定されたフォルダに個別にコピー

do shell script "cp " & filePath & " " & quoted form of destinationFolderPath

-- ファイルコピーの終了時間を記録

set copyEndTime to current date

set copyDuration to (copyEndTime - copyStartTime)

log "ファイルコピーにかかった時間: " & copyDuration & " "

on error errMsg

log "エラーが発生しました: " & errMsg

end try

else

log "ファイル" & filePath & "EXIFデータが見つかりません。"

end if

end repeat

-- 処理全体の終了時間を記録

set totalEndTime to current date

set totalDuration to (totalEndTime - totalStartTime)

log "全体処理にかかった時間: " & totalDuration & " "

end tell

解説

このスクリプトでは、exiftool を使って各ファイルの撮影日を取得し、その日付に基づいてフォルダを作成し、ファイルを整理しています。フォルダ名は YYMMDD 形式に変換されて保存されます。フォルダが存在しない場合は自動で作成され、ファイルがフォルダにコピーされます。

例えば、撮影日が「2023年9月5日」のファイルは「230905」というフォルダに移動されます。

exiftoolで全ファイルの撮影日(DateTimeOriginal)をまとめて取得するためには、exiftool のパスを指定する必要があります。以下のスクリプトでは、/opt/homebrew/bin/exiftool を使っていますが、環境によっては異なるパスになることがあります。正しいパスはターミナルで以下のコマンドを実行して確認できます:

which exiftool

また、exiftool をインストールしていない場合は、以下の方法でインストールできます:

  • 公式サイトからダウンロード: ExifTool公式サイト
  • Homebrew経由でインストール: brew install exiftool
set exifResult to do shell script "/opt/homebrew/bin/exiftool -T -DateTimeOriginal " & filesString

フォルダやファイルがすでに存在する場合、以下の動作がデフォルトの挙動です:

フォルダが既に存在する場合:
スクリプトで使用している mkdir -p コマンドは、指定したフォルダが既に存在している場合でもエラーを発生させず、処理を続けます。既存のフォルダがある場合、そのまま処理が進行し、新しいフォルダが作成されません。

ファイルが既に存在する場合:
スクリプトで使用している cp コマンドのデフォルトの挙動では、コピー先に同名のファイルが存在すると、そのファイルが上書きされます。上書きを防ぎたい場合は、cp コマンドに -n オプション(no clobber)を付けることで、ファイルが既に存在する場合にコピー処理をスキップすることができます。

例えば、ファイルが既に存在している場合に上書きを防ぐには、以下のように変更します:

-- ファイルを指定されたフォルダにコピー、既に存在する場合は上書きしない
do shell script "cp -n " & filePath & " " & quoted form of destinationFolderPath

こうすることで、同名のファイルがコピー先に既に存在する場合、ファイルは上書きされず、コピー処理がスキップされます。

一応Python用のコード

GUIはAppleScriptを使い、上のコードと同じく、Finderで選択しているファイルをPythonに渡します。

AppleScriptのみのコードもそうですが、デバッグのための処理時間測定など、いらないコードが入ったままです。

Pythonコードの保存先になるパスとファイル名で

/Users/XXXX/Desktop/exif_date_sorter.py

この部分を書き換えて下さい。

AppleScript

tell application "Finder"

-- Finderで選択されたファイルを取得

set fileObject to selection as alias list

-- ファイルが選択されているか確認

if fileObject is {} then

display dialog "ファイルが選択されていません。" buttons {"OK"} default button "OK"

return

end if

-- すべての選択されたファイルのPOSIXパスを取得してリストに格納

set filePaths to ""

repeat with obj in fileObject

set filePaths to filePaths & quoted form of (POSIX path of obj) & " " -- ファイルパスにスペースを追加

end repeat

-- コピー先フォルダを選択

set targetDir to (choose folder with prompt "コピー先フォルダを選択してください")'s POSIX path

-- Pythonスクリプトを実行し、ファイルパスとコピー先ディレクトリを渡し、結果を受け取る

set pythonScript to "/usr/bin/python3 /Users/XXXX/Desktop/exif_date_sorter.py"

set shellResult to do shell script pythonScript & " " & filePaths & quoted form of targetDir

-- 結果を表示(コンソールの代わりにダイアログで表示)

display dialog shellResult buttons {"OK"} default button "OK"

end tell

Python

import os
import subprocess
import shutil
import sys
import time

def get_exif_dates(file_paths):
"""複数ファイルを一度に処理して、各ファイルの撮影日をYYMMDD形式で取得する"""
result = subprocess.run(
["/opt/homebrew/bin/exiftool", "-T", "-DateTimeOriginal"] + file_paths, # 複数ファイルを渡す
capture_output=True,
text=True
)
exif_data_list = result.stdout.strip().splitlines() # 結果を行ごとに分割
exif_dates = []
for exif_data in exif_data_list:
if exif_data:
date_only = exif_data.split(" ")[0] # 日付部分のみ抽出
date_parts = date_only.split(":")
if len(date_parts) >= 3:
yy = date_parts[0][2:] # "20XX"から"XX"部分を取得
mm = date_parts[1]
dd = date_parts[2]
exif_dates.append(yy + mm + dd)
else:
exif_dates.append(None)
else:
exif_dates.append(None) # EXIFデータがない場合はNoneを返す
return exif_dates

def group_files_by_date(file_paths):
"""ファイルを撮影日ごとにグループ化"""
from collections import defaultdict
exif_dates = get_exif_dates(file_paths) # まとめて撮影日を取得
grouped_files = defaultdict(list)
# 各ファイルの撮影日でグループ化
for file_path, exif_date in zip(file_paths, exif_dates):
if exif_date:
grouped_files[exif_date].append(file_path)
else:
print(f"EXIFデータが見つかりませんでした: {file_path}")
return grouped_files

def copy_files_to_date_folders(grouped_files, target_dir):
"""ファイルを撮影日ごとにフォルダにまとめてコピー"""
for exif_date, files in grouped_files.items():
destination_folder = os.path.join(target_dir, exif_date)
os.makedirs(destination_folder, exist_ok=True)
for file_path in files:
shutil.copy(file_path, destination_folder)

if __name__ == "__main__":
# 全体の開始時間を記録
total_start_time = time.time()

# AppleScriptからファイルパスとターゲットディレクトリを取得
file_paths = sys.argv[1:-1] # 最後の要素はターゲットディレクトリ
target_dir = sys.argv[-1]
# ファイルを日付ごとにグループ化
grouped_files = group_files_by_date(file_paths)
# グループごとにファイルをコピー
copy_files_to_date_folders(grouped_files, target_dir)

# 全体の終了時間を記録
total_end_time = time.time()
total_duration = total_end_time - total_start_time
# 結果を表示
print(f"全体処理にかかった時間: {total_duration:.4f}秒")