[파이썬 100강] 54강. fnmatch와 glob 패턴으로 파일 선별 자동화하기
파일 자동화에서 진짜 시간을 잡아먹는 건 “복사”나 “압축”이 아니라, 사실 대상 파일을 정확히 고르는 일입니다. 예를 들어 report_2026-02-*.csv만 가져와야 하는데 report_old.csv까지 같이 잡히면, 뒤 단계(집계/백업/배포)가 전부 오염됩니다. 그래서 이번 강의는 서론을 짧게 하고 바로 핵심으로 들어갑니다.
초보가 가장 많이 실수하는 지점은 “문자열 포함 여부(in)로 필터링하면 되겠지”라고 생각하는 부분입니다. 이 방식은 당장 돌아가 보여도 예외 케이스(대소문자, 확장자 변형, 하위 경로 패턴)에서 금방 무너집니다. 이번 강의 목표는 패턴 기반 선별 규칙을 코드로 명시해, 재현 가능하고 유지보수 가능한 자동화를 만드는 것입니다.
핵심 개념
glob는 파일 시스템 경로를 대상으로 와일드카드 패턴(*,?,[]) 매칭을 수행해 경로 목록을 반환합니다.fnmatch는 이미 가진 문자열(파일명/경로 문자열)에 패턴 매칭을 적용할 때 유용하며, 필터 체인을 만들기 좋습니다.- 실무에서는 “포함 규칙(include)”과 “제외 규칙(exclude)”을 분리해 선언하고, 순서를 고정해야 결과가 안정됩니다.
패턴 매칭은 단순 편의 기능이 아니라 품질 제어 장치입니다. 예를 들어 운영 로그를 매일 수집할 때 *.log만 고르는 규칙과 debug_*를 제외하는 규칙을 명확히 분리해 두면, 신규 파일이 생겨도 기존 파이프라인이 크게 흔들리지 않습니다. 반대로 문자열 contains 방식은 규칙이 코드 곳곳에 흩어져 있어 수정할 때 누락이 생기기 쉽습니다. 또한 팀 협업에서 중요한 포인트는 “왜 이 파일이 포함/제외됐는지” 설명 가능한 구조를 만드는 것입니다. 패턴을 변수로 선언해두면 리뷰와 디버깅이 쉬워집니다.
기본 사용
예제 1) 가장 기본 패턴: glob으로 날짜형 리포트 선택
>>> from pathlib import Path
>>> base = Path("reports")
>>> files = sorted(base.glob("report_2026-02-*.csv"))
>>> [p.name for p in files][:3]
[]
해설:
glob("report_2026-02-*.csv")는 파일명 패턴을 경로에 직접 적용합니다.- 날짜 prefix가 고정된 파일 수집에서 사람이 읽기 쉬운 규칙을 만들 수 있습니다.
예제 2) 조건/조합 확장: fnmatch로 include/exclude 체인
>>> from fnmatch import fnmatch
>>> names = [
... "sales_2026-02-17.csv", "sales_2026-02-17.tmp",
... "sales_2026-02-18.csv", "debug_sales_2026-02-18.csv"
... ]
>>> include = ["sales_2026-02-*.csv"]
>>> exclude = ["debug_*", "*.tmp"]
>>> selected = []
>>> for n in names:
... if not any(fnmatch(n, pat) for pat in include):
... continue
... if any(fnmatch(n, pat) for pat in exclude):
... continue
... selected.append(n)
>>> selected
['sales_2026-02-17.csv', 'sales_2026-02-18.csv']
해설:
- 포함/제외를 분리하면 규칙 변경이 쉬워집니다.
- “무엇을 먼저 평가할지” 순서를 코드로 고정해 결과 일관성을 확보합니다.
예제 3) 실전형 미니 케이스: 하위 디렉터리 재귀 수집 + 다중 패턴
>>> from pathlib import Path
>>> from fnmatch import fnmatch
>>> root = Path("dataset")
>>> include = ["*.csv", "*.json"]
>>> exclude = ["*_backup.*", "temp_*", "*/archive/*"]
>>> picked = []
>>> for p in root.rglob("*"):
... if not p.is_file():
... continue
... posix = p.as_posix()
... name = p.name
... if not any(fnmatch(name, pat) for pat in include):
... continue
... if any(fnmatch(name, pat) for pat in exclude) or any(fnmatch(posix, pat) for pat in exclude):
... continue
... picked.append(posix)
>>> len(picked) >= 0
True
해설:
- 파일명 기준 패턴과 전체 경로 기준 패턴을 분리하면 훨씬 정교해집니다.
name만 보면 경로 기반 제외(*/archive/*)를 놓치기 쉬우므로as_posix()기준 검사도 함께 둡니다.
예제 4) 패턴 규칙을 함수로 고정해 재사용하기
>>> from pathlib import Path
>>> from fnmatch import fnmatch
>>> def select_files(root: str, include: list[str], exclude: list[str]) -> list[Path]:
... out = []
... for p in Path(root).rglob("*"):
... if not p.is_file():
... continue
... n = p.name
... full = p.as_posix()
... if include and not any(fnmatch(n, pat) or fnmatch(full, pat) for pat in include):
... continue
... if exclude and any(fnmatch(n, pat) or fnmatch(full, pat) for pat in exclude):
... continue
... out.append(p)
... return out
>>> isinstance(select_files("dataset", ["*.csv"], ["*_old.csv"]), list)
True
해설:
- 선별 로직을 함수로 고정하면 후속 작업(복사, 검증, 업로드)에서 재사용이 가능합니다.
- 팀 코드에서는 이 함수 하나를 공통 규약으로 삼는 것이 실수 예방에 매우 효과적입니다.
자주 하는 실수
실수 1) 문자열 포함 검사로 패턴 대체
>>> names = ["report_2026-02-01.csv", "report_2026-02-01.csv.bak"]
>>> [n for n in names if ".csv" in n]
['report_2026-02-01.csv', 'report_2026-02-01.csv.bak']
원인:
".csv" in n은 확장자 끝 조건을 보장하지 못합니다.- 백업 파일, 임시 파일이 섞여도 걸러내지 못해 데이터 오염이 발생합니다.
해결:
>>> from fnmatch import fnmatch
>>> [n for n in names if fnmatch(n, "*.csv")]
['report_2026-02-01.csv']
패턴 규칙으로 끝 확장자 조건을 명확히 표현하면 예외를 줄일 수 있습니다.
실수 2) include/exclude 우선순위를 매번 다르게 구현
>>> from fnmatch import fnmatch
>>> n = "debug_sales_2026-02-18.csv"
>>> include = ["*.csv", "debug_*.csv"]
>>> exclude = ["debug_*.csv"]
>>> # 팀원 A: include 우선
>>> a = any(fnmatch(n, p) for p in include)
>>> # 팀원 B: exclude 우선
>>> b = not any(fnmatch(n, p) for p in exclude)
>>> a, b
(True, False)
원인:
- 규칙의 철학(포함 우선 vs 제외 우선)을 문서화하지 않아, 코드마다 결과가 달라집니다.
- 자동화 파이프라인에서 가장 위험한 버그는 “조용히 틀리는 버그”인데, 이 경우가 대표적입니다.
해결:
>>> def allowed(name: str, include: list[str], exclude: list[str]) -> bool:
... if include and not any(fnmatch(name, p) for p in include):
... return False
... if exclude and any(fnmatch(name, p) for p in exclude):
... return False
... return True
>>> allowed("debug_sales_2026-02-18.csv", ["*.csv"], ["debug_*.csv"])
False
“include 통과 후 exclude 최종 차단”처럼 순서를 고정하면 디버깅이 쉬워집니다.
실수 3) 경로 구분자/대소문자 차이를 무시
- 증상: macOS에서는 되는데 서버(Linux/Windows 혼합)에서 일부 파일이 누락됩니다.
- 원인: 파일명은 소문자로 비교했지만 경로 패턴은 원본 문자열 그대로 비교해 케이스/구분자 차이가 생겼습니다.
- 해결: 비교 전
as_posix()로 경로 정규화, 필요 시lower()변환 후 패턴 비교 규칙을 통일합니다.
실무 패턴
- 입력 검증 규칙: 루트 경로가 실제 존재하는지, 파일 수집 대상이 예상 범위인지 dry-run에서 먼저 출력합니다.
- 로그/예외 처리 규칙: 최종적으로
총 파일 수 / 포함 수 / 제외 수 / 제외 사유 Top N을 리포트로 남겨 운영 검토가 가능해야 합니다. - 재사용 함수/구조화 팁:
load_rules() -> select_files() -> process_files()3단계로 분리하면 규칙 변경과 작업 변경을 독립적으로 관리할 수 있습니다. - 성능/메모리 체크 포인트: 수십만 파일 규모에서는 결과 전체 리스트를 한 번에 만들기보다 제너레이터로 처리하고, 필요 최소한의 메타데이터만 저장합니다.
여기서 특히 중요한 건 “설정 파일화”입니다. 패턴을 코드에 하드코딩하지 않고 YAML/JSON으로 분리하면 운영 중 규칙 업데이트가 훨씬 안전해집니다. 예를 들어 마케팅팀이 월말에만 특정 패턴을 추가해야 한다면 코드 배포 없이 규칙 파일만 바꿔 대응할 수 있습니다. 또한 dry-run 단계에서 “이번 실행에서 실제로 어떤 파일이 선택될지” 샘플 20개를 출력하면, 사람 검토 후 본 실행으로 넘어갈 수 있어 사고를 크게 줄일 수 있습니다. 즉, 패턴 자동화의 본질은 문법이 아니라 예측 가능성입니다.
오늘의 결론
한 줄 요약: 파일 자동화의 품질은 처리 로직보다 ‘선별 규칙의 명확성’에서 먼저 결정됩니다.
기억할 것:
- 문자열 contains 대신
glob/fnmatch패턴 규칙을 사용하세요. - include/exclude 순서를 고정하고 함수로 캡슐화하세요.
- 경로 정규화 + dry-run 리포트를 습관화하면 운영 사고를 크게 줄일 수 있습니다.
연습문제
logs/하위에서app_2026-02-*.log만 선택하되*_debug.log,archive/경로는 제외하는 선택 함수를 작성하세요.- include/exclude 규칙을 JSON 파일로 읽어와 선택 함수에 주입하고, dry-run에서 선택된 파일 10개를 출력하세요.
- 선택된 파일만 날짜별 폴더(
dist/YYYY-MM-DD)로 복사하되, 실행 전/후 파일 수를 비교해 검증 리포트를 출력하세요.
이전 강의 정답
workspace_data/순회에서.csv,.json만 선택 +tmp/.git/node_modules제외
>>> import os
>>> def pick_data_files(root: str):
... excluded = {"tmp", ".git", "node_modules"}
... out = []
... for cur, dirs, files in os.walk(root, topdown=True):
... dirs[:] = [d for d in dirs if d not in excluded]
... for f in files:
... if f.endswith(".csv") or f.endswith(".json"):
... out.append(os.path.join(cur, f))
... return out
>>> isinstance(pick_data_files("workspace_data"), list)
True
max_depth적용(path, depth)로그 확인
>>> import os
>>> def walk_with_depth(root: str, max_depth: int):
... base = root.rstrip(os.sep).count(os.sep)
... logs = []
... for cur, dirs, files in os.walk(root, topdown=True):
... depth = cur.count(os.sep) - base
... logs.append((cur, depth))
... if depth >= max_depth:
... dirs[:] = []
... return logs
>>> all(d <= 2 for _, d in walk_with_depth("workspace_data", 2))
True
- 확장자별 개수/용량 상위 3개 리포트
>>> import os
>>> from collections import defaultdict
>>> def top_ext_report(root: str):
... cnt = defaultdict(int)
... siz = defaultdict(int)
... for cur, _, files in os.walk(root):
... for f in files:
... p = os.path.join(cur, f)
... ext = os.path.splitext(f)[1].lower() or "(noext)"
... try:
... cnt[ext] += 1
... siz[ext] += os.path.getsize(p)
... except OSError:
... pass
... top_cnt = sorted(cnt.items(), key=lambda x: x[1], reverse=True)[:3]
... top_siz = sorted(siz.items(), key=lambda x: x[1], reverse=True)[:3]
... return top_cnt, top_siz
>>> isinstance(top_ext_report("workspace_data"), tuple)
True
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 사용 모듈:
pathlib,fnmatch,os - 재현 절차:
dataset,reports,logs폴더에 샘플 파일을 생성합니다.- 예제 1~4를 순서대로 실행해 include/exclude 결과를 비교합니다.
- 연습문제 2번처럼 규칙을 JSON으로 분리해 dry-run 출력과 실제 처리 결과가 일치하는지 확인합니다.
- 검증 포인트: 패턴 누락 여부, 제외 규칙 우선순위, 경로 정규화 일관성, 실행 전/후 파일 수 일치