|

파이썬 파일명 일괄 변경: pathlib로 접두사와 번호 붙이기

파이썬 파일명 일괄 변경 도구를 pathlib로 만드는 방법을 설명하는 대표 이미지
접두사와 번호를 한 번에 붙이는 작은 파일 정리 도구

파이썬 파일명 일괄 변경은 결과가 눈에 바로 보여서 작은 자동화 연습으로 아주 좋습니다. 특히 다운로드 폴더나 스캔 이미지 폴더처럼 이름이 제각각인 파일들을 한 번에 정리할 때 바로 실용성이 생깁니다.

이번 글에서는 폴더 안 파일들에 접두사와 번호를 한 번에 붙이는 미니 도구를 만들어보겠습니다. 핵심은 pathlib 중심의 경로 처리, 미리보기 모드, 덮어쓰기 방지, 그리고 실행할 때마다 흔들리지 않는 번호 규칙입니다.


먼저 목표를 잡자

예를 들어 banana.png, apple.png, cat.png 같은 파일들을 trip_01.png, trip_02.png, trip_03.png처럼 바꾸고 싶다고 해보겠습니다. 중요한 점은 단순히 이름만 바꾸는 것이 아니라, 다시 실행해도 같은 규칙으로 번호가 붙고 기존 파일을 실수로 덮어쓰지 않게 만드는 것입니다.

  • pathlib로 경로를 읽기 쉽게 다룬다
  • preview 모드로 실제 변경 전에 결과를 확인한다
  • 이미 있는 이름은 먼저 검사해서 충돌을 막는다
  • 정렬 후 번호를 붙여 결정적 numbering을 만든다

왜 pathlib를 쓸까

이 글에서는 pathlib를 중심으로 가겠습니다. 경로를 문자열처럼 이어 붙이기보다 경로 객체로 다루는 편이 훨씬 읽기 쉽고, 파일명 추출이나 확장자 유지 같은 작업도 자연스럽기 때문입니다. 공식 문서도 일반적인 경우에는 Path를 먼저 떠올리면 된다고 설명합니다.

os가 쓸모없다는 뜻은 아닙니다. 입문자 입장에서는 pathlib는 읽기 좋은 경로 처리, os는 운영체제 인터페이스와 예외 감각을 이해하는 쪽으로 나누어 보면 편합니다. Path 객체가 PathLike 인터페이스를 따른다는 점도 함께 알아두면 흐름이 잘 보입니다.

참고한 공식 문서는 pathlib 문서, os 문서, argparse 문서입니다.


가장 짧은 rename 예제

먼저 파일 하나의 이름만 바꾸는 가장 짧은 버전부터 보겠습니다. 여기서 rename 감각을 잡고, 그다음에 여러 파일을 다루는 도구로 키우면 됩니다.

from pathlib import Path

path = Path("apple.png")
new_path = Path("trip_01.png")

path.rename(new_path)

이 코드는 파일 하나의 이름만 바꿉니다. 우리가 원하는 것은 폴더 안 여러 파일을 한 번에 다루는 흐름이므로, 이제 파일 수집, 정렬, 새 이름 계산, 충돌 검사 단계를 추가해야 합니다.


실전형 전체 코드

아래 예제는 같은 폴더 안 파일들에 접두사와 번호를 붙이는 전체 코드입니다. preview 모드, 덮어쓰기 방지, 정렬 기반 번호 부여를 모두 넣었습니다.

from __future__ import annotations

import argparse
import os
from pathlib import Path


def build_target_name(path: Path, prefix: str, number: int, digits: int) -> str:
    suffix = path.suffix
    return f"{prefix}_{number:0{digits}d}{suffix}"


def collect_files(folder: Path, pattern: str) -> list[Path]:
    files = [path for path in folder.iterdir() if path.is_file() and path.match(pattern)]
    return sorted(files, key=lambda path: path.name.casefold())


def plan_renames(files: list[Path], prefix: str, start: int, digits: int) -> list[tuple[Path, Path]]:
    plan: list[tuple[Path, Path]] = []

    for offset, path in enumerate(files, start=start):
        target = path.with_name(build_target_name(path, prefix, offset, digits))
        plan.append((path, target))

    return plan


def validate_plan(plan: list[tuple[Path, Path]]) -> None:
    seen_targets: set[str] = set()

    for source, target in plan:
        target_key = os.fspath(target)

        if target_key in seen_targets:
            raise ValueError(f"같은 새 이름이 두 번 만들어졌습니다: {target.name}")
        seen_targets.add(target_key)

        if source == target:
            raise ValueError(f"변경 전후 이름이 같습니다: {source.name}")

        if target.exists() and target != source:
            raise FileExistsError(f"이미 같은 이름의 파일이 있습니다: {target.name}")


def apply_plan(plan: list[tuple[Path, Path]], preview: bool) -> None:
    for source, target in plan:
        print(f"{source.name} -> {target.name}")

    if preview:
        print("미리보기 모드이므로 실제 변경은 하지 않았습니다.")
        return

    for source, target in plan:
        source.rename(target)

    print(f"총 {len(plan)}개 파일 이름을 변경했습니다.")


def main() -> None:
    parser = argparse.ArgumentParser(description="파일명에 접두사와 번호를 붙이는 일괄 변경기")
    parser.add_argument("folder", help="대상 폴더 경로")
    parser.add_argument("--prefix", required=True, help="붙일 접두사")
    parser.add_argument("--pattern", default="*", help="대상 파일 패턴. 예: *.png")
    parser.add_argument("--start", type=int, default=1, help="시작 번호")
    parser.add_argument("--digits", type=int, default=2, help="번호 자릿수")
    parser.add_argument("--preview", action="store_true", help="실제 변경 없이 결과만 출력")
    args = parser.parse_args()

    folder = Path(args.folder)

    if not folder.exists() or not folder.is_dir():
        raise NotADirectoryError(f"폴더를 찾을 수 없습니다: {folder}")

    files = collect_files(folder, args.pattern)
    if not files:
        raise FileNotFoundError("조건에 맞는 파일이 없습니다.")

    plan = plan_renames(files, args.prefix, args.start, args.digits)
    validate_plan(plan)
    apply_plan(plan, preview=args.preview)


if __name__ == "__main__":
    main()

꼭 봐야 할 포인트

파일 수집 단계에서는 같은 폴더 안 일반 파일만 고르고, 패턴에 맞는 것만 남겼습니다. 처음 버전에서 하위 폴더 재귀 탐색까지 욕심내지 않은 이유는 범위를 좁혀야 예외와 충돌을 다루기 쉽기 때문입니다.

결정적 numbering의 핵심은 정렬입니다. 먼저 이름 기준으로 정렬한 뒤 번호를 붙이면, 같은 파일 집합에 대해 결과가 매번 예측 가능해집니다. 폴더에서 읽힌 순서를 그대로 믿으면 결과가 흔들릴 수 있습니다.

새 이름 계산을 함수 하나로 모아 둔 것도 중요합니다. 접두사 규칙, 자릿수, 확장자 유지 방식을 한 군데에서 관리할 수 있어서 나중에 수정이 쉬워집니다.

validate 단계는 실전 안전장치입니다. 새 이름끼리 서로 충돌하는 경우와 이미 같은 이름의 파일이 존재하는 경우를 먼저 막아야 어중간한 중간 상태를 줄일 수 있습니다.

preview 모드는 선택이 아니라 거의 필수에 가깝습니다. 파일 이름 변경 도구는 잘못 돌리면 되돌리기가 귀찮기 때문에, 먼저 old 에서 new로 어떻게 바뀌는지 눈으로 보고 실행하는 습관이 좋습니다.

번호를 예쁘게 붙이는 것보다 먼저, 다시 실행해도 같은 규칙으로 번호가 붙는지가 더 중요합니다.


실행 예시

이미지 파일만 바꾸고 싶다면 아래처럼 실행할 수 있습니다. 처음에는 preview 모드로 결과만 확인하는 편이 안전합니다.

python bulk_rename.py ./sample --prefix trip --pattern "*.png" --preview

화면에는 old 에서 new로 바뀌는 매핑이 출력됩니다. 확인이 끝났다면 preview 옵션을 빼고 실제 실행하면 됩니다.

apple.png -> trip_01.png
banana.png -> trip_02.png
cat.png -> trip_03.png
미리보기 모드이므로 실제 변경은 하지 않았습니다.
python bulk_rename.py ./sample --prefix trip --pattern "*.png"

번호를 세 자리로 만들고 10번부터 시작하고 싶다면 이렇게 바꿀 수 있습니다.

python bulk_rename.py ./sample --prefix report --pattern "*.pdf" --start 10 --digits 3 --preview

자주 하는 실수

  • 실제 변경 전에 preview 없이 바로 실행한다
  • 정렬 기준 없이 번호를 붙인다
  • 확장자를 빼먹고 새 이름을 만든다
  • 같은 이름 파일이 이미 있는지 검사하지 않는다
  • 폴더가 아닌 경로를 넣었는데 예외 처리를 하지 않는다

특히 정렬 기준 누락과 충돌 검사 누락은 결과가 꼬였을 때 원인을 찾기 어렵게 만듭니다. 처음 버전에서는 기능을 늘리기보다 실패를 먼저 막는 쪽이 훨씬 중요합니다.


정리

파이썬 파일명 일괄 변경 도구는 작지만 배울 것이 꽤 많습니다. pathlib로 경로를 읽기 좋게 다루고, 정렬로 번호 규칙을 고정하고, preview로 먼저 확인하고, 덮어쓰기 충돌을 미리 막는 흐름만 잡아도 실전 감각이 꽤 생깁니다.

한 번 잘 만들어 두면 이미지 정리, PDF 정리, 다운로드 폴더 정리 같은 일에 바로 재사용할 수 있습니다. 같은 흐름의 작은 파이썬 도구를 더 보고 싶다면 파이썬 PDF 합치기 만들기파이썬 터미널 로딩 스피너 만들기도 함께 읽어보면 좋습니다.

함께보면 좋은 글