고객 데이터 다루기 · 글자가 깨졌다 · 실습
이름 칸이 전부 네모다 — 판정 도구를 만든다
목표
고객이 보낸 파일의 인코딩을 추측하지 않고 판정하는 도구 enc_probe.py 를 만든다. BOM 과 UTF-8 디코딩 가능성에서 시작해 후보 인코딩과 표본 대조로 내려가고, 이중 인코딩을 되살리고, 되살릴 수 없는 파일을 가려내고, 정규화와 보이지 않는 문자가 비교를 어떻게 망가뜨렸는지를 자기 데이터로 증명한다.
왜 중요한가
텍스트 파일 안에는 인코딩이 적혀 있지 않다. 그래서 "무슨 인코딩이냐" 는 물음에는 언제나 절차로만 답할 수 있다. 에디터 메뉴를 눌러 보는 방식은 파일이 셋일 때만 통하고, 무엇보다 맞았는지를 눈으로만 판단하게 만든다.
판정이 끝나면 파일은 세 부류로 갈린다. 제대로 읽히는 것, 한 번 잘못 읽혀 굳었지만 되돌릴 수 있는 것, 그리고 글자가 뭉개져 원래 바이트를 알 수 없는 것이다. 마지막 부류를 붙들고 시간을 쓰는 것은 노력이 아니라 낭비다 — 그 파일은 다시 받아야 한다. 이 구별을 코드로 할 수 있어야 고객에게 무엇을 요청할지 말할 수 있다.
그리고 글자가 멀쩡히 보이는데 안 맞는 자리가 있다. 같은 한글이 완성형과 자모 분해형으로 나뉘어 저장되면 화면은 같고 바이트는 다르다. 눈에 안 보이는 NBSP 와 제로폭 공백도 마찬가지다. 조인 결과가 0건인데 원인이 화면에 안 보이는 상황은 여기서 나온다.
채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 파일을 차려 놓고 여러분의 도구를 실제로 실행해 판정을 대조한다. 이름과 행 수는 실행마다 바뀐다.
단계
1. /root/enc/gen_inbox.py 를 만들어 실행해 /root/enc/inbox 아래 일곱 파일을 만드세요. 같은 24명의 명부가 일곱 가지 상태로 저장됩니다.
2. /root/enc/enc_probe.py 의 detect 를 만들어 BOM·UTF-8 디코딩 가능성·후보 인코딩·표본 대조 순서로 인코딩을 판정하게 하세요.
3. decode 를 더해 판정한 인코딩으로 읽어 UTF-8 로 표준출력에 내게 하고, 수신함 전체를 /root/enc/norm 아래 같은 이름으로 통일해 저장하세요.
4. repair 를 더해 이중 인코딩을 되살리게 하고, detect 가 그런 파일에 mojibake 를 true 로, verdict 를 mojibake 로 답하게 하세요.
5. detect 에 lossy 를 더해 U+FFFD 가 남은 파일의 verdict 를 lost 로 하고, 수신함 판정을 /root/enc/lost.json 에 적으세요.
6. keys 를 더해 이름 칸을 NFC 로 정규화한 비교용 키로 내게 하고, NFD 판과 UTF-8 판의 이름이 정규화 전후로 몇 건 맞는지를 /root/enc/join_report.json 에 적으세요.
7. scan 을 더해 보이지 않는 문자를 코드포인트별로 세게 하고, keys 가 그것들을 걷어 내게 한 뒤 /root/enc/invisible_report.json 에 적으세요.
8. 수신함 일곱 파일을 한 번에 처리해 /root/enc/enc_report.json 과 /root/enc/enc_report.md 를 만드세요.
참고
- 실행 계약:
python3 /root/enc/enc_probe.py <명령> <파일>. 명령은 detect·decode·repair·keys·scan 다섯입니다. 성공하면 종료 코드 0, 되살릴 수 없거나 읽을 수 없으면 3, 사용법이 틀리면 2 입니다. detect응답:{"path": 문자열, "bom": true|false, "utf8_ok": true|false, "encoding": 문자열|null, "verdict": "ok"|"mojibake"|"lost"|"unknown"}. 4단계부터mojibake, 5단계부터lossy가 함께 나옵니다.decode와repair는 고친 본문을 표준출력에 그대로 냅니다. 파일로 저장하는 일은 호출하는 쪽이 합니다.keys는 두 번째 칸(이름)만 다듬어 JSON 배열로 냅니다.scan은{"U+00A0": 개수, ...}꼴로 냅니다.- 되살리는 한 줄:
깨진문자열.encode("latin-1").decode("utf-8"). latin-1 은 0부터 255까지 모든 바이트에 글자가 있어 되돌리는 길이 막히지 않습니다. - 표본 대조의 잣대로 완성형 한글 음절 범위
U+AC00부터U+D7A3까지를 쓰고, 비율 0.9 를 기준선으로 삼습니다. 이 숫자는 이 실습의 가정입니다 — 한국어 명부에 맞춘 값이고 자료가 달라지면 다시 정해야 합니다. - CP949 와 EUC-KR 을 구별하지 않고 CP949 로 통일해 읽습니다. CP949 가 EUC-KR 의 확장이라 이렇게 해도 손해가 없습니다.
- 공식 문서: [RFC 3629(UTF-8)](https://www.rfc-editor.org/rfc/rfc3629.html) · [Unicode UTF-8·BOM 안내](https://www.unicode.org/faq/utf_bom.html) · [UAX #15 정규화](https://www.unicode.org/reports/tr15/) · [python codecs](https://docs.python.org/3/library/codecs.html) · [python unicodedata](https://docs.python.org/3/library/unicodedata.html)
- 흔한 실수: BOM 을 떼지 않고 헤더를 비교하기, U+FFFD 가 남은 파일을 계속 고치려 들기, 정규화한 값으로 원본을 덮어쓰기, 제로폭 문자를
strip()으로 지워질 것이라 믿기. - 바이트를 직접 보고 싶으면
od -An -tx1 -N 16 <파일>과file <파일>을 쓰세요.
단계 8개
- 일곱 가지 상태의 같은 명부 만들기
- 추측하지 않고 판정하기
- 판정한 대로 읽어 UTF-8 로 통일하기
- 한 번 잘못 읽혀 굳은 글자 되살리기
- 되살릴 수 있는 것과 이미 잃은 것 가르기
- 같아 보이는데 안 맞는 이름
- 눈에 안 보이는 글자가 키에 섞였다
- 수신함 한 장으로 보고하기