파이썬 유니코드 하우투

원 문 : http://www.amk.ca/python/howto/unicode
작 성 : A.M. Kuchling
한글판 johnsonj 2005.08.20

이 하우투에서는 파이썬의 유니코드 지원을 알아보고, 유니코드로 작업하고자 할때 흔히 마주하는 다양한 문제들을 설명한다.

유니코드 개론

문자 코드의 역사

1968년 이른바 ASCII라는 약자로 더 잘 알려진, 정보 교환용 미국 표준 코드가 표준화되었다. 다양한 문자들에 대하여 ASCII에서 숫자 코드가 정의되었는데, 그 수치는 0에서 127에 이른다. 예를 들어, 소문자 'a'는 97이 그의 코드 값으로 할당되었다.

ASCII는 미국-개발 표준이었다. 그래서 액센트가 없는 문자들만 정의되었다. 'e'는 있었지만, 'ê'이나 'Î'는 없었다. 이는 곧 액센트가 붙은 문자를 요구하는 언어가 ASCII로는 제대로 표현이 불가능하다는 뜻이다. (실제로 액센트의 부재는 영어에도 문제가 되었는데, 예를 들어 'naïve'와 'cafè' 같은 문자에 포함되어 있었고, 어떤 서적에서는 회사 스타일로 'coöperate' 같은 철자를 요구한다.)

잠시 동안 사람들은 그냥 액센트를 보여주지 않는 프로그램을 작성했다. 프랑스-어로 1980년 중반에 출간된, Apple ][ BASIC 프로그램을 본 기억이 있는데, 다음과 같은 줄이 있었다:

PRINT "FICHER EST COMPLETE."
PRINT "CARACTERE NON ACCEPTE."

이 메시지에는 액센트가 포함되어 있어야 하며, 프랑스어를 읽을 줄 아는 사람에게는 완전히 엉터리로 보인다.

1980년대에 대부분의 개인용 컴퓨터는 8-비트였고, 그 바이트로는 0에서 255 범위의 값들을 담을 수 있다는 뜻이다. ASCII 코드는 127까지만 올라갈 수 있었다. 그래서 어떤 머신들은 액센트 문자들에 128에서 255 사이의 값을 할당하였다. 그렇지만 머신이 다르면 코드도 다르다. 이 때문에 파일 교환에 문제가 생겼다. 마침내 다양하게 흔히 사용되는 범위가 128-255인 값의 집합이 출현하였다. 어떤 것들은 국제 표준 기구에서 정의된 진정한 표준이었고, 어떤 것들은 한 두 회사에서 고안되어 인기를 얻는데 성공한 사실상의 관례였다.

255개의 문자는 그렇게 많은 것이 아니다. 예를 들어, 서유럽에서 사용되는 액센트 문자들과 러시아어에 사용되는 키릴 알파벳 모두를 128-255 범위에 맞출 수는 없다. 그런 문자는 127개가 넘기 때문이다.

다른 코드를 사용하여 파일을 작성할 수 있다 (러시아 파일들은 모두 KOI8이라는 코딩 시스템으로, 프랑스어 파일은 모두 Latin1이라는 다른 코딩 시스템으로 작성할 수 있다). 그러나 러시아어 텍스트를 인용한 프랑스어 문서를 작성하고 싶다면 어떻게 할까? 1980년대 사람들은 이 문제를 풀고 싶어했으며, 유니코드 표준화 노력이 시작되었다.

유니코드는 8-비트 문자들 대신에 16-비트 문자들을 사용하여 시작되었다. 16 비트라는 것은 2^16 = 65,536개의 구별된 값을 사용할 수 있어서, 다양한 많은 알파벳으로부터 유래한 많은 문자들을 표현할 수 있다; 처음 목표는 유니코드에 인류 언어의 알파벳을 모두 담는 것이었다. 결과적으로 16 비트조차도 그 목표를 충족하기에 부족했고, 현대의 유니코드 규격에서는 더 넓은 코드, 0에서부터 1,114,111(16진수로 0x10ffff임)까지의 범위를 사용한다.

관련 ISO 표준은 ISO 10646이다. 유니코드와 ISO 10646은 원래 따로 시작되었지만, 유니코드 1.1 개정판에서 그 규격이 통합되었다.

(유니코드의 역사를 아주 간략하게 언급하였다. 보통의 파이썬 프로그래머라면 역사적 사실에 세세하게 신경쓸 필요가 없다고 생각한다; 참조 목록에 나열된 유니코드 콘솔티엄에 문의하면 정보를 더 많이 얻을 수 있다.)

정의

문자(character)란 텍스트의 가장 작은 구성요소이다. 'A', 'B', 'C', 등등은, 모두 다른 문자이다. 'E'와 'I'도 그렇다. 문자는 추상이다. 그리고 언급하고 있는 상황이나 언어에 따라 달라진다. 예를 들어 옴(Ω) 심볼은 보통 그리스 알파벳에서 대문자 오메가(Ω)와 아주 비슷하게 그려지지만 (심지어 어떤 폰트에서는 똑 같다), 이 두 문자는 서로 다른 의미를 지닌 다른 문자이다.

유니코드 표준은 문자들이 코드 포인트(code points)로 어떻게 표현되는지 기술한다. 코드 포인트는 정수 값이며, 보통 16진수로 표기 된다. 표준에서, 코드 포인트는 U+12ca 표기법으로 쓰여져 값이 0x12ca (10진수로 4810임)인 문자를 의미한다. 유니코드 표준에는 문자와 그에 상응하는 코드 포인트를 나열한 테이블이 많다:

0061    'a'; LATIN SMALL LETTER A
0062    'b'; LATIN SMALL LETTER B
0063    'c'; LATIN SMALL LETTER C
...
007B    '{'; LEFT CURLY BRACKET

엄밀히 말해, 이 정의는 '이것은 문자 U+12ca이다'라고 말하는 것이 의미가 없다는 것을 암시한다. U+12ca는 코드 포인트이다. 이는 특정한 문자를 대표한다. 이 경우에는 'ETHIOPIC SYLLABLE WI' 문자를 나타낸다. 비공식적인 상황에서는, 이렇게 코드 포인트와 문자를 구별하는 일을 종종 잊어 버릴 것이다.

문자는 획(glyph)이라고 불리우는 그래픽 요소의 집합으로 화면이나 종이에 나타낸다. 예를 들어, 대문자 A에 대한 획은 두 개의 사선과 한 개의 수평선으로 구성된다. 물론 정확한 세부사항은 사용되는 폰트에 따라 결정된다. 대부분의 파이썬 코드는 획에 관하여 걱정할 필요가 없다; 화면에 나타낼 올바른 획을 알아내는 일은 보통 GUI 도구함이나 터미널의 폰트 가공자가 할 일이다.

인코딩(Encodings)

앞 섹션을 요약하면: 유니코드 문자는 코드 포인트의 연속열이며, 코드 포인트는 0에서 0x10ffff까지의 숫자이다. 이 연속열은 메모리에서 바이트 집합(즉, 0-255의 값)으로 표현될 필요가 있다. 유니코드 문자열을 바이트 연속열로 번역하는 규칙을 이른바 인코딩(encoding)이라고 한다.

제일 먼저 떠 오를 인코딩은 32-비트 정수 배열일 것이다. 이 표현법에서, 문자열 "Python"은 다음과 같이 보일 것이다:

   P           y           t           h           o           n
0x50 00 00 00 79 00 00 00 74 00 00 00 68 00 00 00 6f 00 00 00 6e 00 00 00 
   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 

이 표현법을 UCS-4라고 부른다. 아주 직관적이지만 실제 사용에는 문제가 많다.

  1. 이식성이 없다; 프로세서마다 바이트 순서를 다르게 취급한다.
  2. 공간 낭비가 많다. 대부분의 텍스트에서, 코드 포인트의 대다수는 127 이하이거나, 255 이하이다. 그래서 많은 공간이 널(0)바이트로 채워진다. ASCII 표현법으로는 6 바이트가 필요함에 비하여 위의 문자열은 24 바이트를 차지한다. RAM을 더 사용하더라도 크게 문제가 되지는 않지만 (테스크탑 컴퓨터는 RAM이 메가 바이트가 넘고, 문자열들이 보통 그 정도로 크지는 않으므로), 디스크 공간과 네트워크 대역폭이 4배나 확대되는 것은 참기 힘들다.
  3. strlen() 같은 기존의 C 함수와 호환되지 않는다. 그래서 넓은 문자열 함수 가족을 새로 사용할 필요가 있다.
  4. 많은 인터넷 표준이 텍스트 데이터의 관점에서 정의되어 있고, 그래서 널(0)바이트가 임베드된 웹소는 처리하지 못한다.

일반적으로 사람들은 이 인코딩을 사용하지 않고, 보다 효율적이고 편리한 다른 인코딩을 골라 쓴다.

인코딩이 유니코드 문자를 모두 처리할 필요는 없다. 그리고 대부분의 인코딩이 모두 처리하지는 않는다. 예를 들어, 파이썬의 기본 인코딩은 'ascii'이다. 유니코드 문자열을 ASCII 인코딩으로 번역하는 규칙은 간단하다; 각 코드 포인트에 대하여:

  1. 코드 포인트가 128보다 작으면, 각 바이트는 코드 포인트의 값과 동일하다.
  2. 코드 포인트가 128 이상이면, 그 유니코드 문자열은 이 인코딩으로 표현이 불가능하다. (파이썬은 이 경우에 UnicodeEncodeError 예외를 일으킨다.)

ISO-8859-1이라고도 알려진 Latin-1도 비슷한 인코딩이다. 0-255까지의 유니코드 코드 포인트는 Latin-1 값과 동일하다. 그래서 이 인코딩으로 변환하려면 그냥 코드 포인트를 바이트 값으로 변환하기만 하면 된다; 255보다 큰 코드 포인트를 만나면, 그 문자열은 Latin-1로 인코드할 수 없다.

인코딩은 Latin-1처럼 간단한 1대1 짝짓기일 필요는 없다. IBM의 EBCDIC를 생각해 보자. 이는 IBM 메인프레임에 사용된 적이 있다. 글자 값들은 한 구획에 있지 않았다: 'a'에서 'i'까지는 129에서 137까지의 값을 가졌지만, 'j'에서 'r'까지는 145에서 153까지의 값을 가졌다. EBCDIC를 인코딩으로 사용하고 싶다면, 아마도 찾기표를 사용하여 변환을 수행하겠지만, 이는 대체로 내부적인 일이다.

가장 널리 사용되는 인코딩은 UTF-8이다. UTF는 "Unicode Transformation Format(유니코드 변환 포맷)"의 약자이며, '8'은 8-비트 숫자가 인코딩에 사용된다는 뜻이다. (UTF-16 인코딩도 있지만, UTF-8보다는 사용빈도가 낮다.) UTF-8은 다음과 같은 규칙을 사용한다:

  1. 코드 포인트가 128보다 작으면, 상응하는 바이트 값으로 표현된다.
  2. 코드 포인트가 128과 0x7ff 사이이면, 128과 255 사이의 두 바이트 값으로 변환된다.
  3. 0x7ff보다 큰 코드 포인트는 셋- 또는 네-바이트 연속열로 변환되는데, 여기에서 각 바이트는 128과 255 사이이다.

UTF-8은 여러 편리한 특성이 있다:

  1. 유니코드 코드 포인트를 모두 다룰 수 있다.
  2. 유니코드 문자열은 널(0)바이트가 임베드되지 않은 바이트 문자열로 변환된다. 이러면 바이트-순서 문제를 피할 수 있으며, 이는 UTF-8 문자열이 strcpy() 같은 C 함수로 처리가 가능하며 널(0)바이트를 처리하지 못하는 프로토콜을 통하여 전송될 수 있다는 뜻이다.
  3. ASCII 텍스트로 구성된 문자열도 유효한 UTF-8 텍스트이다.
  4. UTF-8은 아주 빽빽하다; 코드 포인트의 대다수는 2 바이트로 변환되며, 128보다 작은 값들은 겨우 1 바이트를 차지할 뿐이다.
  5. 바이트가 부패하거나 소실되면, 다음 UTF-8-인코드 코드 포인트가 시작되는 곳을 찾아서 재동기화가 가능하다. 또한 임의의 무작위 8-비트 데이터가 유효한 UTF-8처럼 보이는 것도 불가능하다.

참조

유니코드 콘솔티엄 사이트 <http://www.unicode.org/>에 가면 문자 차트, 어휘집, 그리고 PDF 버전의 유니코드 명세서가 있다. 좀 어려운 것을 읽기 위해 준비하자. <http://www.unicode.org/history/>는 유니코드의 기원과 개발에 관한 연대기이다.

표준을 이해하는데 도움을 주기 위해, 주카 코펠라(Jukka Korpela)가 유니코드 문자 테이블을 읽는 방법을 소개하였다. <http://www.cs.tut.fi/~jkorpela/unicode/guide.html>에서 참조하자.

로만 찌보라(Roman Czyborra)도 유니코드의 기본 원리에 대한 설명서를 썼다; <http://czyborra.com/unicode/characters.html>에 있다. 찌보라(Czyborra)는 수 많은 유니코드-관련 문서를 썼는데 <http://www.cyzborra.com/>에서 볼 수 있다.

다른 두 가지 좋은 소개 글이 있다. 조엘 스폴스키(Joel Spolsky)가 쓴 <http://www.joelonsoftware.com/articles/Unicode.html> 그리고 제이슨 오렌도르프(Jason Orendorff)가 집필한 <http://www.jorendorff.com/articles/unicode/>이다. 이 소개로도 잘 이해가 가지 않는다면, 반드시 이런 글들 중의 하나를 읽어보고 앞으로 나아가자.

위키피디어(Wikipedia) 항목도 종종 도움이 된다; 예를 들어, "문자 인코딩" <http://en.wikipedia.org/wiki/Character_encoding> 그리고 UTF-8 <http://en.wikipedia.org/wiki/UTF-8>을 찾아 보자.

파이썬의 유니코드 지원

이제 유니코드의 기본을 배웠으므로, 파이썬의 유니코드 특징을 살펴보자.

유니코드 유형

유니코드 문자열은 파이썬의 내장 유형중의 하나인 unicode 유형의 실체로 표현된다. 유니코드 유형은 basestring이라고 부르는 추상 유형으로부터 파생되는데, 이 유형은 또한 str 유형의 조상이기도 하다; 그러므로 값이 문자열인지 isinstance(value, basestring)으로 알아볼 수 있다.

unicode() 구성자는 unicode(string[, encoding, errors])라는 메쏘드가 있다. 그의 인자는 모두 8-비트 문자열이어야 한다. 첫 인자는 지정된 인코딩을 사용하여 유니코드로 변환된다; encoding 인자를 빼먹으면, ASCII 인코딩이 변환에 사용된다. 그래서 127보다 큰 문자는 에러로 취급된다:

>>> unicode('abcdef')
u'abcdef'
>>> s = unicode('abcdef')
>>> type(s)
<type 'unicode'>
>>> unicode('abcdef' + chr(255))
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeDecodeError: 'ascii' codec can't decode byte 0xff in position 6: 
                    ordinal not in range(128)

errors 인자에는 입력 문자열이 인코딩 규칙에 따라 변환되지 못할 때의 응답을 지정한다. 이 인자에 적접한 값은 'strict' (UnicodeDecodeError 예외를 일으킴), 'replace' (U+FFFD를 더함, 'REPLACEMENT CHARACTER'), 또는 'ignore' (유니코드 결과로부터 그 문자를 그냥 둠). 다음 예제에서 그 차이를 볼 수 있다:

>>> unicode('\x80abc', errors='strict')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 0: 
                    ordinal not in range(128)
>>> unicode('\x80abc', errors='replace')
u'\ufffdabc'
>>> unicode('\x80abc', errors='ignore')
u'abc'

인코딩 이름을 담고 있는 인코딩이 문자열로 지정된다. 파이썬 2.4에는 대략 100가지 인코딩이 따라온다; 파이썬 라이브러리 참조서에서 리스트를 찾아보자 <http://docs.python.org/lib/standard-encodings.html>. 어떤 인코딩은 이름이 여러가지이다; 예를 들어, 'latin-1', 'iso_8859_1' 그리고 '8859'는 모두 같은 인코딩에 대한 동의어이다.

한-문자 유니코드 문자열도 내장된 unichr() 함수로 만들 수 있다. 이 함수는 정수를 취해 그에 상응하는 코드 포인트를 담고 있는 길이가 1인 유니코드 문자열을 돌려준다. 역으로는 내장 ord() 함수로 처리하는데 이 함수는 한-문자 유니코드 문자열을 취해 코드 포인트 값을 돌려준다:

>>> unichr(40960)
u'\ua000'
>>> ord(u'\ua000')
40960

unicode 유형의 실체는 검색과 포맷 같은 연산에 대하여 8-비트 문자열 유형과 같은 메쏘드가 많다:

>>> s = u'Was ever feather so lightly blown to and fro as this multitude?'
>>> s.count('e')
5
>>> s.find('feather')
9
>>> s.find('bird')
-1
>>> s.replace('feather', 'sand')
u'Was ever sand so lightly blown to and fro as this multitude?'
>>> s.upper()
u'WAS EVER FEATHER SO LIGHTLY BLOWN TO AND FRO AS THIS MULTITUDE?'

이런 메쏘드들에 대하여 인자가 유니코드 문자열이나 8-비트 문자열이 모두 가능함을 주목하자. 8-비트 문자열은 유니코드로 변환된 다음에 연산을 수행할 것이다; 파이썬의 기본 ASCII 인코딩이 사용될 것이므로, 127보다 큰 문자들은 예외를 일으킨다:

>>> s.find('Was\x9f')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeDecodeError: 'ascii' codec can't decode byte 0x9f in position 3: ordinal not in range(128)
>>> s.find(u'Was\x9f')
-1

그러므로 문자열 연산을 하는 파이썬 코드는 대부분 코드를 전혀 변경하지 않아도 유니코드 문자열과 작동한다. (입력과 출력 코드는 유니코드를 위해 갱신될 필요가 있다; 이에 관해서는 나중에 상술함.)

또다른 중요한 메쏘드는 .encode([encoding], [errors='strict'])이다. 이는 8-비트 문자열로 변환된 유니코드 문자열을 돌려준다. 요청된 인코딩으로 인코드된다. errors 매개변수는 unicode() 구성자의 매개변수와 똑같다. 한가지를 덧 붙이자면; 'strict', 'ignore', 그리고 'replace' 말고도, 또 'xmlcharrefreplace'를 건넬 수 있는데 이는 XML의 문자 참조를 사용한다. 다음 예제는 다른 결과를 보여준다:

>>> u = unichr(40960) + u'abcd' + unichr(1972)
>>> u.encode('utf-8')
'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeEncodeError: 'ascii' codec can't encode character '\ua000' in position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
'abcd'
>>> u.encode('ascii', 'replace')
'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
'&#40960;abcd&#1972;'

파이썬의 8-비트 문자열은 .decode([encoding], [errors]) 메쏘드가 있다. 이 메쏘드는 주어진 인코딩을 사용하여 문자열을 번역한다:

>>> u = unichr(40960) + u'abcd' + unichr(1972)   # 문자열을 조립한다
>>> utf8_version = u.encode('utf-8')             # UTF-8로 인코드한다
>>> type(utf8_version), utf8_version
(<type 'str'>, '\xea\x80\x80abcd\xde\xb4')
>>> u2 = utf8_version.decode('utf-8')            # UTF-8로 디코드한다
>>> u == u2                                      # 두 문자열이 일치한다
True

인코딩에 접근하고 등록하는데 사용되는 저-수준 루틴들은 codecs 모듈에 있다. 그렇지만, 이 모듈에서 돌려주는 인코딩 함수와 디코딩 함수는 보통 편안하지 않고 좀 낮은 수준이다. 그래서 여기에서 codecs 모듈을 기술하지는 않겠다. 완전히 새로운 인코딩을 구현할 필요가 있다면, codecs 모듈 인터페이스에 관하여 더 연구할 필요가 있을 것이다. 그러나 인코딩을 구현하는 일은 특별한 과업이므로 역시 여기에서 다루지 않는다. 파이썬 문자를 뒤져서 이 모듈에 관하여 더 배우시기를 바란다.

codecs 모듈에서 가장 흔히 사용되는 부분은 codecs.open() 함수로서 입력과 출력에 관한 섹션에서 다루겠다.

파이썬 소스 코드의 기호상수(Literals)

파이썬 소스 코드에서, 유니코드 기호상수들은 앞에 'u' 또는 'U' 문자를 붙인 문자열로 씌여진다: u'abcdefghijk'. 구체적인 코드 포인트는 \u 피신 연속열로 표기가 가능하다. 다음에 16진수 네자리가 따라와서 코드 포인트를 지정한다. \U 피신 연속열도 비슷하지만, 4 자리가 아니라 16진수 8 자리가 예상된다.

유니코드 기호상수도 8-비트 문자열과 똑 같은 피신 연속열을 사용할 수 있다. 여기에는 \x가 포함된다. 그러나 \x는 오직 두 개의 16진수만을 취하며 그래서 아무 코드 포인트나 표현할 수는 없다. 8진수 피신은 U+01ff까지 올라갈 수 있으며, 이 수치는 8진수로 777이다.

>>> s = u"a\xac\u1234\u20ac\U00008000"
           ^^^^ two-digit hex escape
               ^^^^^^ four-digit Unicode escape 
                           ^^^^^^^^^^ eight-digit Unicode escape
>>> for c in s:  print ord(c),
... 
97 172 4660 8364 32768

127보다 큰 코드 포인트에 대하여 피신 연속열을 사용하는 것은 작은 분량에는 문제가 없지만, 액센트 붙은 문자를 많이 사용할 수록 점점 곤란해진다. 프랑스어 메시지나 기타 액센트-사용 언어로된 메시지를 가진 프로그램에서 그렇듯이 말이다.unichr() 내장 함수를 사용하여 문자열을 조립할 수도 있지만, 이것은 훨씬 더 지겹다.

이상적으로, 모국 언어의 인코딩으로 기호상수를 작성할 수 있으면 좋을 것이다. 그러면 즐겨쓰는 편집기로 파이썬 소스 코드를 편집할 수 있을 것이다. 액센트 문자들을 자연스럽게 보여줄 것이며, 실행시간에 올바른 문자를 사용할 수 있을 것이다.

파이썬은 유니코드 기호상수를 아무 인코딩으로도 작성하도록 지원하지만, 사용될 인코딩을 선언해야 한다. 인코딩 선언은 소스파일의 첫줄 또는 두 번재 줄에 특별한 주석을 포함시키면 된다:

#!/usr/bin/env python
# -*- coding: latin-1 -*-

u = u'abcde'
print ord(u[-1])

이 구문은 한 파일에 지역 변수를 지정하는 이맥스의 표기법에서 영향을 받았다. 이맥스는 다양한 많은 변수를 지원하지만, 파이썬은 오직 'coding'만을 지원한다. 심볼 -*-은 그 주석이 특별하다는 뜻을 나타낸다; 이 심볼 안에서, coding이라는 이름과 선택한 인코딩 이름을 지정해야 한다. ':'로 갈라서 말이다.

그런 주석이 포함되지 않으면, 기본 인코딩으로 ASCII가 사용된다. 2.4 이전의 파이썬 버전은 유럽-중심이어서 Latin-1을 문자열 기호상수의 기본 인코딩으로 간주했지만; 파이썬 2.4에서는 127보다 큰 문자도 여전히 작동하지만 결과적으로 경고를 산출한다. 예를 들어, 다음 프로그램은 인코딩 선언이 되어 있지 않다:

#!/usr/bin/env python
u = u'abcde'
print ord(u[-1])

파이썬 2.4에서 실행하면, 다음과 같은 경고가 출력된다:

amk:~$ python p263.py
sys:1: DeprecationWarning: Non-ASCII character '\xe9' 
     in file p263.py on line 2, but no encoding declared; 
     see http://www.python.org/peps/pep-0263.html for details

유니코드의 특성

유니코드 규격에는 코드 포인트에 관한 정보 데이터베이스가 포함되어 있다. 정의된 각 코드 포인트에 대하여, 그 정보에는 문자의 이름, 범주, 적용가능한 숫치 값이 포함된다 (유니코드는 로마 숫자 그리고 3분의 1 그리고 5분의 4 같이 분수를 표현하는 문자가 있다). 양방향 텍스트에 사용되는 코드 포인트에 관한 특성도 있고 기타 화면표시-관련 특성도 있다.

다음 프로그램은 여러 문자들에 대한 정보를 보여준다. 그리고 특정한 문자 하나의 숫치 값을 인쇄한다:

import unicodedata

u = unichr(233) + unichr(0x0bf2) + unichr(3972) + unichr(6000) + unichr(13231)

for i, c in enumerate(u):
    print i, '%04x' % ord(c), unicodedata.category(c),
    print unicodedata.name(c)

# 두 번째 문자의 숫자 값을 얻는다
print unicodedata.numeric(u[1])

실행하면, 다음과 같이 인쇄된다:

0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 No TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lo TAGBANWA LETTER SA
4 33af So SQUARE RAD OVER S SQUARED
1000.0

범주 코드는 문자의 본성을 기술하는 약자이다. "Letter", "Number", "Punctuation", 또는 "Symbol"의 범주로 그룹지어지며, 이 번에는 하부범주로 쪼개진다. 위의 출력으로부터 코드를 취해 보면, 'Ll'는 'Letter, lowercase'를 뜻하고, 'No'는 "Number, other"를 뜻하며, 'Mn'은 "Mark, nonspacing"이고, 'So'는 "Symbol, other"이다. 범주 코드 목록은 <http://www.unicode.org/Public/UNIDATA/UCD.html#General_Category_Values>를 참고하자.

참조

유니코드와 8-비트 문자열 유형은 파이썬 라이브러리 참조서 <http://docs.python.org/lib/typesseq.html>에 기술되어 있다.

unicodedata 모듈에 대한 문서는 <http://docs.python.org/lib/module-unicodedata.html>에 있다.

codecs 모듈에 대한 문서는 <http://docs.python.org/lib/module-codecs.html>에 있다.

마크-안드레 렘버그(Marc-Andre Lemburg)는 "파이썬과 유니코드"라는 제목으로 EuroPython 2002에 발표를 했다. PDF 버전의 슬라이드는 <http://www.egenix.com/files/python/Unicode-EPC2002-Talk.pdf>에 있으며, 파이썬의 유니코드 특징의 디자인을 탁월하게 개관한 것이다.

유니코드 데이터 읽기와 쓰기

유니코드 데이터와 작동하는 코드를 일단 작성했으면, 다음 문제는 입력/출력이다. 어떻게 유니코드 문자열을 프로그램에 집어 넣을까, 그리고 어떻게 하면 유니코드를 저장이나 전송에 알맞는 형태로 변환할 것인가?

입력 소스와 출력 목적지에 따라 아무것도 할 필요가 없을 수도 있다; 어플리케이션에 사용되는 라이브러리에서 유니코드를 지원하는가를 알아 보아야 한다. 예를 들어, XML 해석기는 유니코드 데이터를 돌려주는 경우가 많다. 많은 관계형 데이터베이스도 유니코드-값의 컬럼을 지원하고 SQL 질의에 유니코드 값을 돌려준다.

유니코드 데이터는 보통 특정한 인코딩으로 변환되고 나서 디스크에 씌여지거나 소켓을 통하여 전송된다. 그 모든 일을 직접하는 것이 가능하다: 파일을 연다. 그 파일로부터 8-비트 문자열을 읽는다. unicode(str, encoding) 함수로 그 문자열을 변환한다. 그렇지만, 수동 접근은 추천하지 않는다.

한가지 문제는 인코딩의 여러-바이트 본성때문이다; 유니코드 문자 하나는 여러 바이트로 표현이 가능하다. 파일을 길이에 관계없이 마음대로 읽고 싶다면 (예를 들어, 1K 또는 4K), 에러-처리 코드를 작성할 필요가 있다. 어떤 경우는 조각의 끝에서 유니코드 문자 하나를 인코딩한 바이트 연속열의 일부만 읽는 경우가 있기 때문이다. 한가지 해결책은 전체 파일을 메모리 안으로 읽은 다음 디코딩을 수행하는 것이지만, 그렇게 하면 큰 파일과 작업하지 못한다; 2Gb 파일을 읽어야 할 필요가 있다면, 2Gb의 램이 필요하다. (게다가, 실제로는 적어도 한 동안 인코드된 문자열과 그의 유니코드 버전을 메모리에 함께 유지할 필요가 있기 때문이다.)

해결책은 저-수준의 디코딩 인터페이스를 사용하여 일부만 코딩된 연속열의 경우를 잡아내는 것이다. 이런 구현 작업이 이미 여러분을 대신하여 완성되어 있다: codecs 모듈에는 open() 함수가 있어서 파일-류의 객체를 돌려준다. 이 파일-류의 객체는 파일의 내용이 지정된 인코딩으로 되어 있다고 간주하며 유니코드 매개변수를 .read().write() 같은 메쏘드에 받아 들인다.

그 함수의 매개변수는 open(filename, mode='rb', encoding=None, errors='strict', buffering=1)이다. mode'r', 'w', or 'a'가 될 수 있다. 보통의 내장 open() 함수에 똑 같이 상응한다; '+'를 추가하면 파일을 갱신한다. buffering은 표준 함수의 매개변수에 비슷하게 상응한다. encoding은 사용할 인코딩을 지정하는 문자열이다; None으로 남겨지면, 8-비트 문자열을 받는 표준 파이썬 파일 객체가 반환된다. 그렇지 않으면, 포장자 객체가 반환되며 그 포장자 객체에 쓰여지거나 읽어들인 데이터는 필요에 맞게 변환된다. errors는 인코딩 에러에 대한 조치를 지정하며 일반적인 'strict', 'ignore', 그리고 'replace'라는 값 중의 하나가 될 수 있다.

그러므로 파일에서 유니코드를 읽어 들이는 일은 간단하다:

import codecs
f = codecs.open('unicode.rst', encoding='utf-8')
for line in f:
    print repr(line)

파일을 갱신 모드에서 여는 것도 가능하며, 읽기와 쓰기를 모두 허용한다:

f = codecs.open('test', encoding='utf-8', mode='w+')
f.write(u'\u4500 blah blah blah\n')
f.seek(0)
print repr(f.readline()[:1])
f.close()

유니코드 문자 U+FEFF는 바이트-순서 표식(BOM(byte-order mark))으로 사용되며, 종종 파일에 첫 문자로 씌여져서 그 파일의 바이트 순서를 자동으로 탐지하는 것을 돕는다. UTF-16 같이, 어떤 인코딩은 BOM이 파일의 시작에 존재한다고 예상한다; 그런 인코딩이 사용되면, BOM은 자동으로 첫 문자로 씌여질 것이고 그 파일을 읽을 때는 조용히 빼버릴 것이다. 이런 인코딩의 변종이 다양하다. 예를 들어 'utf-16-le'와 'utf-16-be'는 각각 작은값-종료형(little-endian)과 큰값-종료형(big-endian) 인코딩인데, 순서를 나타내는 특정한 바이트 하나를 지정하며 그 BOM을 빼먹지 않는다.

유니코드 파일이름

오늘날 사용되는 대부분의 운영체제는 유니코드 문자가 담긴 파일이름을 지원한다. 보통 이는 유니코드 문자열을 시스템에 따라 달라지는 어떤 인코딩으로 변환함으로써 구현된다. 예를 들어, MacOS X는 UTF-8을 사용하는 반면 윈도우즈는 MBCS라고 부르는 마이크로소프트 전용 인코딩을 사용한다. 유닉스 시스템에서, 파일 시스템 인코딩은 LANG 또는 LC_CTYPE 환경변수를 설정하여 지정된다; 그렇지 않으면, 기본 인코딩은 ASCII이다.

인코딩을 수작업으로 하고 싶은 경우에, sys.getfilesystemencoding() 함수는 현재 시스템에 사용할 인코딩을 돌려준다. 그러나 그렇게 수고할 이유가 없다. 읽거나 쓰기 위해 파일을 열면, 보통 그냥 유니코드 문자열을 파일이름으로 제공하면 되고, 그러면 자동으로 올바른 인코딩으로 변환된다:

filename = u'filename\u4500abc'
f = open(filename, 'w')
f.write('blah\n')
f.close()

os 모듈 안의 함수들도 (예를 들어, os.stat()) 유니코드 파일이름을 받을 것이다.

파일이름을 돌려주는 os.listdir()이란 함수는 문제가 있다: 파일이름을 유니코드 버전으로 돌려줄 것인가, 아니면 인코드된 버전이 담긴 8-비트 문자열을 돌려줄 것인가? 디렉토리 경로를 8-비트 문자열로 제공했는지 아니면 유니코드 문자열로 제공했는지에 따라 둘 모두를 돌려줄 것이다. 유니코드 문자열을 경로로 건네면, 파일이름은 그 파일시스템의 인코딩을 사용하여 인코드될 것이고, 유니코드 문자열 리스트가 반환될 것이다. 반면에 8-비트 경로를 보내면 8-비트 버전의 파일이름을 돌려줄 것이다. 예를 들어, 기본 파일시스템 인코딩이 UTF-8이라고 간주하고, 다음 프로그램을 실행해 보자:

fn = u'filename\u4500abc'
f = open(fn, 'w')
f.close()

import os
print os.listdir('.')
print os.listdir(u'.')

다음과 같은 출력이 생산된다:

amk:~$ python t.py
['.svn', 'filename\xe4\x94\x80abc', ...]
[u'.svn', u'filename\u4500abc', ...]

첫 리스트에 UTF-8-인코드된 파일이름이 담기고, 두번째 리스트에는 유니코드 버전이 담긴다.

유니코드-인식 프로그램 작성을 위한 팁

이 섹션에서는 유니코드를 다루는 소프트웨어를 작성하는 것에 관하여 몇가지 제시한다.

가장 중요한 팁은 이것이다:

소프트웨어는 내부적으로 오직 유니코드 문자열과 작동해야만 하며, 출력시에만 특정한 인코딩으로 변환되어야 한다.

유니코드와 8-비트 문자열을 모두 받아들이는 처리 함수를 작성해 보면, 두 가지 다른 종류의 문자열을 결합하는 곳마다 버그가 발생할 가능성이 높다는 것을 알게 될 것이다. 파이썬의 기본 인코딩은 ASCII이며, 그래서 ASCII 값이 >127이 넘는 문자가 입력될 때마다, UnicodeDecodeError를 맞이하게 될 터인데 그 문자는 ASCII 인코딩으로 처리하지 못하기 때문이다.

액센트 문자가 없는 데이터로 소프트웨어를 테스트할 경우에는 그런 문제를 놓치기 쉽다; 아무 문제가 없는 듯 보이지만, 실제로는 프로그램에 버그가 있어서 >127의 문자를 사용하려고 시도하는 사용자를 기다린다. 그러므로, 두번째 팁은 다음과 같다:

테스트 데이터에 >127의 문자를 포함시키고, 더 좋게는 >255 문자를 포함시켜라.

웹 브라우저나 기타 신뢰할 수 없는 소스로부터 들어 온 데이터를 사용할 때, 일반적인 테크닉은 문자열에서 불법적인 문자들을 먼저 점검하고 나서 그 문자열을 생성된 명령어 줄에서 사용하거나 데이터베이스에 저장한다. 이렇게 하고 있다면, 일단 사용되거나 저장될 형태로 변했다고 할지라도 주의해서 그 문자열을 한 번 더 점검하자; 문자들을 가장하기 위해 인코딩이 사용될 가능성이 있다. 이는 특히 입력 데이터에서 인코딩을 지정할 경우에 해당된다; 많은 인코딩은 일반적으로 점검된 문자들은 그대로 두지만, 파이썬에는 문자를 모조리 바꿀 수 있는 'base64'와 같은 인코딩이 몇가지 포함되어 있다.

예를 들어, 유니코드 파일이름을 취하는 웹소 관리 시스템이 있다고 해보자. '/' 문자를 가진 경로는 허용하고 싶지 않을 것이다. 다음과 같이 코드를 작성할지 모르겠다:

def read_file (filename, encoding):
    if '/' in filename:
        raise ValueError("'/' not allowed in filenames")
    unicode_name = filename.decode(encoding)
    f = open(unicode_name, 'r')
    # ... 파일의 내용을 돌려준다 ...

그렇지만, 공격자가 'base64' 인코딩을 지정할 수 있다면, 'L2V0Yy9wYXNzd2Q='를 건네어 시스템 파일을 읽을 수 있을 것이다. 이 문자열은 base-64 인코드된 형태의 문자열 '/etc/passwd'이다. 위의 코드는 인코드된 형태에서 '/' 문자들을 찾아 그 위험한 문자를 빼버리고 결과 형태로 디코드한다.

참조

마크-안드레 렘버그(Marc-Andre Lemburg)의 발표 "파이썬으로 유니코드-인식 어플리케이션 작성하는 법"에 대한 PDF 슬이드는 <http://www.egenix.com/files/python/LSM2005-Developing-Unicode-aware-applications-in-Python.pdf>에서 얻을 수 있으며 문자 인코딩의 문제를 연구할 뿐만 아니라 어플리케이션의 지역화와 국제화 방법에 관해서도 연구한다.

개정 이력과 감사의 말

잘못을 지적해 주고 이 글에 대하여 제안을 해준 다음 분들에서 감사의 말을 전한다: Marius Gedminas, Kent Johnson, Marc-Andre Lemburg, Martin von Lowis.

Version 1.0: posted August 5 2005.

Version 1.01: posted August 7 2005. Corrects factual and markup errors; adds several links.