Python 자동화 마스터 2편: 파일 및 폴더 자동화
Python Automation Master Part 2: File and Folder Automation
서론: 파일 자동화의 필요성
컴퓨터를 사용하다 보면 파일과 폴더를 다루는 작업에 상당한 시간을 소비하게 됩니다. 수백 개의 파일 이름을 바꾸거나, 특정 조건의 파일을 찾아 정리하거나, 백업을 위해 파일을 복사하는 등의 작업은 수작업으로 하면 시간이 오래 걸리고 실수하기도 쉽습니다.
Python은 파일 시스템을 다루는 강력한 도구들을 제공합니다. 이번 편에서는 Python의 os, pathlib, shutil, glob 모듈을 활용하여 파일과 폴더를 효율적으로 자동화하는 방법을 배워보겠습니다.
1. os 모듈 기초
os 모듈은 운영체제와 상호작용하기 위한 Python의 기본 모듈입니다. 파일 시스템 작업에 필수적인 기능들을 제공합니다.
1.1 기본 경로 작업
import os
# 현재 작업 디렉토리 확인
current_dir = os.getcwd()
print(f"현재 디렉토리: {current_dir}")
# 작업 디렉토리 변경
os.chdir("/path/to/directory")
# 환경 변수 접근
home_dir = os.environ.get('HOME') # Linux/Mac
user_profile = os.environ.get('USERPROFILE') # Windows
# 사용자 홈 디렉토리 (크로스 플랫폼)
home = os.path.expanduser("~")
print(f"홈 디렉토리: {home}")
1.2 경로 조작
import os
# 경로 결합 (운영체제에 맞는 구분자 사용)
full_path = os.path.join("folder", "subfolder", "file.txt")
print(full_path) # Windows: folder\subfolder\file.txt
# 경로 분리
directory = os.path.dirname("/path/to/file.txt") # /path/to
filename = os.path.basename("/path/to/file.txt") # file.txt
# 파일명과 확장자 분리
name, extension = os.path.splitext("document.pdf")
print(f"파일명: {name}, 확장자: {extension}") # document, .pdf
# 절대 경로 얻기
abs_path = os.path.abspath("relative/path/file.txt")
# 경로 존재 여부 확인
if os.path.exists("/path/to/check"):
print("경로가 존재합니다.")
# 파일인지 폴더인지 확인
if os.path.isfile("/path/to/file.txt"):
print("파일입니다.")
if os.path.isdir("/path/to/folder"):
print("폴더입니다.")
1.3 디렉토리 목록 조회
import os
# 디렉토리 내 항목 목록
items = os.listdir("/path/to/directory")
print(items) # ['file1.txt', 'folder1', 'file2.py']
# 파일만 필터링
files = [f for f in os.listdir(".") if os.path.isfile(f)]
# 폴더만 필터링
folders = [f for f in os.listdir(".") if os.path.isdir(f)]
# 특정 확장자 파일만 필터링
python_files = [f for f in os.listdir(".") if f.endswith(".py")]
1.4 디렉토리 순회 (os.walk)
import os
# 모든 하위 디렉토리 순회
for root, dirs, files in os.walk("/path/to/start"):
print(f"현재 디렉토리: {root}")
print(f"하위 폴더: {dirs}")
print(f"파일들: {files}")
print("-" * 50)
# 특정 확장자 파일 모두 찾기
def find_files_by_extension(start_path, extension):
"""지정된 경로에서 특정 확장자의 파일을 모두 찾습니다."""
found_files = []
for root, dirs, files in os.walk(start_path):
for file in files:
if file.endswith(extension):
full_path = os.path.join(root, file)
found_files.append(full_path)
return found_files
# 모든 .txt 파일 찾기
txt_files = find_files_by_extension(".", ".txt")
for f in txt_files:
print(f)
2. pathlib 모듈 활용
pathlib은 Python 3.4에서 도입된 객체 지향적 파일 시스템 경로 라이브러리입니다. os.path보다 직관적이고 현대적인 방식으로 경로를 다룰 수 있습니다.
2.1 Path 객체 기본
from pathlib import Path
# Path 객체 생성
p = Path("/path/to/file.txt")
current = Path(".")
home = Path.home()
# 경로 결합 (/ 연산자 사용)
full_path = Path.home() / "Documents" / "project" / "file.txt"
print(full_path)
# 경로 속성
print(p.name) # file.txt (파일명)
print(p.stem) # file (확장자 제외 파일명)
print(p.suffix) # .txt (확장자)
print(p.parent) # /path/to (부모 디렉토리)
print(p.parts) # ('/', 'path', 'to', 'file.txt')
# 절대 경로
abs_path = Path("relative/path").resolve()
# 존재 여부 확인
if p.exists():
print("존재합니다.")
if p.is_file():
print("파일입니다.")
if p.is_dir():
print("디렉토리입니다.")
2.2 디렉토리 탐색
from pathlib import Path
# 현재 디렉토리의 모든 항목
for item in Path(".").iterdir():
print(item)
# 특정 패턴 매칭 (현재 디렉토리만)
for py_file in Path(".").glob("*.py"):
print(py_file)
# 재귀적 패턴 매칭 (모든 하위 디렉토리)
for py_file in Path(".").rglob("*.py"):
print(py_file)
# 여러 확장자 검색
extensions = ["*.jpg", "*.png", "*.gif"]
images = []
for ext in extensions:
images.extend(Path(".").rglob(ext))
# 파일만 필터링
files_only = [p for p in Path(".").iterdir() if p.is_file()]
# 폴더만 필터링
dirs_only = [p for p in Path(".").iterdir() if p.is_dir()]
2.3 파일/폴더 생성 및 삭제
from pathlib import Path
# 디렉토리 생성
new_dir = Path("new_folder")
new_dir.mkdir(exist_ok=True) # 이미 존재해도 에러 없음
# 중첩 디렉토리 생성
nested_dir = Path("parent/child/grandchild")
nested_dir.mkdir(parents=True, exist_ok=True)
# 파일 생성 (빈 파일)
new_file = Path("new_file.txt")
new_file.touch()
# 파일 삭제
if new_file.exists():
new_file.unlink()
# 빈 디렉토리 삭제
if new_dir.exists() and new_dir.is_dir():
new_dir.rmdir() # 디렉토리가 비어있어야 함
# 파일명 변경
old_path = Path("old_name.txt")
new_path = Path("new_name.txt")
if old_path.exists():
old_path.rename(new_path)
3. 파일 읽기/쓰기
Python에서 파일을 읽고 쓰는 방법을 알아봅니다. with문을 사용하면 파일을 자동으로 닫아주므로 권장됩니다.
3.1 텍스트 파일 처리
# 파일 쓰기
with open("example.txt", "w", encoding="utf-8") as f:
f.write("첫 번째 줄\n")
f.write("두 번째 줄\n")
# 파일에 내용 추가
with open("example.txt", "a", encoding="utf-8") as f:
f.write("추가된 줄\n")
# 파일 읽기 (전체)
with open("example.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# 파일 읽기 (줄 단위)
with open("example.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip()) # 줄바꿈 문자 제거
# 파일 읽기 (리스트로)
with open("example.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
print(lines) # ['첫 번째 줄\n', '두 번째 줄\n', ...]
# 여러 줄 한 번에 쓰기
lines_to_write = ["줄 1\n", "줄 2\n", "줄 3\n"]
with open("output.txt", "w", encoding="utf-8") as f:
f.writelines(lines_to_write)
3.2 pathlib으로 파일 읽기/쓰기
from pathlib import Path
# 간편한 파일 쓰기
Path("simple.txt").write_text("Hello, World!", encoding="utf-8")
# 간편한 파일 읽기
content = Path("simple.txt").read_text(encoding="utf-8")
print(content)
# 바이너리 파일 읽기
binary_content = Path("image.png").read_bytes()
# 바이너리 파일 쓰기
Path("copy.png").write_bytes(binary_content)
3.3 CSV 파일 처리
import csv
# CSV 쓰기
data = [
["이름", "나이", "도시"],
["홍길동", 30, "서울"],
["김철수", 25, "부산"],
["이영희", 28, "대구"]
]
with open("data.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerows(data)
# CSV 읽기
with open("data.csv", "r", encoding="utf-8-sig") as f:
reader = csv.reader(f)
for row in reader:
print(row)
# 딕셔너리로 CSV 다루기
with open("data.csv", "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row in reader:
print(f"{row['이름']}은 {row['나이']}살입니다.")
3.4 JSON 파일 처리
import json
# JSON 쓰기
data = {
"name": "홍길동",
"age": 30,
"skills": ["Python", "JavaScript", "SQL"],
"address": {
"city": "서울",
"district": "강남구"
}
}
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# JSON 읽기
with open("data.json", "r", encoding="utf-8") as f:
loaded_data = json.load(f)
print(loaded_data["name"])
print(loaded_data["skills"])
4. 폴더 생성/삭제/이동
4.1 폴더 생성
import os
from pathlib import Path
# os로 폴더 생성
os.makedirs("parent/child/grandchild", exist_ok=True)
# pathlib으로 폴더 생성
Path("another/nested/folder").mkdir(parents=True, exist_ok=True)
# 날짜별 폴더 생성
from datetime import datetime
today = datetime.now().strftime("%Y-%m-%d")
daily_folder = Path("backups") / today
daily_folder.mkdir(parents=True, exist_ok=True)
4.2 폴더 삭제
import os
import shutil
from pathlib import Path
# 빈 폴더 삭제
os.rmdir("empty_folder")
# 또는
Path("empty_folder").rmdir()
# 내용이 있는 폴더 삭제 (주의: 복구 불가!)
shutil.rmtree("folder_with_contents")
# 안전한 삭제 (확인 후 삭제)
def safe_delete_folder(folder_path):
"""폴더를 안전하게 삭제합니다."""
path = Path(folder_path)
if not path.exists():
print(f"'{folder_path}'가 존재하지 않습니다.")
return False
# 폴더 내용 확인
items = list(path.rglob("*"))
file_count = sum(1 for item in items if item.is_file())
folder_count = sum(1 for item in items if item.is_dir())
print(f"삭제할 폴더: {folder_path}")
print(f"포함된 파일: {file_count}개")
print(f"포함된 폴더: {folder_count}개")
confirm = input("정말 삭제하시겠습니까? (yes/no): ")
if confirm.lower() == "yes":
shutil.rmtree(path)
print("삭제 완료!")
return True
else:
print("취소되었습니다.")
return False
5. 파일 검색 (glob)
glob 모듈은 Unix 셸 스타일의 패턴 매칭을 사용하여 파일을 검색합니다.
5.1 glob 기본 사용법
import glob
# 현재 디렉토리의 모든 .txt 파일
txt_files = glob.glob("*.txt")
# 특정 폴더의 모든 .py 파일
py_files = glob.glob("src/*.py")
# 재귀적 검색 (모든 하위 폴더)
all_py_files = glob.glob("**/*.py", recursive=True)
# 여러 확장자 검색
import itertools
extensions = ["*.jpg", "*.png", "*.gif"]
images = list(itertools.chain.from_iterable(
glob.glob(ext, recursive=True) for ext in ["**/" + e for e in extensions]
))
# 패턴 매칭 예시
# ? : 단일 문자
# * : 모든 문자 (0개 이상)
# [abc] : a, b, c 중 하나
# [0-9] : 숫자
files = glob.glob("file?.txt") # file1.txt, fileA.txt
files = glob.glob("data[0-9].csv") # data0.csv ~ data9.csv
files = glob.glob("[!_]*.py") # 언더스코어로 시작하지 않는 .py
5.2 pathlib의 glob
from pathlib import Path
# 현재 디렉토리에서 검색
for txt_file in Path(".").glob("*.txt"):
print(txt_file)
# 재귀적 검색
for py_file in Path(".").rglob("*.py"):
print(py_file)
# 복잡한 패턴
for file in Path("data").glob("**/report_*.xlsx"):
print(file)
6. 파일 복사/이동 (shutil)
shutil 모듈은 파일과 폴더의 고수준 작업(복사, 이동, 삭제)을 위한 기능을 제공합니다.
6.1 파일 복사
import shutil
# 파일 복사 (메타데이터 미포함)
shutil.copy("source.txt", "destination.txt")
# 파일 복사 (메타데이터 포함)
shutil.copy2("source.txt", "destination.txt")
# 폴더로 복사 (원본 파일명 유지)
shutil.copy("source.txt", "backup_folder/")
# 폴더 전체 복사
shutil.copytree("source_folder", "destination_folder")
# 특정 파일만 제외하고 복사
def ignore_patterns(directory, files):
"""특정 패턴의 파일을 무시합니다."""
return [f for f in files if f.endswith('.pyc') or f.startswith('.')]
shutil.copytree("source", "dest", ignore=ignore_patterns)
# 또는 내장 함수 사용
shutil.copytree("source", "dest",
ignore=shutil.ignore_patterns('*.pyc', '*.tmp', '__pycache__'))
6.2 파일 이동
import shutil
# 파일 이동
shutil.move("source.txt", "new_location/source.txt")
# 파일 이름 변경 (같은 폴더 내 이동)
shutil.move("old_name.txt", "new_name.txt")
# 폴더 이동
shutil.move("source_folder", "new_location/")
# 안전한 이동 함수
from pathlib import Path
def safe_move(source, destination):
"""파일을 안전하게 이동합니다."""
src = Path(source)
dst = Path(destination)
if not src.exists():
print(f"원본 '{source}'가 존재하지 않습니다.")
return False
# 대상이 폴더면 원본 파일명 유지
if dst.is_dir():
dst = dst / src.name
# 대상이 이미 존재하면 확인
if dst.exists():
confirm = input(f"'{dst}'가 이미 존재합니다. 덮어쓰시겠습니까? (y/n): ")
if confirm.lower() != 'y':
print("취소되었습니다.")
return False
shutil.move(str(src), str(dst))
print(f"'{source}' -> '{dst}' 이동 완료")
return True
7. 파일명 일괄 변경
파일명을 일괄적으로 변경하는 것은 자동화의 대표적인 활용 사례입니다.
7.1 기본 파일명 변경
import os
from pathlib import Path
# 접두사 추가
def add_prefix(folder, prefix):
"""폴더 내 모든 파일에 접두사를 추가합니다."""
folder_path = Path(folder)
for file in folder_path.iterdir():
if file.is_file():
new_name = folder_path / f"{prefix}{file.name}"
file.rename(new_name)
print(f"'{file.name}' -> '{new_name.name}'")
# 접미사 추가
def add_suffix(folder, suffix):
"""파일명에 접미사를 추가합니다 (확장자 전)."""
folder_path = Path(folder)
for file in folder_path.iterdir():
if file.is_file():
new_name = folder_path / f"{file.stem}{suffix}{file.suffix}"
file.rename(new_name)
print(f"'{file.name}' -> '{new_name.name}'")
7.2 순번으로 파일명 변경
from pathlib import Path
def rename_with_sequence(folder, base_name, start=1, padding=3):
"""파일명을 순번으로 변경합니다.
예: image_001.jpg, image_002.jpg, ...
"""
folder_path = Path(folder)
files = sorted([f for f in folder_path.iterdir() if f.is_file()])
for i, file in enumerate(files, start=start):
# 순번 형식화 (001, 002, ...)
sequence = str(i).zfill(padding)
new_name = folder_path / f"{base_name}_{sequence}{file.suffix}"
file.rename(new_name)
print(f"'{file.name}' -> '{new_name.name}'")
7.3 날짜 기반 파일명 변경
from pathlib import Path
from datetime import datetime
import os
def rename_with_date(folder, include_time=False):
"""파일의 수정 날짜를 기반으로 이름을 변경합니다."""
folder_path = Path(folder)
for file in folder_path.iterdir():
if file.is_file():
# 파일 수정 시간 가져오기
mtime = os.path.getmtime(file)
date_obj = datetime.fromtimestamp(mtime)
if include_time:
date_str = date_obj.strftime("%Y%m%d_%H%M%S")
else:
date_str = date_obj.strftime("%Y%m%d")
new_name = folder_path / f"{date_str}_{file.name}"
# 중복 방지
counter = 1
while new_name.exists():
new_name = folder_path / f"{date_str}_{counter}_{file.name}"
counter += 1
file.rename(new_name)
print(f"'{file.name}' -> '{new_name.name}'")
7.4 정규표현식으로 파일명 변경
import re
from pathlib import Path
def rename_with_regex(folder, pattern, replacement):
"""정규표현식을 사용하여 파일명을 변경합니다."""
folder_path = Path(folder)
for file in folder_path.iterdir():
if file.is_file():
new_name = re.sub(pattern, replacement, file.stem)
if new_name != file.stem:
new_path = folder_path / f"{new_name}{file.suffix}"
file.rename(new_path)
print(f"'{file.name}' -> '{new_path.name}'")
# 사용 예시
# 공백을 언더스코어로
rename_with_regex(".", r"\s+", "_")
# 특수문자 제거
rename_with_regex(".", r"[^\w\-_.]", "")
8. 중복 파일 찾기
하드 디스크 공간을 절약하기 위해 중복 파일을 찾아 정리하는 스크립트를 작성해봅니다.
import hashlib
from pathlib import Path
from collections import defaultdict
def get_file_hash(filepath, chunk_size=8192):
"""파일의 MD5 해시값을 계산합니다."""
hasher = hashlib.md5()
with open(filepath, 'rb') as f:
while chunk := f.read(chunk_size):
hasher.update(chunk)
return hasher.hexdigest()
def find_duplicates(folder):
"""폴더 내 중복 파일을 찾습니다."""
folder_path = Path(folder)
# 1단계: 파일 크기로 그룹화
size_dict = defaultdict(list)
for file in folder_path.rglob("*"):
if file.is_file():
size_dict[file.stat().st_size].append(file)
# 2단계: 같은 크기의 파일들만 해시 비교
hash_dict = defaultdict(list)
for size, files in size_dict.items():
if len(files) > 1: # 같은 크기의 파일이 2개 이상일 때만
for file in files:
file_hash = get_file_hash(file)
hash_dict[file_hash].append(file)
# 3단계: 중복 파일 필터링
duplicates = {h: files for h, files in hash_dict.items() if len(files) > 1}
return duplicates
def report_duplicates(folder):
"""중복 파일 보고서를 출력합니다."""
duplicates = find_duplicates(folder)
if not duplicates:
print("중복 파일이 없습니다.")
return
total_wasted = 0
print("=" * 60)
print("중복 파일 보고서")
print("=" * 60)
for hash_value, files in duplicates.items():
file_size = files[0].stat().st_size
wasted = file_size * (len(files) - 1)
total_wasted += wasted
print(f"\n해시: {hash_value[:16]}...")
print(f"파일 크기: {file_size:,} bytes")
print(f"중복 개수: {len(files)}개")
print("파일 목록:")
for f in files:
print(f" - {f}")
print("\n" + "=" * 60)
print(f"총 낭비 공간: {total_wasted:,} bytes ({total_wasted / 1024 / 1024:.2f} MB)")
print("=" * 60)
def delete_duplicates(folder, keep='first'):
"""중복 파일을 삭제합니다.
Args:
folder: 대상 폴더
keep: 'first' (첫 번째 유지) 또는 'newest' (최신 파일 유지)
"""
duplicates = find_duplicates(folder)
deleted_count = 0
freed_space = 0
for hash_value, files in duplicates.items():
if keep == 'newest':
# 수정 시간 기준 정렬 (최신 먼저)
files = sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
# 첫 번째 파일 유지, 나머지 삭제
for file in files[1:]:
file_size = file.stat().st_size
print(f"삭제: {file}")
file.unlink()
deleted_count += 1
freed_space += file_size
print(f"\n삭제된 파일: {deleted_count}개")
print(f"확보된 공간: {freed_space:,} bytes ({freed_space / 1024 / 1024:.2f} MB)")
9. 파일 정리 자동화 프로젝트
지금까지 배운 내용을 종합하여 실용적인 파일 정리 자동화 프로젝트를 만들어봅니다.
"""
고급 파일 정리 자동화 스크립트
- 확장자별 분류
- 날짜별 분류
- 크기별 분류
- 중복 파일 처리
- 로깅 지원
"""
import os
import shutil
import hashlib
import logging
from pathlib import Path
from datetime import datetime
from collections import defaultdict
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('file_organizer.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 확장자별 카테고리 매핑
CATEGORIES = {
'Images': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp', '.ico'],
'Documents': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.rtf', '.odt'],
'Videos': ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm'],
'Music': ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a'],
'Archives': ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
'Programs': ['.exe', '.msi', '.dmg', '.deb', '.rpm', '.apk'],
'Code': ['.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.h', '.json', '.xml'],
'Data': ['.csv', '.sql', '.db', '.sqlite']
}
def get_category(extension):
"""확장자에 해당하는 카테고리를 반환합니다."""
ext_lower = extension.lower()
for category, extensions in CATEGORIES.items():
if ext_lower in extensions:
return category
return 'Others'
def get_file_hash(filepath):
"""파일의 MD5 해시를 계산합니다."""
hasher = hashlib.md5()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
hasher.update(chunk)
return hasher.hexdigest()
class FileOrganizer:
def __init__(self, source_folder, dest_folder=None):
self.source = Path(source_folder)
self.dest = Path(dest_folder) if dest_folder else self.source
self.stats = {
'moved': 0,
'skipped': 0,
'duplicates': 0,
'errors': 0
}
self.hash_dict = defaultdict(list)
def organize_by_extension(self):
"""파일을 확장자별로 정리합니다."""
logger.info(f"확장자별 정리 시작: {self.source}")
for file in self.source.iterdir():
if file.is_file():
category = get_category(file.suffix)
target_folder = self.dest / category
target_folder.mkdir(exist_ok=True)
target_path = target_folder / file.name
# 중복 파일명 처리
if target_path.exists():
target_path = self._get_unique_path(target_path)
try:
shutil.move(str(file), str(target_path))
logger.info(f"이동: {file.name} -> {category}/")
self.stats['moved'] += 1
except Exception as e:
logger.error(f"오류: {file.name} - {e}")
self.stats['errors'] += 1
self._print_stats()
def organize_by_date(self, date_format="%Y-%m"):
"""파일을 수정 날짜별로 정리합니다."""
logger.info(f"날짜별 정리 시작: {self.source}")
for file in self.source.iterdir():
if file.is_file():
mtime = datetime.fromtimestamp(file.stat().st_mtime)
date_folder = mtime.strftime(date_format)
target_folder = self.dest / date_folder
target_folder.mkdir(exist_ok=True)
target_path = target_folder / file.name
if target_path.exists():
target_path = self._get_unique_path(target_path)
try:
shutil.move(str(file), str(target_path))
logger.info(f"이동: {file.name} -> {date_folder}/")
self.stats['moved'] += 1
except Exception as e:
logger.error(f"오류: {file.name} - {e}")
self.stats['errors'] += 1
self._print_stats()
def organize_by_size(self, thresholds=None):
"""파일을 크기별로 정리합니다."""
if thresholds is None:
thresholds = {
'Tiny (< 100KB)': 100 * 1024,
'Small (100KB - 1MB)': 1024 * 1024,
'Medium (1MB - 100MB)': 100 * 1024 * 1024,
'Large (100MB - 1GB)': 1024 * 1024 * 1024,
'Huge (> 1GB)': float('inf')
}
logger.info(f"크기별 정리 시작: {self.source}")
for file in self.source.iterdir():
if file.is_file():
size = file.stat().st_size
for category, threshold in thresholds.items():
if size < threshold:
target_folder = self.dest / category
break
target_folder.mkdir(exist_ok=True)
target_path = target_folder / file.name
if target_path.exists():
target_path = self._get_unique_path(target_path)
try:
shutil.move(str(file), str(target_path))
logger.info(f"이동: {file.name} ({size:,} bytes) -> {category}/")
self.stats['moved'] += 1
except Exception as e:
logger.error(f"오류: {file.name} - {e}")
self.stats['errors'] += 1
self._print_stats()
def find_and_handle_duplicates(self, action='report'):
"""중복 파일을 찾아 처리합니다.
Args:
action: 'report' (보고만), 'move' (별도 폴더로 이동), 'delete' (삭제)
"""
logger.info(f"중복 파일 검색 시작: {self.source}")
# 파일 해시 계산
for file in self.source.rglob("*"):
if file.is_file():
try:
file_hash = get_file_hash(file)
self.hash_dict[file_hash].append(file)
except Exception as e:
logger.error(f"해시 계산 오류: {file} - {e}")
# 중복 파일 처리
duplicates_folder = self.dest / "Duplicates"
for hash_value, files in self.hash_dict.items():
if len(files) > 1:
logger.info(f"중복 발견: {len(files)}개 파일")
# 첫 번째 파일 유지
original = files[0]
for dup in files[1:]:
self.stats['duplicates'] += 1
if action == 'report':
logger.info(f" 중복: {dup} (원본: {original})")
elif action == 'move':
duplicates_folder.mkdir(exist_ok=True)
target = duplicates_folder / dup.name
if target.exists():
target = self._get_unique_path(target)
shutil.move(str(dup), str(target))
logger.info(f" 이동: {dup} -> Duplicates/")
elif action == 'delete':
dup.unlink()
logger.info(f" 삭제: {dup}")
self._print_stats()
def _get_unique_path(self, path):
"""중복되지 않는 파일 경로를 반환합니다."""
counter = 1
new_path = path
while new_path.exists():
new_path = path.parent / f"{path.stem}_{counter}{path.suffix}"
counter += 1
return new_path
def _print_stats(self):
"""통계를 출력합니다."""
logger.info("=" * 50)
logger.info("처리 결과:")
logger.info(f" 이동된 파일: {self.stats['moved']}개")
logger.info(f" 건너뛴 파일: {self.stats['skipped']}개")
logger.info(f" 중복 파일: {self.stats['duplicates']}개")
logger.info(f" 오류: {self.stats['errors']}개")
logger.info("=" * 50)
def main():
"""메인 실행 함수"""
print("=" * 60)
print("고급 파일 정리 자동화")
print("=" * 60)
source = input("정리할 폴더 경로: ").strip()
if not source:
source = str(Path.home() / "Downloads")
print(f"기본 경로 사용: {source}")
print("\n정리 방식을 선택하세요:")
print("1. 확장자별 정리")
print("2. 날짜별 정리")
print("3. 크기별 정리")
print("4. 중복 파일 찾기")
print("5. 모든 정리 수행")
choice = input("\n선택 (1-5): ").strip()
organizer = FileOrganizer(source)
if choice == '1':
organizer.organize_by_extension()
elif choice == '2':
organizer.organize_by_date()
elif choice == '3':
organizer.organize_by_size()
elif choice == '4':
action = input("중복 파일 처리 방식 (report/move/delete): ").strip()
organizer.find_and_handle_duplicates(action or 'report')
elif choice == '5':
organizer.find_and_handle_duplicates('move')
organizer.organize_by_extension()
else:
print("잘못된 선택입니다.")
if __name__ == "__main__":
main()
10. 마무리 및 다음 편 예고
이번 편에서는 Python을 활용한 파일 및 폴더 자동화의 핵심 기술들을 배웠습니다:
- os 모듈을 활용한 기본 파일 시스템 작업
- pathlib의 객체 지향적 경로 처리
- 파일 읽기/쓰기 (텍스트, CSV, JSON)
- 폴더 생성, 삭제, 이동
- glob을 활용한 파일 검색
- shutil을 활용한 파일 복사/이동
- 파일명 일괄 변경 기법
- 중복 파일 찾기 및 처리
- 종합 파일 정리 자동화 프로젝트
이 지식들은 앞으로의 다양한 자동화 프로젝트에서 기초가 될 것입니다. 파일 시스템을 자유자재로 다룰 수 있다면, 백업 자동화, 로그 분석, 데이터 전처리 등 무궁무진한 활용이 가능합니다.
다음 편 예고: Python 자동화 마스터 3편에서는 웹 스크래핑 자동화를 다룹니다. requests, BeautifulSoup, Selenium을 활용하여 웹에서 데이터를 자동으로 수집하는 방법을 배워봅니다!