本稿では、コマンドラインインターフェース(CLI)を通じてファイルのバッチ処理を行うPython製ユーティリティの構築手法を解説します。このツールは、正規表現を用いたファイル名の一括変更、Pillowライブラリを活用した画像フォーマットの変換および圧縮、そしてMD5やSHAファミリーによるハッシュ値の算出をサポートしています。また、画像変換処理にはマルチスレッドを導入し、大量のファイル処理におけるパフォーマンスを最適化しています。
プロジェクト構成
機能ごとにモジュールを分割し、保守性と拡張性を高めたディレクトリ構成を採用しています。
file-manipulator/
├── cli_app.py # CLIエントリーポイント
├── modules/
│ ├── hasher.py # ハッシュ算出ロジック
│ ├── converter.py # 画像変換ロジック
│ └── renamer.py # ファイル名変更ロジック
└── requirements.txt # 依存パッケージ
コアモジュールの実装
CLIエントリーポイント (cli_app.py)
argparseモジュールを利用してサブコマンドを設計し、各機能へのルーティングを行います。標準ライブラリのpathlibを採用することで、パス操作をより安全かつ直感的に扱えます。
import argparse
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from modules.hasher import calculate_hash
from modules.converter import transform_image
from modules.renamer import execute_rename
def setup_cli():
parser = argparse.ArgumentParser(description="CLI File Manipulation Utility")
sub_cmds = parser.add_subparsers(dest="action", required=True)
# リネームサブコマンド
p_rename = sub_cmds.add_parser("rename", help="Batch rename files")
p_rename.add_argument("-d", "--directory", required=True, type=Path, help="Target directory")
p_rename.add_argument("-r", "--regex", help="Regex pattern for matching")
p_rename.add_argument("-s", "--subst", help="Substitution string")
p_rename.add_argument("-p", "--prefix", help="Prefix to append")
p_rename.add_argument("-i", "--index", type=int, default=1, help="Starting index")
# 画像変換サブコマンド
p_convert = sub_cmds.add_parser("convert", help="Batch convert images")
p_convert.add_argument("-d", "--directory", required=True, type=Path, help="Target directory")
p_convert.add_argument("-f", "--source_fmt", required=True, help="Source extension (e.g., png)")
p_convert.add_argument("-t", "--target_fmt", required=True, help="Target extension (e.g., jpg)")
p_convert.add_argument("-q", "--quality", type=int, default=80, help="Compression quality")
# ハッシュ算出サブコマンド
p_hash = sub_cmds.add_parser("hash", help="Compute file hash")
p_hash.add_argument("-t", "--target", required=True, type=Path, help="File or directory path")
p_hash.add_argument("-a", "--algorithm", default="sha256", choices=["md5", "sha1", "sha256"])
return parser.parse_args()
def run():
opts = setup_cli()
if opts.action == "rename":
execute_rename(opts.directory, opts.regex, opts.subst, opts.prefix, opts.index)
elif opts.action == "convert":
targets = [f for f in opts.directory.iterdir() if f.suffix.lower() == f".{opts.source_fmt}"]
with ThreadPoolExecutor() as pool:
futures = [pool.submit(transform_image, f, opts.target_fmt, opts.quality) for f in targets]
for _ in as_completed(futures):
pass
elif opts.action == "hash":
if opts.target.is_file():
print(f"{opts.algorithm}: {calculate_hash(opts.target, opts.algorithm)}")
elif opts.target.is_dir():
for f in opts.target.iterdir():
if f.is_file():
print(f"{f.name}: {calculate_hash(f, opts.algorithm)}")
if __name__ == "__main__":
run()
ハッシュ算出モジュール (modules/hasher.py)
メモリ消費を抑えるため、ファイルをチャンク単位で読み込みながらハッシュダイジェストを生成します。イテレータとiter関数を組み合わせ、効率的なストリーム処理を実現しています。
import hashlib
def calculate_hash(file_path, algo="sha256"):
digest = hashlib.new(algo)
with open(file_path, "rb") as stream:
for block in iter(lambda: stream.read(65536), b""):
digest.update(block)
return digest.hexdigest()
画像変換モジュール (modules/converter.py)
Pillow(PIL)を使用し、画像のカラーモードをRGBに統一した上で指定フォーマットへ保存します。コンテキストマネージャを用いてリソースの確実な解放を図り、フォーマット名のマッピング処理も内包しています。
from PIL import Image
def transform_image(src_path, dest_fmt="jpeg", qual=80):
try:
with Image.open(src_path) as img:
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
out_path = src_path.with_suffix(f".{dest_fmt}")
save_fmt = "JPEG" if dest_fmt.lower() in ("jpg", "jpeg") else dest_fmt.upper()
img.save(out_path, format=save_fmt, quality=qual)
print(f"Success: {src_path.name} -> {out_path.name}")
except IOError as err:
print(f"Error processing {src_path.name}: {err}")
リネームモジュール (modules/renamer.py)
正規表現によるパターンマッチングと置換、および連番プレフィックスの付与を実装しています。pathlib.Pathのrenameメソッドを利用し、OSに依存しないファイル操作を行います。
import re
def execute_rename(base_dir, pattern=None, repl=None, pre=None, idx=1):
items = sorted([p for p in base_dir.iterdir() if p.is_file()])
for item in items:
stem = item.stem
ext = item.suffix
if pattern and repl is not None:
stem = re.sub(pattern, repl, stem)
if pre:
stem = f"{pre}_{idx:04d}"
idx += 1
new_path = base_dir / f"{stem}{ext}"
item.rename(new_path)
print(f"Updated: {item.name} => {new_path.name}")
実行コマンドの例
ファイル名の一括変更
正規表現でキャプチャした文字列を再利用し、プレフィックスと連番を付与する例です。
python cli_app.py rename -d ./assets/images -r "(.+)" -s "pic_\1" -p "IMG" -i 100
画像フォーマットのバッチ変換
PNGファイルをJPEGフォーマットに変換し、圧縮品質を90に設定してマルチスレッドで処理します。
python cli_app.py convert -d ./assets/images -f png -t jpg -q 90
ハッシュ値の検証
特定のディレクトリ内の全ファイルに対してSHA-256ハッシュを算出します。
python cli_app.py hash -t ./assets/images -a sha256
設計上のポイントと注意点
- パス操作のモダナイゼーション: 従来の
os.pathに代わりpathlibを使用することで、オブジェクト指向的なパス操作が可能になり、コードの可読性が向上します。 - スレッドプールの適切な利用:
concurrent.futures.ThreadPoolExecutorはI/Oバウンドなタスク(画像の読み書きなど)に有効ですが、CPUバウンドな処理にはProcessPoolExecutorの検討が必要です。 - 不可逆操作のリスク管理: ファイル名の変更や画像の非可逆圧縮(JPEGなど)は元に戻せない操作であるため、本番環境で実行する前には必ず対象データのバックアップを取得する必要があります。
- 画像モードの統一: PNGなどのアルファチャンネルを持つ画像をJPEGに変換する際、
RGBへのモード変換を省略するとエラーが発生するため、事前のカラーモード判定が不可欠です。