파이썬에서 한글이 깨진다고요? – 파이썬의 한글 입출력과 인코딩에 대해

파이썬의 한글 인코딩에 대해

파이썬의 대화형 인터프리터를 사용하다보면 한글 인코딩의 함정에 빠지기 쉬운데 이를테면 소스를 그대로 해석기로 실행하는 경우에는 인코딩 에러가 안나던 것이, IDLE을 통해서 실행해보면 오류가 난다거나 그 반대의 경우가 있다. 이렇게 이해할 수 없는 상황을 어떻게 해야할까?

몇년 전이라면 그것은 MS의 잘못이거나 파이썬의 잘못이었다. 하지만 윈도에서 한글로 된 데이터를 다뤄야 하는데 파이썬 2를 쓰고 있다면 그것은 매우 높은 확률로 당신의 잘못이다.

현재 실행환경(쉘)의 문자열 인코딩을 확인한다

일단 윈도의 명령줄 도구에서 몇 가지 체크를 우선해보도록 하자.

파이썬 셸(2.7.6)를 실행하고 입출력 인코딩을 확인하자. 기본적으로 터미널의 로케일을 따를테기 때문에 CP949로 되어 있을 확률이 크다. 입출력 인코딩 값은 sys 모듈의 stdin.encoding ,stdout.encoding 을 통해서 확인할 수 있다.

>>> import sys
>>> sys.stdin.encoding
'cp949'
>>> sys.stdout.encoding
'cp949'
>>>

이는 곧, 상호대화형 셸을 통해서 입력한 문자들은 모두 인코딩 CP949를 적용한 값으로 간주하며, 출력되는 문자열 역시 CP949로 인코딩되어 나간다는 의미이다.

유니코드 문자열 테스트

한가지 예를 들어 확인해보자. 한글 ‘가’의 유니코드 값은 ac00이고 이의 파이썬식 코드 표기는 uac00이 될 것이다. 파이썬 2.x에서는 문자열과 유니코드 문자열이 구분되며, 유니코드 문자열을 생성하는 경우, u를 따옴표 앞에 붙여준다. 이렇게 생성한 값은 print 명령으로 찍으면 로 출력되지만, 대화형 쉘에서 변수값을 보면 원래 코드값을 볼 수 있다.

>>> a = u'가'
>>> a
u'uac00'
>>> print a
가
>>>

유니코드 문자열인 u'가'print 문을 거칠 때에는 sys.stdout.encoding에 명시된 문자열 인코딩을 통해서 변환되어 송출된다.

일반 한글 입출력을 확인

이번에는 그냥 '가'에 대해 조사해보자. 참고로 지금 테스트를 하는 환경은 파이썬 쉘이며 이때 정의되는 한글 문자열은 cp949로 인코딩된 값을 키보드를 통해서 전달하는 중이다.

>>> b = '가'
>>> b
'xb0xa1'
>>> print b
가
>>> b.decode('cp949')
u'uac00'
>>> print b.decode('cp949')
가

b의 값은 u'가' 와는 다른 값이며, 이 값을 cp949로 디코딩하였을 때 유니코드 문자 ‘가’와 같은 값이 되는 것을 확인할 수 있었다. 그리고 디코딩한 결과는 print 문을 거치면서 다시 cp949로 인코딩 되기 때문에 윈도의 기본 콘솔에서 깨짐 없이 표시될 수 있다.

UTF-8로 인코딩한 문자

그럼 UTF8 인코딩을 사용하면 무슨일이 생길까?  윈도의 명령 프롬프트 환경에서는 UTF-8 문자열을 입력할 수 없다. (사실 방법이 있는데, 텍스트 파일로 저장해서 열면된다.) 그래서 유니코드 문자 u'가'를 UTF-8로 인코딩해보자.

>>> c = a.encode('utf-8')
>>> c
'xeaxb0x80'
>>> print c
媛€

인코딩된 값은 정상인데, 이를 출력하면 당연히 깨진다.  이 값을 실제로 CP949로 다시 인코딩해서 출력해보자.

>>> c.decode('cp949','ignore')
u'u5a9b'
>>> print u'u5a9b'
媛

짠~ 위에서 깨진 문자랑 같은 값이다.

print utf-8문자열을 제대로 표시하려면 유니코드 값으로 풀어주거나(디코딩) 아니면 표준 출력이 지원하는 인코딩으로 변환해야 한다. (물론 그 과정에 유니코드로 다시 풀어야 한다.)

그런데 소스코드에 한글을 쓸 때는?

그런데 소스코드에서 한글 문자열을 쓸 때 우리는 u"..." 같은 표기를 쓰지 않는다. 어떻게 파이썬은 한글이 들어간 문자열을 해석할 수 있을까? 그것은 비라틴 문자열이 들어간 소스코드가 있는 경우에는 소스파일의 서두에 주석으로 #-*-coding: utf-8 등과 같이 소스 코드를 저장할 때 사용한 인코딩을 명시한다. 그러면 파이썬 해석기가 소스 코드 파일을 읽어 들이는 과정에서 이 인코딩을 사용하여 비라틴 문자를 디코딩한 값을 갖게되고, print 문에서는 이렇게 디코딩된 값을 다시 cp949로 인코딩하여 콘솔에 출력하게되니 한글이 깨지지 않는 것이다.

만약 한글과 같은 비라틴 문자를 쓰면서 인코딩을 명시하지 않으면 예외가 발생하면서 프로그램이 실행되지 않을 것이다.

idle 의 문제

idle의 설정에서 소스코드인코딩을 설정하는 부분이 있는데, 그것은 편집기 모드에서 저장하는 글자의 인코딩을 지정하는 것이지, IDLE 셸의 입출력 인코딩과는 관련이 없다. 따라서 이런 문제가 발생할 수 있다. 여기서부터의 입출력 내용은 IDLE 에서 실행한 것이다. (파이썬 2.7)

>>> a = '가'
>>> a
'xb0xa1'
>>> print a
가
>>> b = u'가'
>>> b
u'xb0xa1'
>>> print b
°¡
>>> 

먼저 한글 문자열을 변수에 할당하면 정상적인 cp949 코드값으로 입력됨을 알 수 있다. 그런데 유니코드 문자열을 생성하면, 따옴표 안의 문자열을 디코딩해야 하는데, 디코딩하지 않고 그대로 유니코드 문자열로 인식해버린다.

>>> c  = a.decode('cp949')
>>> c
u'uac00'
>>> print c
가

실제로 디코딩한 결과는 정상적이다. 단지 변수에 ascii 외의 코드가 담긴 문자열로 바로 유니코드 문자열을 만들면 안되는 것 같다. 이는 idle의 버그로 보인다.

<update 2017-06-21> 파이썬 2.7.13의 IDLE에서는 이 문제가 해결되었다.

편집기에서 소스 코드 인코딩을 명시한 경우

만약 소스 코드의 인코딩이 명시되어 있다면 직접적으로 선언한 문자열의 인코딩은 idle 에서 제대로 표시한다. 한글로 ‘가’라고 명시한 문자열은 결국 'xeaxb0x80'로 저장될 것이며, 콘솔에서 실행했던 것과 마찬가지로 cp949로 인코딩을 변경하거나 유니코드로 디코딩하지 않으면 제대로 출력이 되지 않고 깨진 글자가 출력될 것이다.

#-*-coding:utf-8

a = '가'
print repr(a)
print a
print

b = u'나'
print repr(b)
print b
print

c = b.encode('cp949')
print repr(c)
print c
print

위와 같은 코드를 실제로 utf-8 인코딩으로 저장한다. 이를 실행한 결과는 다음과 같다.

D:temp>python enc_test.py
'xeaxb0x80'
媛€

u'ub098'
나

'xb3xaa'
나

문제는 idle인데, idle에서 위 소스를 실행한 결과는 당췌 설명할 방법이 없다. UTF8 인코딩으로 넘어간 값이 제대로 출력된다 (왜?)

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
'xeaxb0x80'
가

u'ub098'
나

'xb3xaa'
나

>>> 

파이썬 2를 사용하여 작업한다면 UTF8 문자열을 처리해야 하는 경우, idle을 신뢰하지 말고 출력할 문자열을 파일에 써서 확인하는 수 밖에 없을 것 같다.

파이썬 3에서의 한글 처리

파이썬 3에서 문자열은 모두 유니코드 문자열로 처리된다. 이에 비해 파이썬 2는 문자열을 실질적으로는 소스코드 혹은 콘솔입력등에서 받아들인 인코딩된 문자열데이터를 디코딩한 바이트스트림(바이트 배열)을 사용하여 저장했다.

뭐가 바뀌었는지 기술적으로 이해가 힘들 수 있으니 간단하게 설명하자면 다음과 같이 생각하면 된다.

  1. 문자열은 인코딩이 안된 유니코드 문자열이며
  2. 모든 입출력에 대해서는 필수적으로 인코딩이 필요하다.

예를 들어 텍스트 파일을 읽어들이는 경우에, 파이썬 2에서는 텍스트 파일을 그냥 읽어서 그 내용을 미리 알고 있는 인코딩 종류를 사용하여 디코드해서 사용해야 했다.

 

## python 2.7에서 한글 파일 읽는 법 (한글 파일은 UTF-8로 인코딩되었다고 가정)
f = open('myfile.txt', 'r')
content = f.read() # content는 인코딩된 그대로의 바이트 스트림
text = content.decode('utf8') # 바이트배열을 디코딩하여 문자열데이터로 변환
print(text)

하지만 파이썬3에서는 기본적으로 인코딩을 명시해야 한다. (생략하는 경우 기본적으로 utf8이다)

## 파이썬 3에서 한글 텍스트 파일 읽기
f = open('myfile.txt', 'r', encoding='utf8')
text = f.read() # 읽어들이면서 지정된 인코딩을 이용하여 문자열 데이터로 변환한다.
print(text)

 

파이썬3에서의 한글 등의 문자 처리는 덕분에 매우 쉬워졌다. 다음의 규칙만 지키면 된다.

  1. 소스 코드 작성시에는 인코딩을 명시할 것
  2. 외부로부터 문자열을 받아들이거나 읽어야 하는 경우에는 보통 미리 인코딩을 제시하고, 항상 ‘문자열을 읽는다’라고 생각한다.
  3. 반대로 문자열을 외부로 쏴주거나 파일에 쓸 때에도 핸들러를 생성하는 시점에 인코딩을 결정하고, 그대로 쓰면 된다.

파이썬 2.7에서는 한글이 섞인 데이터를 다루기 위해서는 디코딩했다가 처리하고 다시 인코딩해서 전송/기록하고 하는 등의 삽질을 겪어야 했고, 어디 한 군데에서 까딱 빼먹거나 실수하면 그대로 내용이 깨지는 참사로 이어졌던 것에 (그리고 IDLE에서의 버그로 IDLE에서는 되는게 실제 처리시에는 안되고…) 비하면 엄청나게 인터페이스가 깔끔해졌다.

결론 – 윈도 환경에서 파이썬으로 한글을 다룰 때

윈도의 콘솔 입출력 인코딩은  UTF-8이 아니고, 웹이나 왠만한 파일의 인코딩은 UTF-8이며, 파이썬 2.7은 내부적으로 유니코드를 사용하지 않는다. 두 가지도 아닌 이 세가지의 인코딩 미스매치는 안그래도 인코딩이 어쩌고 하는 개념이 이해가 될리 없는 초보자에게 너무나 크나큰 고통이다. 따라서 다음의 지침을 따를 것을 권장한다.

  1. 무조건 Python 3 를 쓸 것
  2. 가능하면 Jupyter Notebook을 쓸 것
  3. 파일을 열고 읽거나/쓸 때, 네트워크 요청으로 데이터를 받아올 때 항상 인코딩을 먼저 명시할 것. 이 때의 인코딩은 해당 데이터 원본이 쓰고 있는 인코딩이어야 한다.

파이썬 3가 나온지가 몇년짼데…. 아직도 파이썬2 써도 괜찮다는 조언해주는 전문가가 근처에 있다면, 사기꾼이다. 철저히 무시해라.