件数が合わない — 行ではなくレコードを数える
한국어 원문으로 표시합니다.
목표
고객 CSV 를 줄이 아니라 레코드로 세는 문지기 csvgate.py 를 만든다. 손으로 자른 파서와 표준 파서의 답이 얼마나 벌어지는지를 자기 자료로 재고, 구분자·줄 끝·머리글 유무를 판별하고, 못 쓰는 줄을 거절 파일로 떼어 낸다.
왜 중요한가
CSV 에서 줄과 레코드는 다른 것이다. 주문 메모에 줄바꿈이 있으면 레코드 하나가 파일에서 두 줄을 차지하고, 상호에 쉼표가 있으면 그 줄을 쉼표로 자를 때 칼럼이 늘어난다. 두 경우 모두 파서는 오류를 내지 않고 그럴듯한 숫자를 내놓는다. 그래서 이 실수는 고객이 숫자를 보고 이상하다고 말할 때까지 살아남는다. RFC 4180 은 인용 규칙과 줄 끝을 정하지만, 규격을 다 지킨 파일만 오지는 않는다. 구분자가 세미콜론이거나 탭이고, 줄 끝이 한 파일 안에서 섞여 있고, 머리글이 없는 파일이 온다. 머리글이 있는지는 파일 안에 적혀 있지 않아서 판별해야 한다. 그리고 못 쓰는 줄을 어떻게 다룰지가 남는다. 조용히 건너뛰면 그 건수만큼 매출이 사라지고 설명할 근거가 없다. 은행 수신 파일이라면 한 줄만 어긋나도 파일을 통째로 돌려보내지만, 고객이 준 분석용 자료는 돌려보낼 곳이 없다 — 쓸 수 있는 줄은 쓰고 못 쓰는 줄만 떼어 낸다. 채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 피드를 차려 놓고 여러분의 문지기를 실제로 실행해 레코드 수·금액 합계·거절 사유를 채점기가 직접 센 값과 대조한다. 상호와 금액은 실행마다 바뀐다.
단계
- /root/csv/gen_feed.py 를 만들어 실행해 /root/csv/feed 아래 일곱 파일을 만드세요. 같은 주문 20건이 일곱 가지 모양으로 나옵니다.
- /root/csv/csvgate.py 에
naive를 만들어 줄 하나를 레코드 하나로 믿는 파서의 답(rows·amount_total·bad_field_count)을 내게 하세요. parse를 더해 csv 모듈로 제대로 센 답(records·amount_total)을 내게 하고, 두 답의 차이를 /root/csv/gap.json 에 적으세요.sniff를 더해 줄 끝을 CRLF·LF·MIXED 로 판별하게 하세요.sniff가 구분자를 후보(쉼표·세미콜론·탭·파이프) 중에서 판별하게 하세요.sniff가 머리글 유무(header)와 칼럼 수(fields)를 판별하게 하세요.check를 더해 못 쓰는 줄을 사유별로 세고 거절 파일로 떼어 내게 하세요. 거절 파일은 입력 파일이 있는 디렉터리 아래rejects/에 같은 이름으로 씁니다.- 피드 일곱 파일을 한 번에 처리해 /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 · python csv · python json
- 흔한 실수:
wc -l로 건수 보고하기,newline=""빠뜨리기, 못 쓰는 줄을 조용히 건너뛰기, 닫히지 않은 따옴표를 오류로 착각하기(파서는 조용합니다). - 줄 끝을 눈으로 보려면
od -c <파일> | head를 쓰세요.
일곱 가지 모양의 같은 피드 만들기
/root/csv/gen_feed.py 를 만들어 실행해 /root/csv/feed 아래 일곱 파일을 만드세요. 같은 주문 20건이 구분자·줄 끝·머리글·손상 여부만 달리해 나옵니다.
파이썬 csv 모듈의 writer 로 쓰면 인용은 알아서 됩니다. lineterminator 를 바꿔 줄 끝을 고르고, delimiter 를 바꿔 구분자를 고릅니다. 일부러 망가뜨리는 파일(칼럼 수가 다른 줄, 닫히지 않은 따옴표)은 writer 를 쓰지 말고 문자열로 직접 씁니다.
손으로 자른 파서의 답 내기
/root/csv/csvgate.py 에 naive <파일> 을 만들어 줄 하나를 레코드 하나로 믿는 파서의 답을 JSON 으로 내게 하세요. rows·amount_total·bad_field_count 세 값입니다.
이 단계는 일부러 틀린 파서를 만드는 것입니다. 파일을 통째로 읽어 개행으로 자르고, 첫 줄과 빈 줄을 건너뛰고, 각 줄을 쉼표로 자릅니다. 칼럼이 5개가 아니면 금액을 더하지 말고 bad_field_count 로만 세세요. 뒤에서 제대로 센 답과 나란히 놓아야 하니 규칙을 정확히 지킵니다.
인용 안의 쉼표와 줄바꿈 제대로 읽기
parse <파일> 을 더해 csv 모듈로 센 답(records·amount_total)을 내게 하고, orders_rfc.csv 에 대한 두 답의 차이를 /root/csv/gap.json 에 file·naive_rows·csv_records·naive_amount·csv_amount 로 적으세요.
파일을 열 때 newline="" 을 주는 것이 핵심입니다. csv.reader 는 인용 안의 구분자와 줄바꿈, 두 개로 쓴 따옴표를 전부 처리합니다. 머리글 줄을 뺀 나머지가 레코드이고, 금액은 칼럼 수가 맞는 레코드에서만 더합니다.
줄 끝이 섞인 파일 가려내기
sniff <파일> 을 더해 줄 끝을 판별하게 하세요. CRLF 만이면 CRLF, LF 만이면 LF, 둘이 섞여 있으면 MIXED 입니다. 응답에는 path·delimiter·newline 이 있어야 합니다.
줄 끝은 바이트로 셉니다. 파일을 바이너리로 읽어 CRLF 의 개수를 세고, 전체 LF 개수에서 그만큼을 빼면 홀로 선 LF 의 개수가 나옵니다. 둘 다 0 이 아니면 섞인 것입니다. 이 단계에서 구분자는 쉼표로 두어도 됩니다.
구분자를 후보 중에서 고르기
sniff 가 구분자를 쉼표·세미콜론·탭·파이프 네 후보 중에서 판별하게 하세요. 후보마다 파일 전체를 파싱해 보고 칼럼 수가 가장 고르게 넓은 것을 고릅니다.
첫 줄만 보고 세면 인용 안의 구분자에 속습니다. 후보마다 csv.reader 로 통째로 파싱해 칼럼 수의 최빈값과 그 최빈값을 갖는 줄의 비율을 구하고, 칼럼이 하나뿐인 후보는 점수를 0 으로 두세요. 칼럼 수가 줄마다 다른 파일에서도 이 방법은 버팁니다.
머리글이 있는지 없는지 판별하기
sniff 응답에 fields(칼럼 수)와 header(머리글 유무)를 더하세요. 첫 줄에 숫자로 읽히는 칸이 하나도 없고 뒤이은 세 줄에는 있으면 머리글로 봅니다.
머리글이 있는지는 파일 안에 적혀 있지 않습니다. RFC 4180 도 그것을 미디어 타입의 파라미터로 알리라고 할 뿐입니다. 그래서 판별이 아니라 추정이고, 추정에는 잣대가 필요합니다. 잣대를 코드에 적어 두면 다음 전달분에서 틀렸을 때 무엇을 고쳐야 하는지 보입니다.
못 쓰는 줄만 떼어 내기
check <파일> 을 더해 레코드마다 검사하고, 빈 줄·칼럼 수 불일치·닫히지 않은 따옴표를 사유별로 세어 거절 파일로 떼어 내게 하세요. 거절 파일은 입력 파일이 있는 디렉터리의 rejects/ 에 같은 이름으로 씁니다. sniff 응답에는 unterminated 를 더합니다.
닫히지 않은 따옴표는 오류를 내지 않습니다. 그 지점부터 파일 끝까지가 한 필드가 되어 '마지막 한 건' 처럼 보일 뿐입니다. 파일을 한 번 훑어 인용 상태로 끝났는지 보면 확실히 알 수 있습니다 — 두 개로 쓴 따옴표는 건너뛰어야 합니다. 거절한 줄에는 원래 몇 번째 레코드였는지를 함께 적으세요.
피드 한 장으로 보고하기
피드 일곱 파일을 한 번에 처리해 /root/csv/feed_report.json 에 files·accepted·rejected·amount_total 을 적고, /root/csv/feed_report.md 에 ## 무엇을 받았나 ## 줄과 레코드는 다르다 ## 떼어 낸 줄 ## 보내는 쪽에 요청할 것 네 절로 쓰세요.
files 는 파일 이름을 키로 두고 delimiter·newline·header·records·accepted·rejected·amount_total 을 담은 객체입니다. 보고서에는 떼어 낸 줄의 수를 숫자로 적으세요 — 고객이 자기 파일에서 그 줄을 열어 볼 수 있어야 대화가 빨라집니다. 앞 단계에서 만든 함수를 그대로 부르면 됩니다.