인코딩 자동 판별은 추측일 뿐: 정확도와 변환 속도 실측

mb_detect_encoding 과 표준 라이브러리 판별기를 9,000개 샘플로 비교. 짧은 한글일수록 틀린다. 10MB 변환 속도와 청크 경계에서 한글이 깨지는 버그까지.

시리즈: 문자 인코딩 강좌 6/6 (고급). 직접 관리하지 않는 시스템(공공데이터 포털의 CSV, 거래처의 엑셀 내보내기, 오래된 게시판 백업)에서 파일을 가져오면서 "인코딩은 자동으로 판별하면 되잖아" 라는 말을 들은 개발자를 위한 글입니다. 다 읽으면 자동 판별이 얼마나 맞는지, 왜 짧은 문자열에서 틀리는지, 변환이 실제로 얼마나 빠른지, 운에 기대지 않는 가져오기 흐름을 어떻게 짜는지 알 수 있습니다.

mb_detect_encoding($data) 결과를 그대로 믿어 본 적이 있거나, 공공데이터 CSV 를 파이썬으로 읽었더니 한글이 깨져서 나온 적이 있다면 이 글이 맞습니다. 근본 문제는 단순합니다. 바이트열은 자신이 어떤 인코딩으로 만들어졌는지 말해 주지 않습니다. 자동 판별은 샘플에 대한 통계이고, 4글자 샘플은 증거로 너무 적습니다. 이 글에서는 한국어·일본어·스페인어·영어의 고정 시드 샘플로 이를 재고, PHP 판별기와 40줄짜리 Python 판별기를 비교하고, 10MB 변환 시간을 재고, 1강에서 예고한 청크 경계 디코딩 버그를 고칩니다.

실행 환경: Apple Silicon Mac(M1 Pro), macOS 26.6, Python 3.9.6(표준 라이브러리만), PHP 8.5.6(mbstring·iconv), macOS iconv 명령. 파이썬에서 흔히 쓰는 chardet 같은 라이브러리는 이 맥에 설치되어 있지 않고, 이번에도 아무것도 설치하지 않았으므로 시험하지 않았습니다. 윈도우와 엑셀도 실행하지 않았습니다.

왜 자동 판별은 추측일 수밖에 없나

짧은 문자열 네 개를 각자의 대표적인 옛 인코딩으로 저장하고, 같은 바이트열을 후보마다 읽어 봤습니다.

원래 문자열 저장 형식 바이트열 cp1252 로 읽기 cp932 로 읽기 cp949 로 읽기
It’s cp1252 49 74 92 73 It’s It痴 It뭩
año cp1252 61 f1 6f año a + U+E0EB(사용자 정의 영역) 오류
文字化け cp932 95 b6 8e 9a 89 bb 82 af •¶Žš‰»‚¯ 文字化け 빒럻돸궚
한글 cp949 c7 d1 b1 db ÇÑ±Û ヌムアロ 한글

대부분의 읽기가 오류 없이 디코딩됩니다. Windows-1252 는 거의 모든 바이트를 어떤 문자로든 바꾸므로 "디코딩됐다" 는 아무 증거도 아닙니다. CP932 와 CP949 는 둘 다 겹치는 범위의 2바이트 열을 쓰기 때문에, 일본어 바이트가 그럴듯한 한글(빒럻돸궚)이 되고 그 반대도 일어납니다. 판별기가 물을 수 있는 것은 "어느 읽기가 진짜 문장처럼 보이는가" 뿐이고, 이는 추측이며 텍스트가 길수록 잘 맞습니다.

반대 방향도 마찬가지입니다. UTF-8 바이트를 CP949 로 읽으면 한글 깨짐 이 �븳湲� 源⑥쭚 이 됐고(errors='replace'), CP949 바이트를 Windows-1252 로 읽으면 한글 이 ÇÑ±Û 가 됐습니다. 둘 다 한국 개발자에게 익숙한 "한글 깨짐" 입니다(원리는 2강).

다만 아주 강한 단서가 하나 있습니다. UTF-8 로서 올바른가 입니다. UTF-8 은 어떤 바이트 뒤에 어떤 바이트가 올 수 있는지 엄격하게 정해져 있어서, 옛 인코딩의 문장은 거의 그 규칙을 지키지 못합니다. 뒤에서 쓸 한국어·일본어·스페인어·영어 레거시 샘플 10,000개 중 UTF-8 로서 올바른 것은 0개 였습니다. UTF-8 을 먼저 확인하기만 해도 현실의 파일 대부분이 정리됩니다. 예외는 "이미 깨진 문자열" 로, cp1252 의 é 바이트는 UTF-8 의 é 와 똑같습니다.

mb_detect_encoding 의 함정 세 가지

정확도 숫자를 보기 전에 PHP 판별기의 동작을 짚어 둡니다.

함정 1: 기본 후보 목록. 후보를 주지 않으면 mb_detect_order() 가 쓰이고, 이 환경에서는 ASCII, UTF-8 이었습니다. 게다가 non-strict 모드에서는 어느 것에도 맞지 않아도 "가장 가까운" 후보를 돌려줍니다.

$b = mb_convert_encoding("文字化け", "SJIS-win", "UTF-8");
var_dump(mb_detect_encoding($b));                         // string(5) "ASCII"
var_dump(mb_detect_encoding("\xff\xfe", ["UTF-8", "ASCII"]));       // string(5) "UTF-8"
var_dump(mb_detect_encoding("\xff\xfe", ["UTF-8", "ASCII"], true)); // bool(false)

Shift_JIS 바이트열이 ASCII, UTF-8 에 절대 나올 수 없는 2바이트가 UTF-8 로 판별됐습니다. 후보 목록을 반드시 명시하고 세 번째 인자로 true(strict)를 주세요.

함정 2: 후보 순서도 답의 일부. 두 후보가 모두 들어맞으면 순서와 PHP 내부 채점이 결정합니다. 아래 결과는 "일본어 먼저" 와 "한국어·서유럽 먼저" 에서 극적으로 달라집니다.

함정 3: 가족까지만 알고, 변형은 모른다. 판별은 "EUC-KR 계열" 까지는 말해도 어느 표인지는 모릅니다. 똠(8c 63)·뷁(94 ee)은 CP949 확장 영역에만 있습니다. 후보를 ["UTF-8", "EUC-KR"] 로 주면 mb_detect_encoding 은 false, ["UTF-8", "EUC-KR", "UHC"] 로 주면 "UHC" 였습니다. 무리하게 mb_convert_encoding($b, "UTF-8", "EUC-KR") 를 하면 ?c?? 가, "UHC" 로 하면 똠뷁 이 나왔습니다. 일본어의 Shift_JIS 와 CP932(물결표 〜 문제), 서유럽의 ISO-8859-1 과 Windows-1252 에서도 같은 일이 일어납니다.

표준 라이브러리만 쓴 판별기: 버전 1 과 2

비교용으로 Python 표준 라이브러리만으로 판별기를 짰습니다. 버전 1 은 먼저 UTF-8 을 시도하고, 안 되면 각 후보로 디코딩해서 문자 종류로 점수를 매깁니다(가나·한글 +2, 한자 +1, 출력 가능한 ASCII +1, 반각 가타카나 −1, 제어 문자·사용자 정의 영역 −5, 글자당 평균). 이 버전은 일본어 CP932 에 약해서 4글자 샘플 200개 중 60개만 맞혔습니다. CP932 바이트를 CP949 로 읽으면 진짜 한글 음절이 나오기 때문입니다.

버전 2 는 실패 사례를 보고 두 가지를 바꿨습니다. 바이트당 점수를 매기고(2바이트 한자 한 글자가 1바이트 라틴 문자 두 개를 이기지 않도록), KS X 1001 상용 2,350음절 밖의 한글(CP949 확장 영역)에는 감점을 줍니다. 실제 한국어 문장에서는 드물지만, 잘못 읽힌 일본어 바이트는 자주 그곳에 떨어지기 때문입니다.

import unicodedata
CANDIDATES = ["cp1252", "cp932", "euc_jp", "cp949"]   # on a tie, earlier wins

def char_score(ch):
    o = ord(ch)
    if o < 0x80: return 1.0 if (ch.isprintable() or ch in "\t\r\n") else -5.0
    if 0x3041 <= o <= 0x30FF: return 2.0                       # kana
    if 0xAC00 <= o <= 0xD7A3:                                   # hangul syllable
        b = ch.encode("cp949")
        return 2.0 if (b[0] >= 0xB0 and b[1] >= 0xA1) else -1.0   # rare outside KS X 1001
    if 0x4E00 <= o <= 0x9FFF: return 1.5                        # kanji / hanja
    if 0x3000 <= o <= 0x303F or 0xFF01 <= o <= 0xFF5E: return 1.5
    if 0x2010 <= o <= 0x2026: return 1.0                        # dashes, smart quotes
    if 0xFF61 <= o <= 0xFF9F or 0x3131 <= o <= 0x318E: return -1.0
    cat = unicodedata.category(ch)
    if cat in ("Cc", "Co", "Cn"): return -5.0
    if cat.startswith("L"): return 1.0 if 0xC0 <= o <= 0xFF else 0.0
    return 0.0

def guess(b):
    if b.startswith(b"\xef\xbb\xbf"): return "utf-8-sig"
    try:
        b.decode("utf-8"); return "utf-8"
    except UnicodeDecodeError:
        pass
    best, best_score = None, float("-inf")
    for enc in CANDIDATES:
        try: s = b.decode(enc)
        except UnicodeDecodeError: continue
        score = sum(char_score(c) for c in s) / len(b)
        if score > best_score: best, best_score = enc, score
    return best

솔직하게 밝혀 두면, 버전 2 는 한 문장 세트의 실패를 보면서 조정했습니다. 자기 숙제를 자기가 채점하지 않도록, 아래 숫자는 모두 조정에 쓰지 않은 별도 문장 세트(스마트폰, 요리, 날씨 같은 일상 문단)로 잰 것입니다. 두 세트 모두 직접 쓴 문장이므로 실제 데이터에서는 결과가 달라집니다.

정확도 재기: 짧은 문자열과 긴 문자열

방법: 언어마다 4·8·16·64·256글자의 무작위 부분 문자열을 뽑고(random.seed(2026)), ASCII 만 있는 것(어떤 추측이든 정답)과 그 인코딩으로 표현할 수 없는 것(㈱, ① 은 EUC-JP 에 없음)을 빼고 인코딩한 뒤 각 판별기에 물었습니다. 그 추측으로 디코딩해서 원래 문자열과 완전히 같으면 정답입니다. 칸마다 N = 200, 모두 9,000개 샘플. PHP 목록 A 는 UTF-8, SJIS-win, EUC-JP, UHC, Windows-1252, 목록 B 는 UTF-8, UHC, Windows-1252, SJIS-win, EUC-JP 입니다(UHC 는 PHP 에서 CP949 의 이름).

200개 중 정답 수, 4글자 샘플:

경우 표준 라이브러리 v1 표준 라이브러리 v2 PHP 목록 A PHP 목록 B
한국어 CP949 200 199 47 200
일본어 CP932 60 185 200 200
일본어 EUC-JP 195 195 200 60
스페인어 Windows-1252 107 200 153 170
영어 Windows-1252 135 200 135 135

16글자 샘플:

경우 표준 라이브러리 v1 표준 라이브러리 v2 PHP 목록 A PHP 목록 B
한국어 CP949 200 200 132 200
일본어 CP932 80 200 200 200
일본어 EUC-JP 200 200 200 152
스페인어 Windows-1252 117 200 166 200
영어 Windows-1252 125 200 125 200

256글자에서는 v1 과 PHP 목록 A 의 영어 CP1252(둘 다 179개)를 빼면 모든 판별기가 200/200 이었습니다. UTF-8 샘플 4,000개는 모든 판별기에서 전부 정답이었습니다.

막대그래프. 한국어 CP949 샘플은 4글자에서 23.5%, 8글자에서 40.5%, 16글자에서 66%, 64글자와 256글자에서 100% 맞게 판별됐다.
일본어 인코딩을 앞에 둔 mb_detect_encoding 이 한국어 CP949 를 맞힌 비율(길이별, 각 N = 200, seed 2026).

한국어 쪽에서 눈여겨볼 것은 목록 A 입니다. "일본어 데이터도 가끔 오니까" 하고 일본어 인코딩을 앞에 두면, 4글자 한국어 CP949 는 4개 중 3개 이상이 EUC-JP 한자로 판별됐습니다. EUC-JP 와 EUC-KR 계열은 같은 바이트 범위(0xA1–0xFE)를 쓰기 때문입니다. 반대로 UHC 를 앞에 둔 목록 B 에서는 일본어 EUC-JP 가 UHC 한글로 바뀌었습니다. 짧은 문자열 + 여러 지역을 아우르는 후보 목록이 최악의 조합이고, 사람 이름·상품 코드·CSV 한 칸이 정확히 그런 입력입니다. 우리 v2 는 반대 방향으로 틀렸습니다. 4글자 일본어 CP932 에서 틀린 15개는 모두 cp1252 로 읽혔는데(天気予報 → “V‹C—\•ñ), 히라가나의 첫 바이트 0x82 가 cp1252 에서는 아래쪽 따옴표 ‚ 이고 v2 가 영어를 위해 스마트 따옴표에 가산점을 주기 때문입니다.

흔한 실수: 큰 파일의 앞부분만으로 판별하기

실수. 10MB 전체 판별은 느리니까(v2 는 10MiB CP932 에 894ms) 앞쪽 64KiB 만 본다: guess(data[:65536]).

실제로 일어나는 일. 자르는 지점은 세 번에 두 번꼴로 글자 중간에 걸립니다. strict 한 guess 에게는 깨진 UTF-8 이자 깨진 레거시 텍스트로 보입니다.

big_cp949_utf8.txt  [:65535] None    [:65536] None    [:65537] utf-8
big_cp932.txt       [:65535] None    [:65536] cp932   [:65537] None
big_cp949.txt       [:65535] cp1252  [:65536] cp949   [:65537] cp1252

UTF-8 파일이 "알 수 없음" 으로, 한국어 CP949 파일이 Windows-1252 로 판별됐습니다. 답이 없는 것보다 나쁩니다. 가져오기는 성공하고 깨진 한글이 저장되기 때문입니다.

고치는 법. 앞부분을 점진적 디코더와 final=False 로 디코딩합니다. 끝에 걸친 미완성 글자를 "뒤에 더 온다" 로 보고 오류로 취급하지 않습니다.

import codecs
def decode_prefix(b, enc):
    return codecs.getincrementaldecoder(enc)().decode(b, final=False)

guess 안의 b.decode 를 decode_prefix 로 바꾸자 15가지 자르기 지점 모두 정답이 나왔고, 64KiB 판별은 894ms 가 아니라 6ms 였습니다.

스트리밍 버그: 청크에 걸친 글자

스트림을 청크 단위로 디코딩할 때도 같은 일이 생깁니다. 1MB 일본어 파일(UTF-8, 그리고 같은 내용의 CP932)을 4,096바이트씩 읽었습니다.

with open(path, "rb") as f:
    while chunk := f.read(4096):
        chunk.decode(enc)          # 버그: 청크마다 따로 디코딩
strict: chunk #1 failed: 'utf-8' codec can't decode bytes in position 4094-4095: unexpected end of data
replace: U+FFFD count = 412
strict: chunk #3 failed: 'cp932' codec can't decode byte 0x83 in position 4095: incomplete multibyte sequence
replace: U+FFFD count = 86

CP932 쪽은 숫자보다 더 나쁩니다. 첫 바이트가 치환되면 그 두 번째 바이트가 다음 청크의 맨 앞이 되어 독립된 글자로 읽힙니다. 基幹システム 이 幹シス�eムか 가 됐습니다. テ(0x83 0x65)의 두 번째 바이트 0x65 가 알파벳 e 가 된 것입니다. 출력은 원본보다 85글자 길었고 원본에 없던 반각 가타카나가 59글자 섞였는데 오류는 하나도 나지 않았습니다. CP949 도 2바이트 인코딩이라 같은 구조의 위험이 있습니다.

Python 에서 고치는 법: 점진적 디코더는 미완성 바이트를 다음 호출까지 들고 갑니다.

import codecs
dec = codecs.getincrementaldecoder(enc)()
with open(path, "rb") as f:
    parts = [dec.decode(c) for c in iter(lambda: f.read(4096), b"")]
parts.append(dec.decode(b"", final=True))     # 파일이 글자 중간에서 끝나면 예외

두 파일 모두 U+FFFD count = 0 이었고 파일 전체를 한 번에 디코딩한 결과와 같았습니다. 텍스트 모드로 여는 것(open(path, encoding="cp949"))만으로도 같은 장치가 쓰입니다. PHP 에서는 fread 한 청크마다 mb_convert_encoding 을 하면 ? 가 412개(UTF-8), 87개(CP932) 늘었습니다. 파일 핸들에 convert.iconv.CP932/UTF-8 스트림 필터를 붙이면 파일 전체 변환과 같은 출력이 나왔습니다. jsc 에는 TextDecoder 가 없어 JavaScript 의 {stream: true} 는 이번에 실행하지 않았습니다.

10MiB 변환 속도

각 파일은 옛 인코딩으로 저장한 10MiB 의 실제 문장이고, 메모리에서 UTF-8 로 변환해 5회 중앙값을 냈습니다. iconv 명령 행은 프로세스 시작과 파일 읽기를 포함합니다(같은 파일의 cat 은 4ms).

UTF-8 로 변환 CP932 CP949 CP1252
Python data.decode(enc).encode("utf-8") 36 ms 33 ms 10 ms
PHP mb_convert_encoding 22 ms 37 ms 19 ms
PHP iconv() 283 ms 197 ms 186 ms
iconv 명령(macOS) 284 ms 212 ms 196 ms
막대그래프. PHP mb_convert_encoding 22밀리초, Python decode·encode 36, PHP iconv() 283, macOS iconv 명령은 프로세스 시작을 포함해 284밀리초.
10MiB 의 CP932 텍스트를 UTF-8 로 변환한 시간(Apple M1 Pro 5회 중앙값). 네 결과는 바이트 단위로 같았다.

네 결과 모두 바이트 단위로 같았습니다(cmp 와 === 로 확인). Python codecs 와 mbstring 은 대략 초당 280MB~1,000MB, macOS libiconv 는 그 10분의 1 정도였습니다. 그래도 10MiB 에 0.3초 미만이니 야간 배치라면 무엇이든 충분하고, 요청 처리 안이라면 mbstring 이나 Python codecs 를 고르세요. 덤으로, 15.6MB UTF-8 검증은 Python decode 로 10.3ms 였습니다. PHP mb_check_encoding 은 새 문자열에서 3.5~4.1ms 였지만 같은 문자열에 대한 두 번째 호출은 약 0.0002ms 였습니다. PHP 가 문자열에 "올바른 UTF-8" 플래그를 캐시하기 때문이니, 벤치마크는 중앙값이 아니라 첫 호출을 재야 합니다.

운에 기대지 않는 가져오기 흐름

흐름도. 들어온 바이트열. 헤더·BOM·계약으로 문자 집합이 선언되어 있나? 그렇다면 엄격하게 디코딩. 아니라면 올바른 UTF-8 인가? 그렇다면 UTF-8 로 디코딩. 아니라면 보낸 곳이 알려진 레거시 시스템인가? 그렇다면 cp932·cp949·cp1252 등 그 인코딩으로 엄격하게 디코딩. 모른다면 수백 바이트 이상으로 추측하고 격리해서 사람이 확인한다. 디코딩한 문자열은 NFC 로 정규화해 UTF-8 로 저장한다.
추측을 마지막 수단으로 두는 가져오기 흐름. 선언됐거나 알려진 인코딩은 엄격하게 디코딩하고, 추측한 것은 사람이 확인한다.
  1. 인코딩을 선언받는다. HTTP Content-Type: text/csv; charset=...(HTTP 헤더 강좌), XML·HTML 선언, BOM, 아니면 거래처와의 약속 한 줄. 선언은 어떤 판별기보다 낫습니다.
  2. BOM, 그다음 올바른 UTF-8 인지 확인. 이것으로 UTF-8 샘플 4,000개는 전부 맞았고 레거시 10,000개는 전부 걸러졌습니다.
  3. 송신처의 알려진 인코딩을 쓴다. 어떤 옛 시스템은 늘 같은 인코딩으로 내보냅니다. 파일마다 추측하지 말고 "송신처 → 인코딩" 표를 둡니다. 공공데이터 CSV 처럼 윈도우 엑셀에서 나온 파일이면 EUC-KR 이 아니라 CP949 로 적어 두어야 똠·뷁 에서 막히지 않습니다.
  4. strict 하게 디코딩한다. Python 은 errors="strict", PHP 는 mb_convert_encoding 전에 mb_check_encoding. 가져오기 실패가 조용한 한글 깨짐보다 쌉니다.
  5. 추측은 마지막 수단. 수백 바이트 이상으로, 실제로 받는 지역으로 좁힌 후보 목록으로 하고, 결과는 사람의 확인이나 격리 폴더로 보냅니다.
  6. 스트림은 점진적 디코더로. 청크마다 decode 하지 않습니다.
  7. 정규화해서 UTF-8 로 저장. NFC(3강), MySQL 은 utf8mb4(5강), 엑셀 사용자용 CSV 는 4강대로.

솔직한 한계와 버전 메모

  • 샘플은 직접 쓴 작은 문장 세트 두 개에서 뽑았습니다. 주소, 상품 코드, 여러 언어가 섞인 실제 데이터에서는 결과가 달라집니다. 표는 짧은 문자열 효과를 보여 주는 시연이지 PHP 벤치마크가 아닙니다.
  • mb_detect_encoding 의 채점 방식은 PHP 8.1 에서 다시 만들어졌습니다. PHP 7 이나 8.0 에서는 결과가 다를 수 있습니다. 실행한 것은 8.5.6 뿐입니다.
  • chardet, charset-normalizer, ICU CharsetDetector 는 쓸 수 없어 시험하지 않았습니다. 리눅스와 GNU iconv 도 실행하지 않았습니다.
  • 같은 인코딩 이름이라도 도구마다 다릅니다. Python 의 euc_kr 은 똠 같은 음절을 KS X 1001 의 8바이트 "조합" 열(a4 d4 a4 a8 a4 c7 a4 b1)로 받아들였지만, macOS 의 iconv -t EUC-KR 은 똠 을 Illegal byte sequence 로 거부했습니다.
  • 속도는 Apple M1 Pro 한 대의 값입니다. 가져갈 것은 "mbstring 과 codecs 가 빠르고 macOS libiconv 는 느리다" 는 순서입니다.

연습 문제

1. mb_detect_encoding($b) 가 Shift_JIS 바이트열에 "ASCII" 를 돌려준 이유는 무엇이고, 무엇을 바꾸면 고쳐지나요?

2. 점진적 디코더에 1바이트씩 넣어 보세요. "똠방각하, 뷁! 한글 깨짐 테스트".encode("cp949") 를 codecs.getincrementaldecoder("cp949")() 에 1바이트씩 넣어 디코딩하면 한 번에 디코딩한 결과와 같을까요?

정답

1. 후보 목록이 없어서 PHP 가 판별 순서 ASCII, UTF-8 을 썼고, non-strict 모드에서는 어느 것도 올바르지 않아도 가장 가까운 후보를 돌려주기 때문입니다. 후보를 명시하고 strict 로 바꿉니다. mb_detect_encoding($b, ["UTF-8", "SJIS-win"], true) 는 "SJIS-win" 을 돌려줬습니다.

2. 같습니다. 출력은 똠방각하, 뷁! 한글 깨짐 테스트 True 였습니다. 디코더는 0x8c 같은 첫 바이트를 두 번째 바이트가 올 때까지 들고 있으므로 청크 크기가 1이어도 안전합니다. UTF-8 로 보면 더 잘 보입니다. dec.decode(b"caf\xc3") 는 'caf' 를, 이어서 b"\xa9!" 를 넣으면 'é!' 를 돌려줬습니다. 새 디코더에서 decode(b"\xc3", final=True) 를 부르면 unexpected end of data 예외가 났습니다.

시리즈를 마치며

이번이 마지막 강의입니다. 바이트와 코드 포인트(1), 깨진 글자(2), 정규화(3), CSV 와 엑셀(4), 문자열 길이(5), 그리고 이번 자동 판별까지 규칙은 늘 같았습니다. 지금 다루는 것이 어떤 인코딩, 어떤 단위인지 알고, 모든 경계에서 명시할 것.

자주 묻는 질문

mb_detect_encoding 은 한글 CP949 를 얼마나 맞히나요?

후보 목록의 순서와 글자 수에 크게 좌우됩니다. 일본어 인코딩을 앞에 둔 목록에서는 4글자 한국어 CP949 샘플 200개 중 47개만 맞혔고, 16글자에서 132개, 64글자 이상에서야 200개 모두 맞혔습니다. UHC(CP949)를 앞에 두면 4글자에서도 200개 모두 맞혔지만, 이번에는 일본어 EUC-JP 가 200개 중 60개로 떨어졌습니다.

EUC-KR 로 판별됐는데 똠, 뷁 이 물음표가 되는 이유는?

EUC-KR(KS X 1001 완성형)에는 2,350개 음절만 있고 똠·뷁 은 CP949 의 확장 영역(8C 63, 94 EE)에 있기 때문입니다. PHP 에서 mb_detect_encoding 은 후보가 EUC-KR 뿐이면 false 를 돌려줬고, mb_convert_encoding(…, 'EUC-KR') 은 '?c??' 를 만들었습니다. 윈도우에서 만든 파일이면 EUC-KR 대신 CP949(PHP 이름 UHC)를 명시하세요.

파일을 나눠 읽으면 한글이 깨지는 이유는?

멀티바이트 문자가 두 청크에 걸쳐 잘렸는데 청크마다 따로 디코딩했기 때문입니다. 1MB 일본어 UTF-8 파일을 4,096바이트씩 읽으면 strict 모드에서 첫 경계에서 실패했고 errors='replace' 로는 U+FFFD 가 412개 생겼습니다. Python 의 codecs.getincrementaldecoder, PHP 의 convert.iconv 스트림 필터를 쓰면 파일 전체를 한 번에 변환한 결과와 완전히 같았습니다.

  • #문자 인코딩
  • #고급
  • #유니코드
  • #CP949
  • #PHP
  • #Python
  • #iconv
  • #성능

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑