Python 강좌 / 종합 실습: CSV 데이터를 검증하고 보고서 만들기
CSV 보고서 도구는 헤더·필수 필드·금액·중복 ID를 검증하고 유효한 행만 분류별로 합산합니다. 제외한 행의 이유와 실패 종료 코드를 함께 남겨 일부 데이터가 빠진 보고서를 완전한 결과로 오해하지 않게 합니다.
입력·출력과 오류 정책
id는 한 지출을 식별하고 category는 분류, amount는 비음수 정수의 십진 숫자 문자열입니다. 이 실습은 먼저 수용한 유효 ID의 중복을 거부하고 잘못된 행은 사유만 기록합니다. report.csv와 rejected.csv는 기존 파일이 있으면 중단하므로 새 실습 폴더에서 실행합니다.
관련 동작과 지원 조건은 종합 실습: CSV 데이터를 검증하고 보고서 만들기 관련 공식 문서에서 확인할 수 있습니다.
rejected의 record는 헤더를 1로 센 논리 레코드 번호입니다. 필드 내부 줄바꿈이 있으면 실제 텍스트 줄 번호와 다를 수 있습니다. 원본에는 쓰지 않습니다.
입력 파일 준비
expenses-input.csv 파일의 전체 내용입니다.
id,category,amount e1,food,3500 e2,food,8000 e3,transport,2500 e2,food,8000 e4,books,-1
중복 ID 하나와 음수 금액 하나를 의도적으로 넣었습니다. 제외 결과를 검증하는 테스트용 입력입니다.
전체 보고서 프로그램
csv_report.py 파일의 전체 내용입니다.
import csv
import re
from collections import defaultdict
from pathlib import Path
source = Path("expenses-input.csv")
report = Path("report.csv")
rejected = Path("rejected.csv")
if report.exists() or rejected.exists():
raise FileExistsError("Use a new output directory")
totals = defaultdict(int)
seen = set()
errors = []
accepted = 0
with source.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
if reader.fieldnames != ["id", "category", "amount"]:
raise ValueError("unexpected header")
for number, row in enumerate(reader, start=2):
key = (row.get("id") or "").strip()
category = (row.get("category") or "").strip()
amount_text = (row.get("amount") or "").strip()
reason = ""
if None in row or not key or not category:
reason = "invalid fields"
elif key in seen:
reason = "duplicate id"
elif not re.fullmatch(r"[0-9]+", amount_text):
reason = "invalid amount"
if reason:
errors.append({"record": number, "reason": reason})
continue
seen.add(key)
totals[category] += int(amount_text)
accepted += 1
with report.open("x", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["category", "total"])
writer.writerows(sorted(totals.items()))
with rejected.open("x", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["record", "reason"])
writer.writeheader()
writer.writerows(errors)
print("accepted:", accepted, "rejected:", len(errors))
print("total:", sum(totals.values()))
raise SystemExit(1 if errors else 0)
python csv_report.py
accepted: 3 rejected: 2와 total: 14000이 출력됩니다. 오류 행이 있으므로 종료 코드는 1입니다. report.csv에는 food 11500, transport 2500이 저장되고 rejected.csv에는 두 제외 사유가 남아야 합니다.
검증과 재실행 절차
- 보고서의 분류 합계와 원본 유효 행의 합이 같은지 비교합니다.
- 중복 행과 음수를 고친 새 입력을 별도 폴더에서 처리하여 종료 코드가 0인지 확인합니다.
- 헤더 순서를 바꿔 현재 엄격한 헤더 검사가 거부하는지 확인합니다.
두 출력 파일 생성은 하나의 원자적 트랜잭션이 아닙니다. 두 번째 파일 쓰기가 실패하면 첫 번째 파일이 남을 수 있으므로 실무에서는 임시 디렉터리·완료 표시·검증 후 배포 등의 절차를 추가합니다. 부분 결과를 바로 후속 업무에 넘기지 않습니다. CSV를 스프레드시트로 배포한다면 외부 문자열의 수식 해석 정책도 검토합니다.




