スクリプト・ファイル自動化

ファイル名を一括変更する前にプレビューする

変更前と変更後の名前を表で確認し、名前の衝突を検査します。--applyを明示したときだけ新しいフォルダーにコピーを作り、元のファイルを残します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 한국어

対象読者複数のファイル名を変えたいものの、ルールの間違いや上書きが心配な初心者

準備するもの
  • Python 3.12以上を用意し、ターミナルでpython --versionを実行してバージョンを確認します。
  • サンプルZIPを新しいフォルダーに展開します。圧縮ファイルの中から直接実行しないでください。
  • example.pyがあるフォルダーでターミナルを開きます。Windowsでpythonコマンドがない場合はpyを、macOS・Linuxでは環境に応じてpython3を使います。
  • 外部パッケージのインストールやアカウントは不要です。同梱の資料は、独自に作成した合成データです。

01まず三つの名前だけをプレビューする

一括変更では、一つのファイルの誤字より先に、誤ったルールが全ファイルに適用される事態を防ぐ必要があります。この例では、名前の一覧をすべて検証してから計画を表示します。通常のコマンドはファイルのコピーもフォルダーの作成もしません。実際の処理は、確認後に--applyを付けて別途実行します。

  1. 展開したフォルダーでmapping.csvと、source_files内の三つの元ファイルを確認します。
  2. mapping.csvのold_nameとnew_nameを比較します。左が現在のファイル名、右が新しいコピーの名前です。
  3. 以下の通常のコマンドを実行し、矢印で示される三組の名前が意図した組み合わせか確認します。
  4. この段階ではsource_filesが変わっておらず、outputsがまだ存在しないことを確認します。
bash
python example.py

02名前の対応表の読み方

old_namenew_name
메모 초안.txtnote-draft.txt
견적 1.txtquote-001.txt
사진 설명.txtphoto-notes.txt

対応表の各行は、一つの元ファイルと一つの結果を結び付けます。old_nameは、大文字・小文字も含めて実際の名前と完全に一致させます。元ファイルを漏らしたり、存在しない名前を追加したりすると、処理全体が停止します。このルールにより、フォルダーに新しいファイルが増えているのに古い対応表をそのまま使う間違いを検出できます。

new_name同士は、大文字・小文字だけが違う場合も衝突と見なします。report.txtとREPORT.txtを別々の結果として作らないための方針です。名前の前後の空白、末尾のピリオド、パス区切り文字、Windowsの予約名も拒否します。命名ルールが業務に適しているかは、プレビューで人が最終確認する必要があります。

03実行に使うコード全体

make_planですべての入力と衝突を検査します。mainは検証済みの計画を表示してから、--applyの有無を確認します。実際に適用する場合も、元の名前をrenameするのではなく、内容を新しい名前でコピーするため、開始時の状態を再確認できます。

example.py
"""기본은 미리보기입니다. --apply일 때만 새 이름의 복사본을 만듭니다."""

import argparse
import csv
import re
import shutil
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
INPUT = BASE / "source_files"
MAPPING = BASE / "mapping.csv"
OUTPUT = BASE / "outputs"
RESERVED = {"CON", "PRN", "AUX", "NUL"} | {
    f"{prefix}{number}" for prefix in ("COM", "LPT") for number in range(1, 10)
}


def validate_name(name):
    # 폴더 경로, Windows 예약 이름, 제어 문자 등을 이름으로 받지 않습니다.
    if (not name or name in {".", ".."} or name != name.strip()
            or name.endswith(".") or re.search(r'[<>:"/\\|?*\x00-\x1f]', name)
            or name.split(".")[0].upper() in RESERVED):
        raise ValueError(f"사용할 수 없는 파일명: {name!r}")


def make_plan():
    if (INPUT.is_symlink() or not INPUT.is_dir()
            or getattr(INPUT, "is_junction", lambda: False)()):
        raise ValueError("source_files는 실제 폴더여야 합니다.")
    if MAPPING.is_symlink():
        raise ValueError("mapping.csv 링크는 처리하지 않습니다.")
    sources = list(INPUT.iterdir())
    if any(path.is_symlink() or not path.is_file() for path in sources):
        raise ValueError("source_files에는 일반 파일만 넣으세요.")
    if not sources:
        raise ValueError("source_files에 파일이 없습니다.")
    plan, old_keys, new_keys = [], set(), set()
    with MAPPING.open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream, strict=True)
        if reader.fieldnames != ["old_name", "new_name"]:
            raise ValueError("mapping.csv의 열은 old_name,new_name이어야 합니다.")
        for row in reader:
            if None in row or any(value is None for value in row.values()):
                raise ValueError(f"mapping.csv {reader.line_num}행의 열 수를 확인하세요.")
            old, new = row["old_name"], row["new_name"]
            validate_name(old)
            validate_name(new)
            if old.casefold() in old_keys:
                raise ValueError(f"원본이 중복 지정되었습니다: {old}")
            if new.casefold() in new_keys:
                raise ValueError(f"새 이름 충돌: {new}")
            old_keys.add(old.casefold())
            new_keys.add(new.casefold())
            plan.append((old, new))
    # 한 파일이라도 누락되거나 없는 파일을 지정하면 아무것도 복사하지 않습니다.
    if {old for old, _ in plan} != {path.name for path in sources}:
        raise ValueError("mapping.csv의 old_name은 원본 파일 전체와 정확히 일치해야 합니다.")
    if OUTPUT.exists() or OUTPUT.is_symlink():
        raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
    return plan


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--apply", action="store_true", help="새 이름의 복사본을 outputs/renamed에 만듭니다.")
    args = parser.parse_args()
    plan = make_plan()  # 충돌 검사는 출력 폴더를 만들기 전에 전체에 대해 끝냅니다.
    for old, new in plan:
        print(f"{old} → {new}")
    if not args.apply:
        print(f"미리보기: {len(plan)}개. 파일은 변경되지 않았습니다.")
        print("이름을 확인한 뒤 python example.py --apply 를 실행하세요.")
        return 0

    OUTPUT.mkdir()
    renamed = OUTPUT / "renamed"
    renamed.mkdir()
    for old, new in plan:
        # 이름 변경 대신 복사합니다. xb는 대상이 생겼으면 덮어쓰지 않고 실패합니다.
        with (INPUT / old).open("rb") as source, (renamed / new).open("xb") as target:
            shutil.copyfileobj(source, target)
    with (OUTPUT / "manifest.csv").open("x", encoding="utf-8-sig", newline="") as stream:
        writer = csv.writer(stream)
        writer.writerow(["old_name", "new_name"])
        writer.writerows(plan)
    print(f"완료: {len(plan)}개 복사본 → {renamed}")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError, csv.Error) as error:
        print(f"중지: {error}", file=sys.stderr)
        raise SystemExit(2)

argparseのstore_trueオプションを使っているため、--applyを省略すると通常のプレビューになります。コピー先はxbモードで開き、対象ファイルがすでに作られている場合は上書きしません。完了した対応関係はmanifest.csvにも記録し、新しい名前から元の名前を逆引きできるようにしています。

04確認した計画でコピーを作る

プレビューの三行を確認したら、同じフォルダーで次のコマンドを実行します。プレビュー後にmapping.csvを変更した場合は、通常のコマンドを再実行して新しい計画を確認してください。適用コマンドは前のプレビュー画面を記憶しているのではなく、実行時点の対応表を読み直して検査します。

bash
python example.py --apply
場所期待される結果
source_files/元の名前のファイル3個を保持
outputs/renamed/note-draft.txt, quote-001.txt, photo-notes.txt
outputs/manifest.csv列名の後に、元の名前→新しい名前の対応3行

ターミナルには「완료: 3개 コピー본」と出力フォルダーのパスが表示されます。新しいファイルは名前だけでなく、内容も一つずつ開いて確認します。たとえば、note-draft.txt内の練習用文書番号は、메모 초안.txtの番号と一致する必要があります。

05衝突を意図的に作って停止を確認する

  1. ZIPをもう一つの新しいフォルダーに展開します。既存の結果があるフォルダーでエラーの実験を混ぜないでください。
  2. mapping.csvの二つ目のnew_nameを、一つ目と同じnote-draft.txtに変更して保存します。
  3. python example.py --applyを実行します。「새 이름 충돌」という名前の衝突を示すメッセージが表示されるはずです。
  4. outputsフォルダーが作られておらず、source_files内の三つの元ファイルがすべて残っているか確認します。
  5. 対応表を元の値に戻し、もう一度プレビューから確認します。

コピーのループ内で一つずつ検証すると、最初のファイルを処理してから二つ目で失敗する可能性があります。この例ではすべての名前を先に検査し、事前検証のエラーが部分的な適用につながらないようにしています。既存のoutputsがある場合も、開始前に停止します。

06自分の文書の命名ルールに合わせる

業務に適用するときは、一つのフォルダーの小さなコピーから始めます。source_filesに元ファイルのコピーを入れ、mapping.csvにファイル数と同じだけ行を作ります。拡張子を含む完全な名前を書く必要があります。拡張子を変えてもファイル形式は変換されないため、テキストの内容に対して名前だけを.pdfにするような対応表は作らないでください。

番号を付けるなら001、002のように桁数をそろえ、日付も同じ順序で書くと探しやすくなります。名前に含める項目を先に決め、プレビューで順序と漏れを確認してください。この例は、文書の内容からタイトルを抽出したり、利用者の意図を推測して名前を自動生成したりしません。

07停止メッセージごとの確認箇所

メッセージ確認する点解決方法
새 이름 충돌(新しい名前の衝突)new_nameに同じ値、または大文字・小文字だけが異なる値がある互いに区別できる最終的な名前を指定します。
원본이 중복 지정됨(元ファイルの重複指定)old_nameが二行に繰り返されている元ファイルごとに一行だけ残します。
원본 파일 전체와 일치해야 함(すべての元ファイルとの一致が必要)ファイルの漏れ、またはold_nameの誤字フォルダー内の一覧と対応表を照合し直します。
사용할 수 없는 파일명(使用できないファイル名)パス、予約名、禁止文字、末尾のピリオドファイル名だけを記入し、使用できない文字を除きます。
outputsがすでに存在する前回のコピー結果結果を別名にして保管し、やり直します。

適用後に同じコマンドを繰り返しても、既存の結果は更新しません。プレビューも既存のoutputsを見つけると停止します。別の作業の結果が混ざらないためのルールであり、上書きオプションは用意していません。

08コピー方式の範囲と追加の確認点

この例は元ファイルを読み取るだけで、名前を変えるのはコピーです。元ファイルを保持したうえで、コピー分の保存容量も必要です。文書の内容は保持しますが、作成・更新時刻、アクセス権限、OSのすべての追加メタデータまで同一に複製するツールではありません。

衝突など、コピー前に見つかるエラーでは全体を停止します。ただし、コピー開始後の電源断やディスク容量不足まで、単一のトランザクションのように元に戻すわけではありません。その場合はoutputsに一部のファイルが残る可能性があるため、完了メッセージと三つのファイルを確認してください。他のプログラムが元ファイルや対応表を同時に変更する場合も、別の設計が必要です。

実行・検証の記録

2026-09-19 · Windows 11 · CPython 3.12.14 · 追加パッケージなし · 配布版の一時コピーで実行

  • 通常のプレビューでは出力フォルダーを作らない
  • --applyで作成した3個のコピーのバイト列が元ファイルと一致
  • 同名と大文字・小文字の衝突を出力作成前に拒否
  • 元ファイルの重複・漏れ・不存在、対象フォルダー外へのパス、予約名を拒否
  • 再実行では既存の結果を保全して停止
検証範囲の限界
  • 元ファイル自体の名前は変更しません。
  • ディスクエラー後の部分的な出力を自動で元に戻しません。
  • ファイルのすべてのメタデータを保持するわけではありません。
  • 実行検証はWindowsで行いました。

サイト全体の執筆・検証方針

自分で実行するためのサンプルファイル

コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。

サンプルZIPをダウンロード

サンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。

独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。