작성자: admin 작성일시: 2016-06-14 23:09:51 조회수: 3913 다운로드: 118
카테고리: Python 태그목록:

파이썬의 문자열 인코딩

문자와 인코딩

파이썬 뿐 아니라 모든 컴퓨터에서 문자는 2진 숫자의 열 즉, 바이트 열(byte sequence)로 바뀌어 저장된다. 이를 인코딩(encoding)이라고 하며 어떤 글자를 어떤 숫자로 바꿀지에 대한 규칙을 인코딩 방식이라고 한다.

가장 기본이 되는 인코딩 방식은 아스키(ASCII) 방식이다.

한글의 경우 과거에는 EUC-KR 방식이 많이 사용되기도 했으나 최근에는 CP949 방식이 더 많이 사용된다.

유니코드

인코딩 방식이 글자마다 혹은 회사마다 다르기 때문에 발생하는 문제를 해결하고자 유니코드라는 것이 만들어졌다. 유니코드에서는 다음과 같은 두 가지 규칙을 정했다.

  • 유니코드 코드 포인트 (unicode code point)
  • 유니코드 인코딩 (UTF-8, UTF-16, UTF-32, ...)

유니코드 코드 포인트(code point) 또는 코드 포지션(code position)은 4 바이트(byte)의 고정된 크기의 술자로 전 세계 모든 글자에 대해 숫자를 대응시킨 것이다. 문서 크기 문제 등으로 직접 문서 파일을 저장할 때는 쓰이지 않고 파이썬 등의 프로그래밍 내에서 문자 데이터를 메모리에 저장할 때 주로 사용된다.

유니코드 인코딩은 실제로 유니코드 문자를 바이트 열로 바꾸어 파일에 저장할 때 사용되는 인코딩 방식으로 ASCII 인코딩 방식과 호환성을 가지며 크기, 정렬 등의 문제를 고려하여 설계된 인코딩 방식이다. 과거의 EUC-KR, CP949 등은 한글이라는 문자에 대해 저장할 값을 지정하는 방식이지만 유니코드 인코딩 방식은 유니코드 코드 포인트에 대해 저장할 값을 지정하는 방식이므로 유니코드 코드 포인트를 가지는 모든 글자를 하나의 방식으로 저장할 수 있다. UTF-8은 현재 가장 많이 사용되는 유니코드 인코딩 방식의 하나이다.

파이썬의 문자열 자료형

파이썬 2에서는 기본 문자열 자료형이 바이트 열인 str 타입이다. 또한 만약 유니코드 포인트 방식으로 저장하는 unicode 타입도 지원한다. str 타입과 unicode 타입간의 변환을 위해서는 다음과 같은 방법을 사용한다.

  1. unicode 명령어로 유니코드 코드 포인트 반환
  2. encode(인코딩)/decode(디코딩) 명령 사용
  3. u 접두사를 이용한 유니코드 리터럴(literal) 생성

Python 3에서는 반대로 str 이란 이름의 타입이 유니코드이며 바이트 열로 다음과 같은 명령으로 유니코드 포인트와 인코딩된 바이트 열를 처리한다.

  1. bytearray 명령어로 바이트열 반환
  2. encode(인코딩)/decode(디코딩) 명령 사용
  3. b 접두사를 이용한 바이트열 리터럴(literal) 생성

Python의 문자열 표시

파이썬에서 콘솔 혹은 화면에 문자열을 표시하려면 __repr__() 메서드 혹은 print() 명령을 쓴다.

__repr__() 메서드는 콘솔 화면에서 문자열 값을 쳤을 때 표시되는 표현(representation)을 출력하고 print() 명령은 명시적으로 문자를 표시한다.

이 때 영어는 두 방법이 똑같이 글자를 표시하지만 한글의 경우에는 다르다. __repr__() 명령은 아스키 테이블로 표시할 수 없는 한글의 경우 바이트 열로 표시하므로 한글로 읽을 수 없으나 print() 명령은 가능한 한글을 찾아서 화면에 표시한다.

예를 들어 a 라는 글자를 그냥 표시하거나 프린트하면 똑같이 보이지만

In:
c = "a"
c
Out:
'a'
In:
print(c)
a

"가"라는 글자를 표시할 때는 __repr__() 명령은 0xEAB080 이라는 값을 표시할 뿐이고 print 명령을 해야지 정상적으로 한글을 보여준다. 다만 한글이 리스트나 딕셔너리 안에 원소로 들어있는 경우에는 print 명령으로도 볼 수 없고 추후 설명할 konlpy의 pprint 명령을 사용해야 한다.

In:
x = "가"
x
Out:
'\xea\xb0\x80'
In:
print(x)
In:
print(x.__repr__())
'\xea\xb0\x80'

유니코드로 한글을 저장해야 하는 이유

만약 한글 문자열을 단순히 화면에 출력하거나 파일로 내보내기만 하는 것인 목적이라면 굳이 유니코드를 이용할 필요는 없다. 그러나 한글 문자열을 분석해야 한다면 반드시 유니코드로 변환해야 한다. 왜냐하면 유니코드가 아닌 인코딩된 바이트열은 한글의 글자 수를 세거나 글자 단위로 분리하는 것이 어렵기 때문이다.

예를 들어 "가"라는 한 글자를 리스트에 넣었을 때, 이 글자가 유니코드이면 리스트의 원소의 수는 1로 계산된다. 하지만 바이트 열이면 인코딩 방식에 따라 달라진다.

In:
x = "가"
len(x)
Out:
3

글자를 한글자씩 분리할 수도 없다.

In:
x = "ABC"
y = "가나다"
print(len(x), len(y))
print(x[0], x[1], x[2])
print(y[0], y[1], y[2])
print(y[0], y[1], y[2], y[3])
3 9
A B C
� � �
� � � �

유니코드 리터럴(Literal)

파이선 코드에서 문자열을 만들 때, 따옴표 앞에 u자를 붙이면 자동으로 유니코드 문자열로 인식하고 내부적으로 유니코드 포인트로 저장

In:
y = u"가"
y
Out:
u'\uac00'
In:
print(y)
In:
y = u"가나다"
print(y[0], y[1], y[2])
가 나 다

인코딩(Encoding) / 디코딩(Decoding)

유니코드를 바이트열로 변환할 때는 인코드 encode 메서드를, 바이트열을 유니코드로 변환할 때는 디코드 decode 메서드를 사용한다.

그런데 인코딩을 하는 경우에는 출력 디바이스(콘솔, 주피터 노트북)이 어떤 인코딩을 지원하는지를 미리 알고 있어야 한다. 만약 출력 디바이스가 지원하지 않는 방식으로 인코딩하면 화면에는 이상한 글자만 보이게 될 것이다.

예를 들어 현재 저자가 이 코드를 실행하는 주피터 노트북에서는 utf-8 인코딩을 지원한다. 따라서 CP949로 인코딩한 바이트 열은 글자가 깨져서 보이게 된다.

In:
print(type(y))
z1 = y.encode("cp949")
print(type(z1))
print(z1)


������
In:
print(type(y))
z2 = y.encode("utf-8")
print(type(z2))
print(z2)


가나다

디코딩을 할 때도 바이트열이 원래 인코딩된 방식으로 디코딩을 해야 제대로 된 문자열을 얻을 수 있다.

In:
print(type(z1))
y1 = z1.decode("cp949")
print(type(y1))
print(y1)


가나다
In:
print(type(z2))
y2 = z2.decode("utf-8")
print(type(y2))
print(y2)


가나다

str에 encode 메소드를 적용하면 또는 unicode에 decode 메소드를 적용하면?

파이썬에서 바이트열과 유니코드를 공부할 때 혼란을 주는 요소 중에 하나는 바이트열 문자열에도 encode 메서드가 있고 유니코드 문자열에도 decode 메서드가 있다는 점이다.

바이트열은 유니코드로 디코딩하는 것만 가능한데 왜 encode 메서드가 있을까? 또 유니코드는 바이트열로 인코딩하는 것만 가능한데 왜 decode 메서드가 존재할까? 우선 실제로 이 메서드가 동작하는지 확인해 본다.

In:
"가".encode("utf-8")

UnicodeDecodeErrorTraceback (most recent call last)
 in ()
----> 1 "가".encode("utf-8")

UnicodeDecodeError: 'ascii' codec can't decode byte 0xea in position 0: ordinal not in range(128)

일단 바이트열 문자열에 encode 메서드가 존재하고 호출된다는 것은 확인하였다. 다만 실행시 한글 바이트열로 테스트하면 위와 같은 오류가 발생한다. 그런데 자세히 보면 위의 에러 메세지가 뭔가 이상하다는 것을 알 수 있다. 분명히 encode 메서드를 호출했는데 에러는 DecodeError가 발생하였다!

그 이유는 바이트열에 인코딩을 시도하면 실제로는 디코딩을 하여 유니코드로 바꾼 뒤 다시 인코딩을 하려고 하기 때문이다. 이 때 디코딩할 방식을 지정하지 않았기 때문에 가장 기본적인 ASCII 방식으로 디코딩하려고 한다. 그런데 한글은 ASCII로 디코딩이 되지 않으므로 에러가 발생한다.

따라서 위 명령은 내부적으로 다음과 같다.

In:
unicode("가", "ascii").encode("utf-8")

UnicodeDecodeErrorTraceback (most recent call last)
 in ()
----> 1 unicode("가", "ascii").encode("utf-8")

UnicodeDecodeError: 'ascii' codec can't decode byte 0xea in position 0: ordinal not in range(128)

반대의 경우도 마찬가지이다.

In:
u"가".decode("utf-8")

UnicodeEncodeErrorTraceback (most recent call last)
 in ()
----> 1 u"가".decode("utf-8")

/home/joel/anaconda2/lib/python2.7/encodings/utf_8.pyc in decode(input, errors)
     14 
     15 def decode(input, errors='strict'):
---> 16     return codecs.utf_8_decode(input, errors, True)
     17 
     18 class IncrementalEncoder(codecs.IncrementalEncoder):

UnicodeEncodeError: 'ascii' codec can't encode character u'\uac00' in position 0: ordinal not in range(128)
In:
u"가".encode("ascii").decode("utf-8")

UnicodeEncodeErrorTraceback (most recent call last)
 in ()
----> 1 u"가".encode("ascii").decode("utf-8")

UnicodeEncodeError: 'ascii' codec can't encode character u'\uac00' in position 0: ordinal not in range(128)

디폴트 인코딩

자신이 현재 사용하고 있는 출력 디바이스가 어떤 인코딩을 지원하는지 알기 위한 방법의 하나는 실제로 인코딩을 해서 나온 결과와 그냥 문자열을 바이트열로 입력해서 나온 결과를 비교하는 것이다. 아래에는 utf-8 인코딩 결과, CP949 인코딩 결과, 그리고 디폴트 인코딩 결과를 비교하고 있다. 이 경우에는 디폴트 인코딩이 utf-8임을 알 수 있다.

In:
u"가".encode("utf-8"), u"가".encode("cp949"), "가"
Out:
('\xea\xb0\x80', '\xb0\xa1', '\xea\xb0\x80')

만약 윈도우즈 DOS 콘솔에서 파이썬을 실행하고 위의 명령을 실행하면 아마도 CP949 인코딩 결과를 얻을 수 있을 것이다.

디폴트 인코딩 방식을 알 수 있는 다른 방법으로는 다음과 같이 sys 패키지나 locale 패키지를 이용하는 방법도 있다.

In:
import sys
print(sys.getdefaultencoding())
print(sys.stdin.encoding)
print(sys.stdout.encoding)
import locale
print(locale.getpreferredencoding())
ascii
None
UTF-8
UTF-8

인코딩 설정

파이썬이 콘솔로 입력된 코드나 스크립트 코드를 읽을 때는 이 코드가 어떤 방식으로 인코딩되어 있는지 알고 있어야 한다. 콘솔 입력의 경우에는 환경 변수 PYTHONIOENCODING 로 지정하고 스크립트(파일)의 경우에는 첫줄에 다음과 같이 인코딩 설정 정보를 넣어 주면 된다.

#-*- coding: utf-8 -*-

질문/덧글

아직 질문이나 덧글이 없습니다. 첫번째 글을 남겨주세요!