폴더 속 파일 목록을 CSV로 정리하기
하위 폴더까지 읽어 파일 경로·확장자·크기·수정 시각을 표로 남깁니다. 작은 예제 4개로 시작하고 원본과 기존 결과를 보존합니다.
바꾸기 전 이름과 새 이름을 표로 확인하고 충돌을 검사합니다. --apply를 명시할 때만 새 폴더에 복사본을 만들어 원본을 남깁니다.
이런 분께여러 파일의 이름을 바꾸고 싶지만 잘못된 규칙이나 덮어쓰기가 걱정되는 입문자
일괄 변경에서는 한 파일의 오타보다 잘못된 규칙이 모든 파일에 적용되는 상황을 먼저 막아야 합니다. 이 예제는 이름 목록을 전부 검증한 다음 계획을 보여 줍니다. 기본 명령은 파일을 복사하거나 폴더를 만들지 않습니다. 실제 작업은 확인 후 --apply를 붙여 별도로 실행합니다.
python example.py| old_name | new_name |
|---|---|
| 메모 초안.txt | note-draft.txt |
| 견적 1.txt | quote-001.txt |
| 사진 설명.txt | photo-notes.txt |
매핑의 각 행은 한 원본과 한 결과를 연결합니다. old_name은 실제 이름과 대소문자까지 정확히 맞춰 적습니다. 원본을 빠뜨리거나 없는 이름을 추가하면 전체 작업이 중지됩니다. 이 규칙 덕분에 폴더에 새 파일이 들어왔는데 예전 매핑을 그대로 실행하는 실수를 발견할 수 있습니다.
new_name끼리는 대소문자 차이만 있어도 충돌로 봅니다. report.txt와 REPORT.txt를 서로 다른 결과로 만들지 않도록 한 선택입니다. 이름 앞뒤 공백, 끝의 점, 경로 구분자, Windows 예약 이름도 거부합니다. 이름 정리 규칙이 업무에 맞는지는 미리보기에서 사람이 최종적으로 확인해야 합니다.
make_plan에서 모든 입력과 충돌을 검사합니다. main은 검증된 계획을 출력한 뒤 --apply 여부를 확인합니다. 실제 적용에서도 원본 이름을 rename하지 않고 파일 내용을 새 이름으로 복사하므로 시작 상태를 다시 확인할 수 있습니다.
"""기본은 미리보기입니다. --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에도 기록해 새 이름에서 원본 이름을 역으로 찾을 수 있게 했습니다.
미리보기의 세 줄을 확인했다면 같은 폴더에서 다음 명령을 실행합니다. 미리보기 이후 mapping.csv를 수정했다면 기본 명령을 다시 실행해 새 계획을 확인하세요. 적용 명령은 이전 미리보기 화면을 기억하는 방식이 아니라 실행 시점의 매핑을 다시 읽고 검사합니다.
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의 번호가 같아야 합니다.
검증을 복사 루프 안에서 한 파일씩 했다면 첫 파일을 처리한 뒤 두 번째에서 실패할 수 있습니다. 이 예제는 전체 이름을 먼저 검사해 이러한 사전 검증 오류가 부분 적용으로 이어지지 않게 했습니다. 기존 outputs가 있는 경우도 시작 전에 중지합니다.
업무에 적용할 때는 한 폴더의 작은 사본부터 시작합니다. source_files에 원본 사본을 넣고 mapping.csv에 파일 수만큼 행을 작성합니다. 확장자까지 포함한 전체 이름을 써야 합니다. 확장자는 파일 형식을 변환하지 않으므로 내용은 텍스트인데 이름만 .pdf로 바꾸는 식의 매핑은 만들지 마세요.
번호를 붙인다면 001, 002처럼 자릿수를 정하고 날짜는 같은 순서로 쓰면 결과를 찾기 쉽습니다. 이름에 넣을 항목을 먼저 합의한 뒤 미리보기에서 순서와 누락을 확인하세요. 이 예제는 문서 내용에서 제목을 추출하거나 사용자의 의도를 추정해 이름을 자동 생성하지 않습니다.
| 메시지 | 확인할 점 | 해결 |
|---|---|---|
| 새 이름 충돌 | new_name에 같은 값 또는 대소문자만 다른 값 | 서로 구분되는 최종 이름을 지정합니다. |
| 원본이 중복 지정됨 | old_name이 두 행에 반복됨 | 각 원본에 한 행만 남깁니다. |
| 원본 파일 전체와 일치해야 함 | 누락된 파일 또는 old_name 오타 | 폴더 목록과 매핑을 다시 대조합니다. |
| 사용할 수 없는 파일명 | 경로·예약 이름·금지 문자·끝 점 | 파일명만 적고 제한된 문자를 뺍니다. |
| outputs가 이미 있음 | 이전 복사 결과 | 결과를 보관할 다른 이름으로 옮기고 다시 시작합니다. |
적용 후 같은 명령을 반복해도 기존 결과를 갱신하지 않습니다. 미리보기도 기존 outputs를 발견하면 중지합니다. 한 작업의 결과를 다른 작업과 섞지 않기 위한 규칙이며, 덮어쓰기 옵션은 제공하지 않습니다.
이 예제는 원본 파일을 읽기만 하고 복사본의 이름을 바꿉니다. 저장 공간은 원본과 복사본만큼 추가로 필요합니다. 문서 내용은 보존하지만 생성·수정 시각, 접근 권한, 운영체제의 모든 부가 메타데이터까지 동일하게 복제하는 도구는 아닙니다.
충돌처럼 복사 전에 발견하는 오류는 전체 중지합니다. 복사를 시작한 뒤 전원이 꺼지거나 디스크 공간이 부족해지는 경우까지 하나의 거래처럼 되돌리지는 않습니다. 이때 outputs에 일부 파일이 남을 수 있으므로 완료 문구와 세 파일을 확인하세요. 동시에 다른 프로그램이 원본이나 매핑을 바꾸는 상황도 별도 설계가 필요합니다.
2026-09-19 · Windows 11 · CPython 3.12.14 · 추가 패키지 없음 · 배포본의 임시 복사본에서 실행
코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.
예제 ZIP 다운로드직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.