LabHub
배우기 러닝패스 코스

Working With Customer Data

The Row Count Is Off: Count Records, Not Lines

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

고객 CSV 를 줄이 아니라 레코드로 세는 문지기 csvgate.py 를 만든다. 손으로 자른 파서와 표준 파서의 답이 얼마나 벌어지는지를 자기 자료로 재고, 구분자·줄 끝·머리글 유무를 판별하고, 못 쓰는 줄을 거절 파일로 떼어 낸다.

왜 중요한가

CSV 에서 줄과 레코드는 다른 것이다. 주문 메모에 줄바꿈이 있으면 레코드 하나가 파일에서 두 줄을 차지하고, 상호에 쉼표가 있으면 그 줄을 쉼표로 자를 때 칼럼이 늘어난다. 두 경우 모두 파서는 오류를 내지 않고 그럴듯한 숫자를 내놓는다. 그래서 이 실수는 고객이 숫자를 보고 이상하다고 말할 때까지 살아남는다. RFC 4180 은 인용 규칙과 줄 끝을 정하지만, 규격을 다 지킨 파일만 오지는 않는다. 구분자가 세미콜론이거나 탭이고, 줄 끝이 한 파일 안에서 섞여 있고, 머리글이 없는 파일이 온다. 머리글이 있는지는 파일 안에 적혀 있지 않아서 판별해야 한다. 그리고 못 쓰는 줄을 어떻게 다룰지가 남는다. 조용히 건너뛰면 그 건수만큼 매출이 사라지고 설명할 근거가 없다. 은행 수신 파일이라면 한 줄만 어긋나도 파일을 통째로 돌려보내지만, 고객이 준 분석용 자료는 돌려보낼 곳이 없다 — 쓸 수 있는 줄은 쓰고 못 쓰는 줄만 떼어 낸다. 채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 피드를 차려 놓고 여러분의 문지기를 실제로 실행해 레코드 수·금액 합계·거절 사유를 채점기가 직접 센 값과 대조한다. 상호와 금액은 실행마다 바뀐다.

단계

  1. /root/csv/gen_feed.py 를 만들어 실행해 /root/csv/feed 아래 일곱 파일을 만드세요. 같은 주문 20건이 일곱 가지 모양으로 나옵니다.
  2. /root/csv/csvgate.pynaive 를 만들어 줄 하나를 레코드 하나로 믿는 파서의 답(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 를 만드세요.

참고

일곱 가지 모양의 같은 피드 만들기

/root/csv/gen_feed.py 를 만들어 실행해 /root/csv/feed 아래 일곱 파일을 만드세요. 같은 주문 20건이 구분자·줄 끝·머리글·손상 여부만 달리해 나옵니다.

파이썬 csv 모듈의 writer 로 쓰면 인용은 알아서 됩니다. lineterminator 를 바꿔 줄 끝을 고르고, delimiter 를 바꿔 구분자를 고릅니다. 일부러 망가뜨리는 파일(칼럼 수가 다른 줄, 닫히지 않은 따옴표)은 writer 를 쓰지 말고 문자열로 직접 씁니다.

손으로 자른 파서의 답 내기

/root/csv/csvgate.pynaive <파일> 을 만들어 줄 하나를 레코드 하나로 믿는 파서의 답을 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 을 담은 객체입니다. 보고서에는 떼어 낸 줄의 수를 숫자로 적으세요 — 고객이 자기 파일에서 그 줄을 열어 볼 수 있어야 대화가 빨라집니다. 앞 단계에서 만든 함수를 그대로 부르면 됩니다.