数万枚のWebP画像をダウンタイムなしでID別ディレクトリに再配置するPythonワンライナーの設計

この記事は約7分で読めます。
この記事が役立ったらブックマーク! あとで読み返したり、環境構築時のリファレンスに活用できます
B! はてなブックマークに追加

Webサイトの運用が長期化すると、単一ディレクトリ内に数万〜数十万枚の画像ファイルがフラットに溜まり、ファイル一覧の取得遅延やバックアップ処理のボトルネックを引き起こします。

今回は、WordPress環境などでフラットに保存された大量のWebP画像(例: chr3040000000_01.webp)を、サービス稼働を止めることなく安全に「10桁IDごとのサブディレクトリ」へ再配置する運用スクリプトの設計と実践コードを解説します。

直面する課題と要件

単にファイルを移動させるだけに見えますが、本番稼働中のサーバーでは以下のリスクを考慮する必要があります。

  • ダウンタイムの防止: 移動(mv)ではなくコピー(cp / shutil.copy2)を用い、新旧の切り替え確認が完了するまで元データを担保する。
  • 命名の衝突と上書きリスク: ディレクトリ名には接頭辞を省いたIDを使用しつつ、ファイル名自体は接頭辞や枝番を含めた完全な状態を維持する。
  • 正規表現の抽出ブレ: シェルスクリプト(Bash)のパイプ処理では、ID内の連続する0(ゼロ)やバッファ詰まりによって処理漏れが起きやすいため、堅牢な正規表現エンジン(Python)を採用する。

再配置Pythonワンライナー

ターミナル(SSH)から直接実行できるPythonワンライナーです。スクリプトファイルを作成することなく、1行で安全にバッチ処理を実行できます。

Bash
python3 -c '
import os, re, shutil

src = "/var/www/example.com/htdocs/wp-content/uploads/chars"
dest = "/var/www/example.com/htdocs/wp-content/uploads/chars_organized"

os.makedirs(dest, exist_ok=True)

# 接頭辞(英字)を読み飛ばし、続く10桁数字をIDとして抽出
pattern = re.compile(r"^[a-zA-Z]*([0-9]{10})")

count = 0
for root, _, files in os.walk(src):
    # 出力先ディレクトリ自身の再帰探索を防止
    if "chars_organized" in root:
        continue
    for f in files:
        if not f.lower().endswith(".webp"):
            continue
        m = pattern.search(f)
        if m:
            asset_id = m.group(1)
            target_dir = os.path.join(dest, asset_id)
            os.makedirs(target_dir, exist_ok=True)
            
            src_file = os.path.join(root, f)
            dest_file = os.path.join(target_dir, f)
            
            # 同名ファイルが存在しない場合のみコピー(上書き防止)
            if not os.path.exists(dest_file):
                shutil.copy2(src_file, dest_file)
                count += 1
                print(f"OK: {f} -> {asset_id}/{f}")
        else:
            print(f"SKIP: {f}")

print(f"\n完了: 合計 {count} 件のファイルを配置しました。")
'

コード設計のポイント

1. shutil.copy2 によるメタデータの保持 通常の copy ではなく copy2 を採用することで、ファイルの最終更新日時(mtime)やパーミッション情報をそのまま複製します。これにより、Webサーバー側のキャッシュ整合性(Last-Modifiedヘッダー)を維持できます。

2. os.walk() と除外ガード 探索元ディレクトリの中に生成先ディレクトリを作成する場合、探索ループが無限に潜り込むリスクがあります。if "chars_organized" in root: continue を挟むことで、自分自身の探索を即座にスキップします。

3. 存在確認による二重実行の安全性 if not os.path.exists(dest_file): により、処理が途中で中断して再実行した場合でも、すでにコピー済みのファイルを無駄に再処理せず差分のみを同期します。

運用後の必須ステップ:パーミッション調整

root権限で上記スクリプトを実行した場合、生成されたディレクトリやファイルの所有者が root:root になります。このままではNginxやApacheなどのWebサーバーがファイルを読み込めず、ブラウザ側で 403 Forbidden エラーが発生します。

処理完了後は、必ずWebサーバー実行ユーザー(Ubuntu環境では www-data)に所有権を戻してください。

Bash
sudo chown -R www-data:www-data /var/www/example.com/htdocs/wp-content/uploads/chars_organized

大量の静的アセット整理は、シェルの文字列操作の限界を見極め、Python標準ライブラリの堅牢なファイル操作APIを活用することで、安全かつダウンタイムなしに完結させることができます。

コメント

タイトルとURLをコピーしました