개요
프로그램 개발을 하면서 항상 제대로 알고있는건지 의심하게 되는, 사소할 수 있는 정보를 한번 정리해봤다. 예를들어 한국의 국가-언어 코드는 ko-kr(ko_KR)이고, 한글은 유니코드 또는 EUC-KR 인코딩 방식을 사용하고, 시간표기에서 Asia/Seoul(UTC+0900) 시간대에 속해있다. 이 문서는 이 중에서 한글을 표기하기위한 코드페이지 표준에 대한 내용을 담고있다.
인코딩 방식
컴퓨터에는 인간의 언어를 기계어로 변환하는 코드표가 존재한다. 컴퓨터 프로그래밍을 배울때 가장 처음 배우는 코드표는 ASCII코드로, 128비트 안에서 알파뱃과 일상생활에서 사용되는 특수기호들을 표현하기 위해 만들어졌다. 이후에 여러언어로 확장되면서 256비트로 늘어났지만 세계의 모든 언어를 표현할 수는 없었기 때문에 이후에 다양한 인코딩 방법이 만들어진다. 그 중에서도 한국에서 널리 사용되는 인코딩 방식으로는 EUC-KR, CP949, 그리고 유니코드가 있다.
유니코드
현재 가장 널리 사용되고있고, 인터넷 환경에서는 표준으로 자리잡은 방식이다. 가장 최신 버전으로는 유니코드 14버전이 있고, 15버전이 alpha버전으로 만들어지는 중으로 보인다. 전체 코드표는 pdf파일로 되어있다.
유니코드는 ISO-8859 표준 논의에서 파생된 표준 코드이다. 유니코드는 몇가지 바리에이션이 있는데, UTF-7, UTF-8, UTF-16, UTF-32 등으로 분화된다. 여기에서 현재 가장 많이 쓰이는것은 UTF-8과 UTF-16인데, UTF-16은 UTF-8에서 확장되는 형태로 표준이 제정되었다. 유니코드에서 참고할만한 것은 정규식으로, 알파뱃을 다 포함시키려면 [a-zA-Z] 라고 표현하면 된다는 것은 다 알고 있을 것이다. 한글을 이렇게 포함시키려면 [가-힣] 이렇게 쓰면 된다. 유니코드 테이블에서 한글의 가장 마지막 글자가 힣 이기 때문이다.
EUC-KR
이 포맷은 한국 산업규격인 KS X 1001을 바탕으로 만든 완성형 코드표로 한글을 컴퓨터로 다루기위해 만들어진 국내 표준 규격이다. 이때에 한국에는 조합형 코드도 있었지만 코드 자체가 이해하기 까다롭고 cpu 아키텍처가 확장되면서 코드를 파싱하는데 어려움이 따랐다고 전해진다. 완성형 코드표이기 때문에 8,836개의 글자를 코드화 해서 담고있다(위키피디아 KS X 1001 페이지 참고). 그래서 옛날에는 한국에서 잘 쓰이는데도 컴퓨터로 쓸 수 없는 글자들이 많이 있었다.
CP949
이 인코딩 포맷은 Code page 949의 약자로, 마이크로소프트가 한글 윈도우즈를 만들면서 EUC-KR 포맷을 확장해서 만들었다. 그래서 전국의 90%이상의 컴퓨터가 윈도우즈 운영체제를 사용중이었던 한국에서는 완성형 코드인 CP949라는 인코딩 포맷이 da facto로 자리잡았고 최근까지도 간혹 한글 윈도우즈에서 유니코드와 CP949가 충돌하면서 글자가 깨지는 문제가 있었다. 만약 구버전 윈도우즈에서 만들어진 텍스트파일의 글자가 정상적으로 출력되지 않는다면 이 인코딩 방식을 의심해볼 필요가 있다.
파이썬에서의 Encoding
파이썬3에서는 공식적으로 모든 스크립트의 인코딩 방식이 유니코드인 것을 기본으로 하고있다. 그러나 유니코드를 실제로 다뤄보면 또 다른 문제가 있다는 것을 알게된다. 아래 예제는 파이썬 공식 문서의 코드를 참고로 했다.
import unicodedata
def compare_strs(s1, s2):
def NFD(s):
return unicodedata.normalize('NFD', s)
return NFD(s1) == NFD(s2)
single_char = 'ê' # 0x00EA
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}' # 0x0065 0x0302
print('length of first string=', len(single_char)) # 1
print('length of second string=', len(multiple_chars)) # 2
print(compare_strs(single_char, multiple_chars)) # True; 둘 다 같은 글자.
저 문자는 로마자로 위에 강세를 표현하는 기호가 혼합된 글자인데, 이 둘을 비교한 것이다. 위 코드에서 말하는 것은 같은 글자를 표기하더라도 두 기호를 합성한 글자인 경우에는 single_char이 multiple_chars로 서로 다를 수 있다는 것이다. 저 둘을 단순히 비교하면 False가 나온다. 이것을 위해서 서로다른 표기를 normalize하는 과정이 필요한데, 그게 저 함수인 unicodedata.normalize이다.
그럼 한글은 어떨까… 싶어서 이런 코드를 실행시켜봤다.
>>> import unicodedata
>>> s = '맥북프로'
>>> nfc_s = unicodedata.normalize('NFC', s)
>>> nfkc_s = unicodedata.normalize('NFKC', s)
>>> nfd_s = unicodedata.normalize('NFD', s)
>>> nfkd_s = unicodedata.normalize('NFKD', s)
>>> nfc_s
'맥북프로'
>>> nfkc_s
'맥북프로'
>>> nfd_s
'ㅁㅐㄱㅂㅜㄱㅍㅡㄹㅗ'
>>> nfkd_s
'ㅁㅐㄱㅂㅜㄱㅍㅡㄹㅗ'
>>> [ord(c) for c in nfc_s] # 각 글자들의 코드값
[47589, 48513, 54532, 47196]
>>> [ord(c) for c in nfkc_s]
[47589, 48513, 54532, 47196]
>>> [ord(c) for c in nfd_s]
[4358, 4450, 4520, 4359, 4462, 4520, 4369, 4467, 4357, 4457]
>>> [ord(c) for c in nfkd_s]
[4358, 4450, 4520, 4359, 4462, 4520, 4369, 4467, 4357, 4457]
NFC, NFKC는 조합된 완성형 한글로 글자를 다루고있고, NFD, NFKD는 한글 초성, 중성, 종성을 모두 분리해서 따로 다루고 있음을 알 수 있다.
NFC(D)와 NFKC(D)의 차이는 무엇인가 하면 stack overflow에서 해답을 찾을 수 있다.
>>> unicodedata.normalize('NFC', '\u2167')
'Ⅷ'
>>> unicodedata.normalize('NFKC', '\u2167')
'VIII'
\u는 유니코드 코드임을 표현하는 prefix이고, 2167은 로마자 8을 의미한다. 이걸 NFKC로 normalize하면 저 조합을 알파뱃 V, 알파뱃 I, 알파뱃 I, 알파뱃 I 이렇게 따로 분해해버린다. 파이썬 문서에서는 compatibility equivalence 이라고 되어있는데, 아무래도 VIII가 로마자 숫자 8과 같은 모양으로 표기되는 것을 서로 호환된다고 보는것 같다.