フォルダ内のファイル名を一括リネームするスクリプト
フォルダ内のファイル名を一括リネームするPythonスクリプト。連番・日付(YYYYMMDD)・ランダム文字のサフィックスを先頭または末尾に付与でき、プレビューモードで実行前に全件の変更内容を確認できます。サブフォルダの再帰走査と同名衝突時の自動スキップを備え、共有フォルダでも安心して使える設計です。
「スキャンしたPDFを日付付きの連番に整えたい」「納品ファイル100本に命名ルールを一括適用したい」といった作業を1回で終わらせるスクリプトです。連番・日付(YYYYMMDD)・ランダム文字のサフィックスを先頭または末尾に付けられ、プレビューモードで本番前に全件の変更内容を確認できます。
このスクリプトでできること
- 連番・日付・ランダム文字の3種類のサフィックス生成
- 先頭(プレフィックス)または末尾(サフィックス)への付与を選択可能
- プレビューモードで実行前に全件の変更内容を確認
- サブフォルダまで再帰的に処理
- 半角・全角スペースの一括除去オプション
.pybes ファイルをPybesにインポートすると、スクリプトと設定フィールドが自動で読み込まれます。
設定フィールド
このスクリプトで使用する設定フィールドです。Pybes上でGUIから値を入力できます。
target_dir フォルダ 必須 対象フォルダ
リネーム対象のファイルがあるフォルダを選択します
base_name テキスト 必須 変換文字列
「元のファイル名を維持する」をOFFにしたときの新しいベース名(例: invoice)
keep_original チェックボックス 必須 元のファイル名を維持する
ONなら元の名前を残してサフィックスだけ付与、OFFなら変換文字列+連番で全面的に置き換え
デフォルト: false
position ドロップダウン 必須 追加位置
サフィックスをファイル名の先頭と末尾のどちらに付けるかを指定します
suffix_type ドロップダウン 必須 追加テキスト
付与する文字列の種類。日付・連番・ランダム文字から選択
trim_space チェックボックス 必須 空白除去
半角スペースと全角スペースをベース名から一括で取り除きます
デフォルト: true
preview_mode チェックボックス 必須 プレビューモード(ログ出力のみ)
ONにすると実際のリネームを行わず、変更予定だけをログに出力します
デフォルト: true
recursive チェックボックス 必須 サブフォルダを含める
指定フォルダ配下のサブフォルダまで再帰的にリネーム対象にするか
デフォルト: false
コード解説
import sys
import json
import os
import random
import string
from datetime import datetime
with open(sys.argv[1], encoding="utf-8") as f:
inputs = json.load(f)
target_dir = inputs["対象フォルダ"]
base_name = inputs["変換文字列"]
keep_original = inputs["元のファイル名を維持する"] == "true"
position = inputs["追加位置"]
suffix_type = inputs["追加テキスト"]
trim_space = inputs["空白除去"] == "true"
preview_mode = inputs["プレビューモード(ログ出力のみ)"] == "true"
recursive = inputs["サブフォルダを含める"] == "true"
# 前提: 追加テキストの区切り文字は "_" を使用
# 前提: 連番は対象ファイル数に応じてゼロ埋め桁数を自動調整
# 前提: 元のファイル名を維持しない場合、変換文字列にインデックスを付与して一意性を保証
if recursive:
file_paths = []
for dirpath, _, filenames in os.walk(target_dir):
for f in filenames:
file_paths.append(os.path.join(dirpath, f))
else:
file_paths = [
os.path.join(target_dir, f)
for f in os.listdir(target_dir)
if os.path.isfile(os.path.join(target_dir, f))
]
total = len(file_paths)
if total == 0:
print("対象ファイルが見つかりませんでした。")
sys.exit(0)
print(f"対象ファイル数: {total} 件")
if preview_mode:
print("\n【プレビューモード】実際のリネームは行いません\n")
digits = len(str(total))
today_str = datetime.now().strftime("%Y%m%d")
success = 0
skip = 0
for i, filepath in enumerate(sorted(file_paths)):
try:
dirpath = os.path.dirname(filepath)
filename = os.path.basename(filepath)
stem, ext = os.path.splitext(filename)
if keep_original:
base = stem
else:
base = f"{base_name}_{str(i + 1).zfill(digits)}"
if trim_space:
base = base.replace(" ", "").replace(" ", "")
if suffix_type == "yyyymmdd(日付)":
suffix_text = today_str
elif suffix_type == "001(連番)":
suffix_text = str(i + 1).zfill(digits)
else:
suffix_text = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
if position == "先頭":
new_stem = f"{suffix_text}_{base}"
else:
new_stem = f"{base}_{suffix_text}"
new_filename = new_stem + ext
dst = os.path.join(dirpath, new_filename)
if preview_mode:
if os.path.exists(dst) and filepath != dst:
print(f"[競合あり] {filepath} → {new_filename}")
else:
print(f"[変更予定] {filepath} → {new_filename}")
success += 1
continue
if os.path.exists(dst) and filepath != dst:
print(f"[スキップ] 同名ファイルが既に存在します: {filepath}", file=sys.stderr)
skip += 1
continue
os.rename(filepath, dst)
print(f"[{i+1}/{total}] {filepath} → {new_filename}")
success += 1
except Exception as e:
print(f"[エラー] {filepath}: {e}", file=sys.stderr)
skip += 1
if preview_mode:
print(f"\nプレビュー完了: {success} 件の変更予定を表示しました")
print("実際にリネームするには「プレビューモード」のチェックを外して実行してください")
else:
print(f"\n完了: {success} 件リネーム / {skip} 件スキップ") 標準ライブラリ(sys / json / os / random / string / datetime)を読み込み、Pybesが sys.argv[1] に渡してくるJSON設定を inputs に展開します。Pybesではチェックボックスの値が文字列の "true" / "false" として届くので、== "true" で比較して明示的にboolへ変換している点に注目してください。inputs のキーは全て ASCII snake_case で、Pybesアプリ側のフィールド名と一致している必要があります。
再帰フラグによって収集方法を切り替えます。recursive が真なら os.walk で配下のサブフォルダを含めて全ファイルを拾い、偽なら os.listdir + os.path.isfile で直下のファイルだけをリスト内包表記で集めます。os.walk はフォルダを再帰的にたどる標準的なイディオムで、権限エラーなどは内部で握りつぶされるため、読めないフォルダがあってもループ全体は止まりません。
メインループでは os.path.splitext で拡張子を一度剥がしてからベース名を組み立てます。keep_original が真なら元のファイル名をそのまま土台にし、偽なら 変換文字列_001 のように連番付きの新しい名前を生成します。digits = len(str(total)) が効いていて、対象件数が3桁なら自動で3桁ゼロ埋めになります。サフィックスは「日付」「連番」「ランダム6文字」の3択で、random.choices の ascii_lowercase + digits で英数字ランダム文字列を作る定番パターンです。
プレビューモードでは os.rename を呼ばず、ログに [変更予定] / [競合あり] を出すだけで次のファイルへ進みます。本番実行時は、リネーム先が既に存在すれば [スキップ] を sys.stderr に流してカウントし、それ以外は os.rename で原子的に改名します。try / except Exception で個別ファイルの失敗を握り、ループ全体が途中で落ちないようにしているのがポイントです。
仕組みの詳細
リネーム対象の収集
「サブフォルダを含める」がONの場合は os.walk で再帰的にフォルダをたどり、OFFの場合は os.listdir で直下のファイルだけを対象にします。どちらのルートも最終的に file_paths という絶対パスのリストに揃えるので、後段のループは収集方法を気にせず同じ処理を回せる設計です。sorted() で並べてから回すため、連番の付き方もファイル名順で安定します。
新ファイル名の組み立て
「ベース名 + _ + サフィックス」という単純なルールで新しい名前を組み立てます。ベース名は keep_original がONなら元のstem(拡張子を除いた部分)、OFFなら変換文字列+連番です。サフィックスは今日の日付・連番・ランダム6文字のいずれかで、position の値(「先頭」か「末尾」か)で位置を切り替えます。拡張子(.pdf など)は最後に再結合するので、元の拡張子は必ず維持されます。
プレビューと衝突回避
preview_mode がONのとき、os.rename は一切呼ばずログ出力だけに留めます。本番実行時は、改名先に既に同名ファイルがあると [スキップ] 扱いで sys.stderr に書き出し、処理自体は止めません。個別ファイルの例外も try / except で握っているため、1本の失敗で残りの90本が中断することはありません。
応用・カスタマイズ
特定の拡張子だけをリネーム対象にする
ループの先頭 for i, filepath in enumerate(sorted(file_paths)): の直後に if not filepath.lower().endswith((".pdf", ".xlsx")): continue を1行追加すれば、指定した拡張子のファイルだけを対象にできます。小文字比較にするのが、.PDF など大文字拡張子を取りこぼさないコツです。
ベース名とサフィックスの区切り文字を変える
f"{base_name}_{str(i + 1).zfill(digits)}" や f"{suffix_text}_{base}" / f"{base}_{suffix_text}" の中の _ を - や . に差し替えるだけで、区切り文字を変更できます。ファイル名に使えない記号(/ \\ : * ? " < > |)は避けてください。
連番のゼロ埋め桁数を固定する
digits = len(str(total)) を digits = 4 のように固定値に書き換えると、件数に関わらず常に4桁ゼロ埋めになります(0001, 0002, ...)。納品番号のフォーマットが決まっている運用で便利です。
よくあるエラーと対処
PermissionError が出て途中で止まります
対象ファイルが他のアプリ(Excel・PDFビューア・エディタなど)で開かれているとWindowsがファイルロックを掛け、os.rename が PermissionError で失敗します。該当ファイルを閉じてから再実行してください。エクスプローラーでプレビューウィンドウを開いたままにしている場合も、プレビューウィンドウを閉じる必要があります。
「同名ファイルが既に存在します」と表示されてスキップされた
生成された新ファイル名が、フォルダ内の他のファイルと衝突しました。keep_original がOFFのときは変換文字列+連番で一意性を保証していますが、ONのまま日付サフィックスだけを付けて複数回実行すると同じ名前が再計算されて衝突します。ランダム文字サフィックスに切り替えるか、先にプレビューモードで競合を確認してから実行してください。
「対象ファイルが見つかりませんでした。」で即終了する
指定フォルダが空か、「サブフォルダを含める」がOFFでファイルがサブフォルダ側にしか無い状態です。サブフォルダも対象にしたい場合は recursive をONにして再実行してください。フォルダのパスが正しいかも合わせて確認してください。
FAQ
リネームした後で元のファイル名に戻せますか?
このスクリプト単体では戻せません。os.rename は元の名前を記録しないためです。どうしても戻す必要がある場合は、プレビューモードの出力(元ファイル名 → 新ファイル名の対応表)を先にコピーしておいてください。実務では、重要フォルダは事前にZipでバックアップしてから実行することをおすすめします。
連番のゼロ埋め桁数はどうやって決まりますか?
対象ファイル数から自動計算しています。digits = len(str(total)) の1行で、15件なら2桁(01〜15)、150件なら3桁(001〜150)になります。カスタマイズの項目にある通り、固定桁数にしたい場合はここを書き換えてください。
report.tar.gz のような2重拡張子はどう扱われますか?
os.path.splitext は最後の拡張子しか分離しないため、このスクリプトは .gz のみを拡張子として扱い、report.tar をベース名とみなします。結果的に report.tar_001.gz のような形になります。.tar.gz を一体として残したい場合は、stem, ext = os.path.splitext(filename) の直後に「末尾が .tar なら更に剥がす」処理を足してください。
スキップされたファイルは、もう一度実行すればリネームされますか?
はい。スキップ理由の多くは「新ファイル名が既存と衝突した」「ファイルが開かれていた」のいずれかです。開いていたアプリを閉じた上で再実行するか、ランダム文字サフィックスに切り替えて衝突を回避してください。リネームが通らないファイルだけが残る形になるので、2周目はかなり速く終わります。
.pybes ファイルをPybesにインポートすると、スクリプトと設定フィールドが自動で読み込まれます。