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

複数ファイルの名前変更を適用する前にプレビューする

新旧のファイル名を表で比較し、競合を確認します。明示的に --apply を指定した場合だけ新しいフォルダにコピーを作成し、元ファイルは保持します。

目次を表示

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

対象読者複数のファイル名を変更したいものの、誤った規則の適用やファイルの上書きを心配している初学者向けです。

準備するもの
  • Python 3.12 以降をインストールし、terminal で python --version を使って version を確認します。
  • example ZIP を新しいフォルダに展開します。ZIP archive 内から直接 example を実行しないでください。
  • example.py があるフォルダで terminal を開きます。Windows で python command が使えない場合は py を使用します。macOS または Linux では、環境によって python3 を使用します。
  • 外部 packages や accounts は不要です。付属ファイルはこの tutorial 用に作成した合成データです。

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

batch operation では、1つの file の typo を見つけることよりも、誤った rule がすべての files に適用されるのを防ぐことが重要です。この例では plan を表示する前に name list 全体を検証します。default command は files を copy したり folders を作成したりしません。plan を確認した後、--apply を付けて operation を別途実行します。

  1. 展開した folder で mapping.csv と source_files 内の3つの original files を確認します。
  2. mapping.csv の old_name と new_name を比較します。左の column が現在の filename、右の column が新しい copy に付ける名前です。
  3. 以下の default command を実行し、矢印で結ばれた3組の names が意図どおりであることを確認します。
  4. この段階では source_files が変更されておらず、outputs もまだ存在していないことを確認します。
bash
python example.py

02ファイル名の mapping を確認する

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

各 mapping row は1つの original と1つの result を対応付けます。old_name は実際の filename と同じ表記で、letter case も含めて正確に入力してください。original を1つでも省略したり、存在しない name を追加したりすると operation 全体が停止します。この rule により、folder に新しい file を追加した後も古い mapping を使い続けるような mistake を検出できます。

new_name の values は、letter case だけが異なる場合でも conflict として扱います。これにより report.txt と REPORT.txt が別々の results になることを防ぎます。leading or trailing spaces、trailing dots、path separators、Windows reserved names も拒否されます。preview では、naming rules が task に合っているかを最後に人が確認する必要があります。

03この例で使用する完全なコード

make_plan はすべての inputs と conflicts を確認します。main は validated plan を表示した後、--apply が指定されたかを確認します。plan を適用する場合でも originals に rename を使わず、新しい names で file contents を copy するため、開始時の状態を引き続き確認できます。

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 option により、--apply を省略した場合は preview が default になります。copy destinations は xb mode で開かれるため、すでに存在する file を上書きしません。完了した mapping は manifest.csv にも記録されるので、新しい name から original name を追跡できます。

04確認済み plan から copies を作成する

3つの preview lines を確認したら、同じ folder で以下の command を実行します。preview 後に mapping.csv を編集した場合は、default command をもう一度実行して新しい plan を確認してください。apply command は前回の preview を記憶していません。実行時に mapping を再読み込みして検証します。

bash
python example.py --apply
Location想定結果
source_files/既存 names のまま3つの original files を保持
outputs/renamed/note-draft.txt, quote-001.txt, photo-notes.txt
outputs/manifest.csvheader の後に original → new name mapping rows が3行

terminal には “완료: 3개 Copy본” (complete: three copies) と output folder path が表示されます。各 new file を開き、name だけでなく contents も確認してください。たとえば note-draft.txt 内の practice document number は 메모 초안.txt の number と一致するはずです。

05競合を作り、script が停止することを確認する

  1. 別の copy を新しい folder に展開します。すでに results がある folder と error tests を混在させないでください。
  2. mapping.csv で2つ目の new_name を note-draft.txt に変更し、1つ目と同じにして保存します。
  3. python example.py --apply を実行します。“새 이름 충돌” (new-name collision) message が表示されるはずです。
  4. outputs folder が作成されておらず、3つの originals がすべて source_files に残っていることを確認します。
  5. 元の mapping values に戻し、preview step から再度確認します。

validation を copy loop 内で1 file ずつ行うと、2つ目で failure が発生する前に1つ目が処理される可能性があります。この例では最初にすべての names を確認するため、このような pre-check errors で partial application が発生しません。また、outputs がすでに存在する場合も開始前に停止します。

06独自の文書命名規則を使用する

実務に適用するときは、まず1つの folder の小さな copy から始めてください。originals の copies を source_files に入れ、mapping.csv に file ごとに1 row を書きます。extension を含む full name を使用してください。extension を変えても file format は変換されないため、text file を .pdf で終わる name に mapping しないでください。

numbers を追加する場合は 001, 002 のように固定桁数を使い、dates は一貫した順序にすると results を見つけやすくなります。まず name に含める fields を決め、その後 preview で順序と omissions を確認します。この例では document contents から titles を抽出したり、intent を推測して names を自動生成したりはしません。

07各停止 message で確認すること

Message確認すること解決方法
New-name collision (새 이름 충돌)同一の new_name values、または letter case だけが異なる values重複しない final names を選びます。
Original specified more than onceold_name が2 rows に重複original ごとに1 row だけ残します。
Must match the complete set of original filesmissing file または old_name の typofolder contents と mapping を再度比較します。
Invalid filenamePaths, reserved names, forbidden characters, trailing dotsfilename だけを入力し、restricted characters を取り除きます。
outputs already exists以前の copy operation の resultsresult folder を rename して保持してから、再度開始します。

plan を適用した後に同じ command を繰り返しても、既存 results は update されません。preview であっても existing outputs folder が見つかれば停止します。この rule により、別々の operations の results が混ざるのを防ぎます。overwrite option はありません。

08copy の制限と、引き続き必要な確認

この例では original files を読み取り、その copies に new names を付けるだけです。originals と copies の両方を保持するため、追加 storage が必要です。document contents は保持しますが、creation and modification times, access permissions, その他すべての operating-system metadata を複製する tool ではありません。

name conflicts など copy 前に見つかる errors は operation 全体を停止します。一度 copying が始まった後の power failure や insufficient disk space は、single transaction として rollback されません。一部の files が outputs に残る場合があるため、completion message と3つすべての files を確認してください。別 program による originals や mapping の concurrent changes も別の design が必要です。

実行・検証の記録

2026-09-19 · Windows 11 · CPython 3.12.14 · 追加 packages なし · distribution の temporary copy で実行

  • default preview が output folder を作成しないことを確認
  • --apply で作成した3つの copies が originals と byte-for-byte で同一であることを確認
  • 同一 name と case-only conflicts が output 作成前に拒否されることを確認
  • duplicate または omitted originals, nonexistent originals, path traversal, reserved names が拒否されることを確認
  • 再実行時に existing results を保持したまま停止することを確認
検証範囲の限界
  • original files 自体は rename されません。
  • disk error 後の partial output は自動 rollback されません。
  • すべての file metadata が保持されるわけではありません。
  • Windows での実行を確認しました。

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

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

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

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

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

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

参考資料

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