앱에 넣을 번역 제목 442개를 sort | uniq -c 로 검사하다가, uniq 가 거짓말을 하고 있는 걸 봤습니다.
저는 파일이 망가진 줄 알았는데 파일은 멀쩡했습니다.
uniq 가 442줄 중 397줄을 중복이라고 했습니다
제목 목록을 uniq 로 세어봤습니다.
cut -f2 titles.tsv | sort | uniq -c | sort -rn | head -3
397 街で人を眺める
12 夜に1時間本を読む
8 朝5時に起きる
397개가 같은 제목이라니 번역이 통째로 망가진 줄 알았습니다. 그런데 그 문자열이 파일에 몇 번 있는지 직접 세어보니 이랬습니다.
grep -c '街で人を眺める' titles.tsv
1
한 번입니다.
파이썬으로 set() 에 넣어봐도 442개가 전부 서로 달랐습니다.
망가진 건 파일이 아니라 세는 쪽이었습니다.
범인은 sort 가 아니라 uniq 였습니다
처음에는 sort 를 의심했습니다.
정렬이 이상해서 엉뚱한 줄이 붙었나 싶었는데, sort 결과를 눈으로 보니 세 줄이 멀쩡히 세 줄로 나왔습니다.
printf '가나다\n라마바\n사아자\n' > k.txt
sort k.txt
가나다
라마바
사아자
그런데 여기에 uniq -c 를 붙이면 이렇게 됩니다.
sort k.txt | uniq -c
3 가나다
서로 다른 세 줄이 한 줄로 접혔습니다.
uniq 는 붙어 있는 줄끼리 같은지 비교하는데, 그 비교가 로케일을 탑니다.
제 셸은 이렇게 되어 있었습니다.
locale | grep LC_COLLATE
LC_COLLATE="en_US.UTF-8"
비교에서 한글이 통째로 빠집니다
어떤 규칙으로 같다고 보는지 궁금해서 몇 가지를 넣어봤습니다.
각 줄은 서로 다른 문자열이고, 숫자는 uniq 가 만든 그룹 수입니다.
| 넣은 줄 | 기본 로케일 | LC_ALL=C |
|---|---|---|
가나다 라마바 사아자 | 1 | 3 |
가나다1 라마바2 사아자3 | 3 | 3 |
a가나다 a라마바 a사아자 | 1 | 3 |
가a나 나a가 | 1 | 2 |
apple banana cherry | 3 | 3 |
세 번째 줄이 규칙을 보여줍니다.
a가나다 와 a라마바 는 앞의 a 만 같고 뒤가 전부 다른데 같은 것으로 접혔습니다.
즉 한글 부분이 비교에서 아예 안 읽힙니다.
네 번째 줄은 더 분명합니다. 가a나 와 나a가 는 남는 것이 a 하나뿐이라 같아집니다.
두 번째 줄이 왜 3개인지도 이걸로 설명됩니다.
1 2 3 은 읽히니까 그것만으로 갈린 겁니다.
제 파일에서 397줄이 한 덩어리가 된 것도 같은 이유였습니다.
그 397줄에는 숫자나 알파벳이 하나도 없었고, 나머지 45줄은 제목에 1 이나 5 같은 숫자가 들어 있어서 그 숫자로만 나뉜 것이었습니다.
한글만 그런 게 아닙니다
같은 방법으로 다른 문자도 재봤습니다.
| 넣은 두 줄 | 기본 로케일 | LC_ALL=C |
|---|---|---|
日本 中国 | 1 | 2 |
αβγ δεζ | 1 | 2 |
Привет Мир | 1 | 2 |
🍎 🍊 | 1 | 2 |
café cafe | 2 | 2 |
straße strasse | 2 | 2 |
한자, 그리스 문자, 키릴 문자, 이모지가 전부 무시됩니다.
반면 악센트가 붙은 라틴 문자는 제대로 구분됩니다.
café 와 cafe 는 다르게 나오고 straße 와 strasse 도 다르게 나옵니다.
그래서 영어권 코드에서는 이 문제가 안 보입니다.
sort -u 도 같은 비교를 쓰니 같이 틀립니다.
고치는 법은 한 줄입니다
cut -f2 titles.tsv | LC_ALL=C sort | LC_ALL=C uniq -d
sort 와 uniq 양쪽 다 붙여야 합니다.
LC_ALL=C 는 로케일 규칙 대신 바이트로 비교하라는 뜻이라, 문자열이 다르면 다르다고 나옵니다.
제 파일에 이걸 붙이니 중복 0건이 나왔고, 그게 맞는 답이었습니다.
정렬 순서가 필요한 경우라면 조심할 점이 있습니다.
LC_ALL=C 는 사전 순이 아니라 바이트 순이라 한글 정렬 결과가 사람이 기대하는 순서와 다릅니다.
중복을 세는 것과 사람에게 보여줄 순서로 정렬하는 것은 목적이 다르니, 중복 검사에만 쓰면 문제가 없습니다.
배운 것
에러가 안 났다는 게 이 문제의 전부입니다.
uniq 는 종료 코드 0을 주고, 숫자도 그럴듯하게 397 같은 값을 냅니다.
저는 그 숫자를 보고 번역 파일이 망가졌다고 판단했고, 잠깐이지만 멀쩡한 결과물을 버릴 뻔했습니다.
같은 저장소에서 통과했다고 말하지만 아무것도 안 보던 타입 검사를 겪은 적이 있습니다. 그때도 문제는 검사기가 틀린 게 아니라 검사기를 검사하지 않은 것이었습니다.
그래서 지금은 중복 검사를 셸에서 안 합니다. 검사기를 쓰는 코드 안에서 딕셔너리로 세게 바꿨습니다. 셸 파이프라인은 짧아서 좋지만, 짧다는 이유로 검산 없이 믿을 수는 없다는 걸 이번에 배웠습니다.
어디까지 확인했는지
macOS 26.6 (빌드 25G72), sort 2.3-Apple (199) 에서 확인했습니다.
LC_COLLATE=en_US.UTF-8 인 기본 셸 환경입니다.
리눅스에서도 같은지는 확인하지 못했습니다.
GNU coreutils 의 uniq 는 구현이 다르고 glibc 의 로케일 데이터도 다르므로 결과가 다를 수 있습니다.
다른 로케일(ko_KR.UTF-8 등)에서 어떻게 되는지도 재보지 않았습니다.
확인한 것은 위에 적은 조합 하나입니다.