Python で複数ファイルを一括リネームする

ファイルの一括リネームは、写真の整理やログファイルの命名規則統一など、日常的に発生する作業だ。Python では os モジュールや pathlib を使って、柔軟なリネーム処理をスクリプトとして記述できる。

基本的なリネーム操作

1 つのファイルをリネームするには os.rename() か pathlib の rename() を使う。

import os
from pathlib import Path

# os モジュール
os.rename("old_name.txt", "new_name.txt")

# pathlib
path = Path("old_name.txt")
path.rename("new_name.txt")

pathlib の rename() は新しいパスを Path オブジェクトとして返すため、リネーム後のパスをそのまま変数に受け取れる。

from pathlib import Path

old = Path("report.txt")
new = old.rename("report_final.txt")
print(new)  # report_final.txt

ディレクトリ内のファイルを一括リネームする

特定のディレクトリ内にあるファイルをまとめてリネームするには、glob でファイルを列挙してループ処理を行う。

from pathlib import Path

target_dir = Path("./photos")

for i, file in enumerate(sorted(target_dir.glob("*.jpg")), start=1):
    new_name = f"photo_{i:04d}.jpg"
    file.rename(target_dir / new_name)
    print(f"{file.name} -> {new_name}")

このスクリプトは photos ディレクトリ内の JPEG ファイルを photo_0001.jpg, photo_0002.jpg, … のように連番でリネームする。sorted() を使っているのは、glob の結果が OS によって順序が保証されないためだ。

プレフィックスやサフィックスを付ける

既存のファイル名を保持しつつ、先頭や末尾に文字列を追加するパターンもよく使われる。

from pathlib import Path

target_dir = Path("./logs")

for file in target_dir.glob("*.log"):
    # プレフィックスを追加: access.log -> 2025_access.log
    new_name = f"2025_{file.name}"
    file.rename(target_dir / new_name)

サフィックス(拡張子の前に文字列を挿入する)場合は stem と suffix を活用する。

from pathlib import Path

target_dir = Path("./data")

for file in target_dir.glob("*.csv"):
    # data.csv -> data_backup.csv
    new_name = f"{file.stem}_backup{file.suffix}"
    file.rename(target_dir / new_name)

pathlib の stem プロパティは拡張子を除いたファイル名、suffix は拡張子をそれぞれ返す。この 2 つを組み合わせることで、拡張子を維持したまま名前を加工できる。

正規表現でパターン置換する

ファイル名の一部を正規表現で置換したい場合は、re モジュールと組み合わせる。

import re
from pathlib import Path

target_dir = Path("./reports")

for file in target_dir.glob("*"):
    if file.is_file():
        # "report_2024-01-15.txt" -> "report_20240115.txt"
        new_name = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", file.name)
        if new_name != file.name:
            file.rename(target_dir / new_name)
            print(f"{file.name} -> {new_name}")

置換前と置換後が同じ場合はスキップしている。同名ファイルへの rename() は OS によって挙動が異なるため、この条件チェックは入れておくのが安全だ。

ドライランで安全に確認する

一括リネームは取り消しが難しいため、実行前にドライラン(実際にはリネームせず結果だけ表示)で確認するのが望ましい。

from pathlib import Path

def batch_rename(target_dir, pattern, name_func, dry_run=True):
    target = Path(target_dir)
    changes = []

    for file in sorted(target.glob(pattern)):
        new_name = name_func(file)
        if new_name != file.name:
            changes.append((file, target / new_name))

    if not changes:
        print("リネーム対象なし")
        return

    for old, new in changes:
        if dry_run:
            print(f"[DRY RUN] {old.name} -> {new.name}")
        else:
            old.rename(new)
            print(f"{old.name} -> {new.name}")

    return changes

dry_run=True の状態で結果を確認し、問題なければ dry_run=False で実行するという流れになる。

# 使用例: 拡張子を .txt から .md に変更
batch_rename(
    "./docs",
    "*.txt",
    lambda f: f"{f.stem}.md",
    dry_run=True
)

glob でファイルを列挙

name_func で新しい名前を生成

ドライランで結果を確認

問題なければ実行

名前の衝突を防ぐ

一括リネームで注意すべきなのが、リネーム先のファイル名が既に存在するケースだ。何も対策しないと、既存のファイルが上書きされてしまう可能性がある。

from pathlib import Path

def safe_rename(file_path, new_path):
    """衝突時はサフィックスを付けてリネーム"""
    if not new_path.exists():
        return file_path.rename(new_path)

    # 衝突した場合は連番を付ける
    stem = new_path.stem
    suffix = new_path.suffix
    parent = new_path.parent

    counter = 1
    while True:
        candidate = parent / f"{stem}_{counter}{suffix}"
        if not candidate.exists():
            return file_path.rename(candidate)
        counter += 1

ただし、この存在チェックとリネームの間にタイミングの問題(race condition)が生じうる。複数のプロセスが同時にリネームを行う環境では、テンポラリファイルを介したアトミックな操作を検討する必要がある。

大文字・小文字の変換

ファイル名の大文字・小文字を統一するリネームは、OS によって挙動が異なる点に気をつけたい。

Linux / macOS(APFS 以外)

ファイルシステムが大文字小文字を区別するため、FILE.txtfile.txt は別ファイルとして共存できる

Windows / macOS(APFS デフォルト)

大文字小文字を区別しないため、FILE.txtfile.txt にリネームしようとすると「同じファイル」として扱われる

Windows や macOS のデフォルト環境では、大文字から小文字への直接リネームが失敗する場合がある。一度別の名前にリネームしてから目的の名前にする 2 段階方式が安全だ。

from pathlib import Path

def rename_case(file_path, new_name):
    """大文字小文字のみ異なるリネームを安全に行う"""
    path = Path(file_path)
    target = path.parent / new_name

    if path.name.lower() == new_name.lower() and path.name != new_name:
        # 一時名にリネームしてから目的の名前にする
        tmp = path.parent / f"{path.stem}_tmp_{id(path)}{path.suffix}"
        path.rename(tmp)
        tmp.rename(target)
    else:
        path.rename(target)

一括リネームは手軽に見えて落とし穴が多い操作だ。ドライランでの事前確認、名前衝突への対策、OS 間の違いを考慮しておくと、本番環境でのトラブルを防げる。