고객 데이터 다루기 · 규격을 안 지킨 CSV · 실습
행 수가 안 맞는다 — 줄이 아니라 레코드를 센다
목표
고객 CSV 를 줄이 아니라 레코드로 세는 문지기 csvgate.py 를 만든다. 손으로 자른 파서와 표준 파서의 답이 얼마나 벌어지는지를 자기 자료로 재고, 구분자·줄 끝·머리글 유무를 판별하고, 못 쓰는 줄을 거절 파일로 떼어 낸다.
왜 중요한가
CSV 에서 줄과 레코드는 다른 것이다. 주문 메모에 줄바꿈이 있으면 레코드 하나가 파일에서 두 줄을 차지하고, 상호에 쉼표가 있으면 그 줄을 쉼표로 자를 때 칼럼이 늘어난다. 두 경우 모두 파서는 오류를 내지 않고 그럴듯한 숫자를 내놓는다. 그래서 이 실수는 고객이 숫자를 보고 이상하다고 말할 때까지 살아남는다.
RFC 4180 은 인용 규칙과 줄 끝을 정하지만, 규격을 다 지킨 파일만 오지는 않는다. 구분자가 세미콜론이거나 탭이고, 줄 끝이 한 파일 안에서 섞여 있고, 머리글이 없는 파일이 온다. 머리글이 있는지는 파일 안에 적혀 있지 않아서 판별해야 한다.
그리고 못 쓰는 줄을 어떻게 다룰지가 남는다. 조용히 건너뛰면 그 건수만큼 매출이 사라지고 설명할 근거가 없다. 은행 수신 파일이라면 한 줄만 어긋나도 파일을 통째로 돌려보내지만, 고객이 준 분석용 자료는 돌려보낼 곳이 없다 — 쓸 수 있는 줄은 쓰고 못 쓰는 줄만 떼어 낸다.
채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 피드를 차려 놓고 여러분의 문지기를 실제로 실행해 레코드 수·금액 합계·거절 사유를 채점기가 직접 센 값과 대조한다. 상호와 금액은 실행마다 바뀐다.
단계
1. /root/csv/gen_feed.py 를 만들어 실행해 /root/csv/feed 아래 일곱 파일을 만드세요. 같은 주문 20건이 일곱 가지 모양으로 나옵니다.
2. /root/csv/csvgate.py 에 naive 를 만들어 줄 하나를 레코드 하나로 믿는 파서의 답(rows·amount_total·bad_field_count)을 내게 하세요.
3. parse 를 더해 csv 모듈로 제대로 센 답(records·amount_total)을 내게 하고, 두 답의 차이를 /root/csv/gap.json 에 적으세요.
4. sniff 를 더해 줄 끝을 CRLF·LF·MIXED 로 판별하게 하세요.
5. sniff 가 구분자를 후보(쉼표·세미콜론·탭·파이프) 중에서 판별하게 하세요.
6. sniff 가 머리글 유무(header)와 칼럼 수(fields)를 판별하게 하세요.
7. check 를 더해 못 쓰는 줄을 사유별로 세고 거절 파일로 떼어 내게 하세요. 거절 파일은 입력 파일이 있는 디렉터리 아래 rejects/ 에 같은 이름으로 씁니다.
8. 피드 일곱 파일을 한 번에 처리해 /root/csv/feed_report.json 과 /root/csv/feed_report.md 를 만드세요.
참고
- 실행 계약:
python3 /root/csv/csvgate.py <명령> <파일>. 명령은 naive·parse·sniff·check 넷입니다. 성공하면 종료 코드 0, 파일이 없으면 3, 사용법이 틀리면 2 입니다. 답은 JSON 한 덩어리로 표준출력에 냅니다. naive응답:{"rows": 정수, "amount_total": 정수, "bad_field_count": 정수}. 첫 줄은 머리글로 보고 건너뛰고, 빈 줄도 세지 않으며, 쉼표로 자른 칼럼이 5개가 아니면 bad_field_count 로 셉니다.parse응답:{"records": 정수, "amount_total": 정수}.sniff응답:{"path": 문자열, "delimiter": 문자열, "newline": "CRLF"|"LF"|"MIXED"|"NONE"}에 6단계부터fields·header가, 7단계부터unterminated가 더해집니다.check응답:{"file": 이름, "delimiter": 문자열, "records": 정수, "accepted": 정수, "rejected": 정수, "amount_total": 정수, "reasons": {"field_count": 정수, "empty": 정수, "unterminated": 정수}}.- 거절 파일은 머리글
reason,record_no,fields로 시작하고, record_no 는 머리글을 뺀 몇 번째 레코드인지(1부터)입니다. - 파이썬 csv 모듈로 파일을 열 때는
open(path, encoding="utf-8", newline="")입니다.newline=""을 빠뜨리면 인용 안의 줄바꿈이 레코드를 끊습니다. - 머리글 판별은 이 실습의 가정을 씁니다 — 첫 줄에는 숫자로 읽히는 칸이 하나도 없고 뒤이은 세 줄에는 있으면 머리글로 봅니다. 자료가 달라지면 이 잣대도 다시 정해야 합니다.
- 구분자 판별도 가정입니다. 후보마다 파일 전체를 파싱해 보고 칼럼 수가 가장 고르게 넓은 후보를 고릅니다.
- 공식 문서: [RFC 4180](https://www.rfc-editor.org/rfc/rfc4180.html) · [python csv](https://docs.python.org/3/library/csv.html) · [python json](https://docs.python.org/3/library/json.html)
- 흔한 실수:
wc -l로 건수 보고하기,newline=""빠뜨리기, 못 쓰는 줄을 조용히 건너뛰기, 닫히지 않은 따옴표를 오류로 착각하기(파서는 조용합니다). - 줄 끝을 눈으로 보려면
od -c <파일> | head를 쓰세요.
단계 8개
- 일곱 가지 모양의 같은 피드 만들기
- 손으로 자른 파서의 답 내기
- 인용 안의 쉼표와 줄바꿈 제대로 읽기
- 줄 끝이 섞인 파일 가려내기
- 구분자를 후보 중에서 고르기
- 머리글이 있는지 없는지 판별하기
- 못 쓰는 줄만 떼어 내기
- 피드 한 장으로 보고하기