Python CSV 가져오기: BOM·쉼표·잘못된 행 처리
CSV를 단순 문자열 분할로 읽을 때 생기는 오류를 재현하고 행 단위 검증 결과를 분리합니다.
CSV 파일을 가져오는 작업에서 어려운 부분은 정상 행을 읽는 코드보다 잘못된 행을 설명하는 방식입니다. 이름에 쉼표가 들어가거나 첫 열 이름에 BOM이 붙으면 겉으로는 정상인 파일도 다르게 해석됩니다. 이 글은 Python 표준 라이브러리로 작은 주문 파일을 검증하는 예제입니다. 실제 고객 정보는 사용하지 않습니다.
먼저 정할 가져오기 규칙
예제의 열은 name,quantity 두 개입니다. 이름은 비어 있으면 안 되고 수량은 1 이상의 정수여야 합니다. 잘못된 행은 저장하지 않고 이유를 별도 목록으로 돌려줍니다. 여기서는 정상 행만 받는 방식을 보여 주지만, 정산 파일처럼 일부 누락이 전체 결과를 바꾸는 업무라면 오류가 하나라도 있을 때 파일 전체를 거절하는 편이 적합합니다.
line.split(",")는 따옴표 안의 쉼표도 나눕니다. "펜, 파랑",2는 두 열이어야 하므로 CSV 파서를 사용해야 합니다. 파일을 열 때는 encoding="utf-8-sig", newline=""를 지정합니다. 전자는 UTF-8 BOM을 처리하고 후자는 CSV 모듈이 줄바꿈을 처리하도록 합니다. 다른 문자 인코딩의 파일까지 자동 판별해 주는 설정은 아닙니다.
임시 파일로 성공과 실패를 함께 재현하기
아래 코드를 Python 3.11 이상에서 실행하면 임시 폴더 안에 예제 파일을 만들고 작업 후 정리합니다. 기존 파일을 바꾸지 않습니다.
import csv
import tempfile
from pathlib import Path
sample = '\ufeffname,quantity\n"펜, 파랑",2\n노트,0\n지우개,abc\n,3\n'
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'orders.csv'
path.write_text(sample, encoding='utf-8')
accepted, rejected = [], []
with path.open(encoding='utf-8-sig', newline='') as handle:
reader = csv.DictReader(handle)
if reader.fieldnames != ['name', 'quantity']:
raise ValueError('열 이름 또는 순서가 다릅니다')
for row in reader:
name = (row.get('name') or '').strip()
try:
if None in row or row.get('quantity') is None:
raise ValueError('열 개수가 다릅니다')
if not name:
raise ValueError('이름이 비었습니다')
try:
quantity = int(row['quantity'])
except ValueError:
raise ValueError('수량이 정수가 아닙니다') from None
if quantity < 1:
raise ValueError('수량은 1 이상이어야 합니다')
accepted.append((name, quantity))
except ValueError as error:
rejected.append((reader.line_num, str(error)))
assert accepted == [('펜, 파랑', 2)]
assert [line for line, _ in rejected] == [3, 4, 5]
print(accepted)
print(rejected)
정상 목록에는 ('펜, 파랑', 2) 하나가 남고 오류 목록에는 3·4·5행이 남습니다. reader.line_num은 읽은 물리적 줄 수입니다. 따옴표 안에 줄바꿈이 들어 있는 레코드는 여러 줄을 차지하므로 업무상의 주문 번호와 같은 값으로 취급하면 안 됩니다.
파일 전체 실패와 행 오류를 나누기
| 상황 | 처리 위치 | 사용자에게 줄 정보 |
|---|---|---|
| UTF-8로 해석 불가 | 파일 읽기 단계 | 허용 인코딩과 재저장 방법 |
| 헤더 이름 변경 | 반복문 진입 전 | 기대한 열 이름 |
| 수량이 0 또는 문자 | 행 검증 | 위치와 필드별 이유 |
| 중복 주문 ID | 업무 검증 | 기존 자료와 충돌 여부 |
예제는 헤더 오류와 디코딩 오류를 정상 행처럼 삼키지 않습니다. 운영 도구에서는 최상위 호출부에서 이런 오류를 받아 파일 전체 실패로 기록해야 합니다. csv가 읽을 수 있다는 사실은 업무 규칙을 만족한다는 뜻이 아닙니다. 특히 중복 ID, 허용 금액 범위, 파일 크기 제한은 따로 설계해야 합니다.
저장 전에 남길 결과
가져오기 결과에는 전체 레코드 수, 정상 수, 거절 수와 파일 식별자를 남깁니다. 개인정보가 있는 원본 행 전체를 로그에 복사하기보다 위치와 오류 유형을 기록합니다. 검증과 DB 저장을 분리하면 같은 입력을 다시 검사하기 쉽습니다. 단, 그 사이에 다른 작업이 같은 주문을 저장할 수 있으므로 DB의 유일성 제약도 필요합니다.
스프레드시트로 다시 내보낼 예정이면 CSV 문법과 별개로 수식으로 해석되는 값도 검토해야 합니다. 이 예제는 가져오기 검증만 다루며, 안전한 스프레드시트 내보내기까지 해결하지 않습니다.
참고 자료
- Python csv 공식 문서: 줄바꿈 처리와 DictReader 동작.
- Python codecs 공식 문서: UTF-8 BOM과 utf-8-sig.
- 함께 읽기: 로그에 남길 필드와 제외할 정보.