목차
시작 하기
이 문서를 쓰기 시작한 것은 이주 전이었다. 그 때 Python과 Unicode에 대하여 내가 아는 사용법은 다음과 같았다:
파이썬에서 유니코드를 사용하려면 unicode()에 문자열을 건네기만 하면 된다
도대체 이런 이상한 지식은 어디에서 얻었는가? 물론, 그렇다. 유니코드에 관하여 파이썬 자습서에서 이렇게 말하고 있다:
"파이썬에서 유니코드 문자열을 만들려면 그냥 간단하게 보통 문자열을 만들듯이 하면 된다":
>>> u'Hello World !' u'Hello World !'
>>> u'Hello World !' u'Hello World !'
이 예제는 기술적으로는 올바르지만, 유니코드 초보자를 그릇된 길로 이끌 수 있다. 왜냐하면 진정한 사용법에 필요한 여러 상세한 정보를 두루뭉실하게 얼버무리기 때문이다. 이렇게 과도하게-간략화된 설명 때문에 본인은 파이썬에서 유니코드가 작동하는 방식을 완전히 잘못 이해했었다.
여러분 역시 과도하게-간략화된 경로를 따라 내려왔다면, 이 자습서가 모쪼록 여러분을 구해줄 수 있기를 바란다. 이 자습서에는 예제와 테스트 그리고 데모가 한 세트로 포함되어 있으며 본인이 파이썬의 유니코드와 올바르게 작업하는 방법을 "다시 익히는" 과정을 문서화했다. 크로스-플랫폼 문제뿐만 아니라, HTML과 XML 그리고 파일시스템을 다룰 때 일어나는 문제들을 다룬다.
그런데, 유니코드는 상당히 단순하다. 처음부터 제대로 배웠으면 좋았을텐데 아쉽다.
어디에서부터 시작할까?
최상위 수준에서, 컴퓨터는 세 가지 유형의 텍스트 표현을 사용한다:
- ASCII
- 멀티바이트 문자 세트
- 유니코드
ASCII에서 멀티바이트로
처음에, ASCII가 있었다. (물론, EBCDIC도 있었지만, 메인프레임 바깥으로 벗어나지 않았다. 그래서 여기에서는 생략하겠다.) ASCII 문자 세트에는 256 개의 문자가 포함되어 있는데, 다음 ASCII 차트에서 보시는 바와 같다. 256개의 문자가 사용가능하지만, 하위 128 (0-127 코드)개는 자주 사용되는 코드이다. 사실 초기 메일 시스템은 오직 0-127 (즉 "7-비트 텍스트")개만 전송을 허용하며 사실 오늘날도 여전히 많은 시스템에 적용된다. 표에서 보시다시피, ASCII는 영어권 문서에는 충분하다.
ASCII만으로는 불충분한 여러 나라에서 컴퓨터의 사용이 증가하자 문제가 일어났다. ASCII는 그리스어나 키릴어 또는 일어 텍스트 등등을 다룰 능력이 없다. 게다가, 일어 텍스트 하나 만으로도 수천개의 문자가 필요하다. 그래서 8-비트 체계 안에 맞출 방법이 없다. 이를 극복하기 위하여, 멀티바이트 문자 세트가 창안되었다. 대부분의 (전부가 아니라면?) 멀티바이트 문자 세트는 ASCII 세트에서 주로 앞쪽 128개의 문자만 주로 사용된다는 사실을 이용한다 (십진수로는 0-127, 또는 16진수로는 0x00-0x7f). 상위 코드(십진수로는 128..255, 또는 십육진수로 0x80-0xff)는 비-영어 확장 세트를 정의하는데 사용된다.
예제를 한 번 보자: Shift-JIS는 일본어 텍스트를 위한 인코딩이다. 그의 문자 테이블은 여기에서 보실 수 있다. 각 문자의 첫 바이트는 0x80 - 0xfc의 16진 값으로 시작함을 주목하자. 이는 흥미로운 특성인데, 왜냐하면 그 의미가 영어와 일어 텍스트가 자유롭게 혼합될 수 있다는 뜻이기 때문이다! 문자열 "Hello World!"는 영어 텍스트로서 완벽하게 유효한 Shift-JIS 인코딩이다. Shift-JIS를 해석할 때 0x80-0xff 범위의 바이트를 맞이하면, 두 코드 연속열 중에서 첫 문자라는 것을 알 수 있다. 그렇지 않으면, 보통 ASCII인 1바이트이다.
이는 오직 일본어로만 작업한다면 똑 같이 잘 작동한다. 그러나 그리스 문자 세트로로 전환하면 어떤 일이 일어날까? 표를 보시면 아시겠지만, ISO-8859-7는 0x80-0xff 범위의 코드를 Shift-JIS와 완전히 다르게 재정의한다. 그래서, 영어와 일어는 혼합해 쓸 수 있지만, 그리스어와 일어는 서로 밟아댄다. 그러므로 섞어 쓸 수 없다. 이런 문제는 멀티바이트 문자 세트를 섞어쓸 때 흔히 일어난다.
멀티바이트에서 유니코드로
다양한 언어를 섞어 쓸 때 일어나는 문제를 극복하기 위해, 유니코드는 세상의 문자 세트를 모두 하나의 거대한 테이블 안으로 넣자고 제안한다. 다음의 유니코드 문자 세트를 한 번 살펴보자.
언뜻 보면, 각 언어에 대하여 따로 표가 있는 것처럼 보인다. 그래서 ASCII에 비해 별로 나아 보이지 않을지도 모르겠다. 실제로는 이것들은 모두 같은 테이블에 있으며, 단지 (인간이) 쉽게 참조하기 위하여 여기에 인덱스되어 있을 뿐이다. 주목할 핵심 열쇠는 이 모든 것들이 같은 테이블에 있으므로, ASCII/멀티바이트 세계처럼 중첩되지 않는다는 것이다. 이 덕분에 유니코드 문서는 코딩 충돌없이 자유롭게 언어를 섞어 쓸 수 있다.
유니코드 전문용어
그리스어 차트를 보고 몇 개의 문자를 가져와 보자:
| 샘플 유니코드 심볼 | ||
|---|---|---|
| 03A0 | Π | Greek Capital Letter Pi |
| 03A3 | Σ | Greek Capital Letter Sigma |
| 03A9 | Ω | Greek Capital Letter Omega |
흔히 이 심볼들은 U+NNNN와 같은 표기법을 사용하여 참조한다. 예를 들어 U+03A0와 같이 말이다. 그래서 다음의 표기법을 사용하여 이 문자들을 담고 있는 문자열을 정의할 수 있다 (명료하게 하기 위해 괄호를 덧붙였다):
uni = {U+03A0} + {U+03A3} + {U+03A9}
이제, 정확하게 'uni'가 무엇을 뜻하는지 알지만 (ΠΣΩ), 다음과 같이 할 방법이 없다:
- 화면에 uni를 인쇄하는 방법.
- 파일에 uni를 저장하는 방법.
- 또다른 텍스트에 uni를 추가하는 방법.
- uni를 저장하는데 얼마나 많은 바이트가 드는지 아는 방법.
왜 유니코드 세계에서는 바이트에 관하여 잊어 버려야 하는가? 그리스 심볼인 오메가를 예로 들어 보자: Ω. 이를 이진코드로 인코드하는데 적어도 4가지 방식이 있다:
| 인코딩 이름 | 이진 표현 |
|---|---|
| ISO-8859-7 | \xD9 "Native" Greek encoding |
| UTF-8 | \xCE\xA9 |
| UTF-16 | \xFF\xFE\xA9\x03 |
| UTF-32 | \xFF\xFE\x00\x00\xA9\x03\x00\x00 |
파이썬의 유니코드 텍스트
이상적인 유니코드 문자열 uni (ΠΣΩ)를 유용한 형태로 변환하려면, 몇 가지를 살펴볼 필요가 있다:
- 유니코드 기호상수를 표현하기
- 유니코드를 이진코드로 변환하기
- 이진코드를 유니코드로 변환하기
- 문자열 연산을 사용하기
유니코드 심볼을 파이썬 기호상수로 변환하기
유니코드 문자열을 심볼로부터 만드는 일은 아주 쉽다. 위의 그리스 심볼을 되살려보자:
| 샘플 유니코드 심볼 | ||
|---|---|---|
| 03A0 | Π | Greek Capital Letter Pi |
| 03A3 | Σ | Greek Capital Letter Sigma |
| 03A9 | Ω | Greek Capital Letter Omega |
위의 문자들과 약간의 ASCII 구형 문자로 유니코드 문자열을 만들고 싶다고 해보자.
- 의사코드:
- uni = 'abc_' + {U+03A0} + {U+03A3} + {U+03A9} + '.txt'
- 다음은 파이썬으로 그 문자열을 만드는 법이다:
- uni = u"abc_\u03a0\u03a3\u03a9.txt"
몇가지 주목할 것들:
- 평범한-ASCII 문자는 그 자체로 쓸 수 있다. 그냥 "a"라고 말하면 되고, 유니코드 심볼 "\u0061"을 사용할 필요가 없다. (그러나 기억하자. "a" 는 실제로는 {U+0061}이다; 유니코드 심볼 같은 것은 없다 "a".)
- \u 피신 연속열은 유니코드를 나타내는데 사용된다.
- 이는 이진 값들을 삽입하기 위한 약간 전통적인 C-스타일의 \xNN과 비슷하다. 그렇지만, 유니코드 테이블을 보면 6 자리까지 값을 보여준다. 이것들은 \xNN으로 편리하게 나타낼 수 없다. 그래서 \u가 고안되었다.
-
4자리 이하의 유니코드 값에는 4-자리 버전을 사용하자:
\uNNNN
4 자리를 모두, 필요하면 앞에 0을 사용하여 채워야 함에 주의하자. - 4비트를 넘어가는 유니코드 값에는 8-자리 버전을 사용하자:
\UNNNNNNNN
8 자리를 모두, 필요하면 앞에 0을 사용하여 채워야 함에 주의하자.
- 의사코드:
- uni = {U+1A} + {U+B3C} + {U+1451} + {U+1D10C}
- 파이썬:
- uni = u'\u001a\u0bc3\u1451\U0001d10c'
- 파이썬:
- uni = u'\u001A\u0BC3\u1451\U0001D10C'
왜 "print"는 작동하지 않는가?
uni는 고정된 컴퓨터 표현이 없다고 앞서 본인이 말했던 바를 기억하자. 그래서 uni를 인쇄하려고 하면 어떤 일이 일어나는가?
uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni
print uni
다음과 같은 메시지를 보게될 것이다:
Traceback (most recent call last):
File "t6.py", line 2, in ?
print uni
UnicodeEncodeError: 'ascii' codec can't encode characters in position 1-4:
ordinal not in range(128)
File "t6.py", line 2, in ?
print uni
UnicodeEncodeError: 'ascii' codec can't encode characters in position 1-4:
ordinal not in range(128)
무슨 일이 일어났는가? 자, 파이썬에게 uni를 인쇄하라고 명령했지만, uni는 고정된 컴퓨터 표현이 없으므로, 파이썬은 먼저 uni를 인쇄가능한 형태로 변환해야 한다. 파이썬에게 변환방법을 가르쳐 주지 않았기 때문에, 파이썬은 여러분이 ASCII를 원한다고 간주한다. 불행하게도, ASCII는 0에서 127 까지의 값만을 다룰 수 있으며, uni에는 범위를 벗어난 값들이 들어 있으므로, 에러를 보게되는 것이다.
uni를 인쇄하는 빠른 방법은 파이썬의 repr() 메쏘드를 사용하는 것이다:
uni = u"\u001A\u0BC3\u1451\U0001D10C"
print repr(uni)
print repr(uni)
다음과 같이 인쇄된다:
u'\x1a\u0bc3\u1451\U0001d10c'
물론 일리는 있다. 정확하게 uni를 정의한 대로이니까 말이다. 그러나 repr(uni)는 실세계에서 uni 그 자체만큼이나 아무 쓸모가 없다. 정말 필요한 것은 코덱에 관하여 배우는 것이다.
코덱(Codecs)
- 코덱(Codecs)
- 일반적으로, 파이썬의 코덱은 임의의 객체-대-객체 변환을 허용한다. 그렇지만, 이 글의 문맥에서 코덱을 다음과 같이 생각하자. 유니코드 객체를 이진 파이썬 문자열 또는 그 반대로 변환해 주는 함수로 생각하면 충분하다.
- 왜 코덱이 필요한가?
- 유니코드 객체는 고정된 컴퓨터 표현이 없다. 유니코드를 인쇄하거나 디스크에 저장하거나 또는 네트워크를 건너서 보내려면 먼저 고정된 컴퓨터 표현으로 인코드되어야 한다. 이는 코덱(codec)을 사용하여 완수한다. 메일 매일의 경험에서 자주 들어 보았을 만한 코덱은 다음과 같다: ascii, iso-8859-7, UTF-8, UTF-16.
유니코드에서 이진 코드로
유니코드 값을 이진 표현으로 변환하려면, 코덱의 이름과 함께 .encode 메쏘드를 호출해야 한다. 예를 들어, 유니코드 값을 UTF-8로 변환하려면:
binary = uni.encode("utf-8")
uni를 좀 더 재미있게 만들어 보면 어떨까? 평범한 텍스트 문자를 추가해 보자:
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
이제 코덱에 따라 uni가 어떻게 표현되는지 한 번 보자. 다음은 작은 테스트 프로그램이다:
test_codec01.py
if __name__ == '__main__':
# define our Unicode string
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
# UTF-8 and UTF-16 can fully encode *any* Unicode string
print "UTF-8", repr(uni.encode('utf-8'))
print "UTF-16", repr(uni.encode('utf-16'))
# ASCII can only code values 0-127. Below, we tell Python
# to replace non-codable characters with '?'
print "ASCII",uni.encode('ascii','replace')
# ISO-8859-1 is similar to ASCII
print "ISO-8859-1",uni.encode('iso-8859-1','replace')
# define our Unicode string
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
# UTF-8 and UTF-16 can fully encode *any* Unicode string
print "UTF-8", repr(uni.encode('utf-8'))
print "UTF-16", repr(uni.encode('utf-16'))
# ASCII can only code values 0-127. Below, we tell Python
# to replace non-codable characters with '?'
print "ASCII",uni.encode('ascii','replace')
# ISO-8859-1 is similar to ASCII
print "ISO-8859-1",uni.encode('iso-8859-1','replace')
이의 출력 결과는 다음과 같다:
UTF-8 'Hello\x1a\xe0\xaf\x83\xe1\x91\x91\xf0\x9d\x84\x8cUnicode'
UTF-16 '\xff\xfeH\x00e\x00l\x00l\x00o\x00\x1a\x00\xc3\x0bQ\x144
\xd8\x0c\xddU\x00n\x00i\x00c\x00o\x00d\x00e\x00'
ASCII Hello????Unicode
ISO-8859-1 Hello????Unicode
UTF-16 '\xff\xfeH\x00e\x00l\x00l\x00o\x00\x1a\x00\xc3\x0bQ\x144
\xd8\x0c\xddU\x00n\x00i\x00c\x00o\x00d\x00e\x00'
ASCII Hello????Unicode
ISO-8859-1 Hello????Unicode
여전히 repr()을 사용하여 UTF-8 문자열과 UTF-16 문자열을 인쇄하고 있음을 주목하자. 왜인가? 음, 그렇지 않았으면, 화면에 날 이진 값들을 인쇄했을 것이고, 이 문서에 집어 넣기가 어려웠을 것이다.
이진코드에서 유니코드로
누군가 여러분에게 UTF-8 인코드된 버전으로 유니코드 객체를 주었다고 해보자. 어떻게 그것을 다시 유니코드로 변환할 것인가? 아마도 순진하게 다음과 같이 시도해 볼 것이다:
순진한 (그른) 방법
uni = unicode( utf8_string )
왜 이것이 잘못인가? 다음은 정확하게 같은 일을 하는 샘플 프로그램이다:
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
utf8_string = uni.encode('utf-8')
# naively convert back to Unicode
uni = unicode(utf8_string)
utf8_string = uni.encode('utf-8')
# naively convert back to Unicode
uni = unicode(utf8_string)
여기에서 무슨 일이 일어나는가:
Traceback (most recent call last):
File "t6.py", line 5, in ?
uni = unicode(utf8_string)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe0
in position 6: ordinal not in range(128)
File "t6.py", line 5, in ?
uni = unicode(utf8_string)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe0
in position 6: ordinal not in range(128)
보시다시피, unicode() 함수는 실제로 매개변수를 두 개 취한다:
def unicode(string, encoding):
....
....
위의 예제에서 인코딩이 없으므로 파이썬은 신뢰성 있게 또다시 ASCII를 원한다고 간주하였다 (각주 1). 그리고 잘못된 것을 돌려주었다.
다음은 올바른 방법이다:
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
utf8_string = uni.encode('utf-8')
# have to decode with the same codec the encoder used!
uni = unicode(utf8_string,'utf-8')
print "Back from UTF-8: ",repr(uni)
utf8_string = uni.encode('utf-8')
# have to decode with the same codec the encoder used!
uni = unicode(utf8_string,'utf-8')
print "Back from UTF-8: ",repr(uni)
다음과 같이 출력된다:
Back from UTF-8: u'Hello\x1a\u0bc3\u1451\U0001d10cUnicode'
문자열 연산
위의 예제가 아무쪼록 왜 우리가 가능하면 유니코드 값을 이진 문자열로 취급하지 않으려고 하는지 그 이유를 알려주었기를 바란다! UTF-8 버전은 길이가 23 바이트였고, UTF-16 버전은 36 바이트였으며, ASCII 버전은 겨우 16 바이트였다 (그러나 4 유니코드 값을 완전히 버렸다) 그리고 ISO-8859-1와 비슷하다.
이 때문에 이 문서의 머리에서 바이트에 관하여 모든 것을 잊어 버리라고 제안한 것이다!
좋은 소식은 일단 유니코드 객체가 있으면, 정확하게 일반 문자열 객체와 똑 같이 행위한다는 것이다. 그래서 (\u와 \U 피신문자 말고는) 새로 배울 구문이 없다. 다음은 짧은 샘플 코드로서 유니코드 객체가 예상대로 행위하는 것을 보여준다:
test_stringops01.py
if __name__ == '__main__':
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
print "uni = ",repr(uni)
print "len(uni) = ",len(uni)
# print the "Hello" part
print "uni[:5] = ",uni[:5]
# print the Unicode characters one at a time
print "uni[5] = ",repr(uni[5])
print "uni[6] = ",repr(uni[6])
print "uni[7] = ",repr(uni[7])
# Depending on how Python was compiled, \U characters
# may be stored as two Unicode characters -- see the
# section "A wrinkle in \U" below for more details ...
print "uni[8] = ",repr(uni[8])
print "uni[9] = ",repr(uni[9])
# print the "Unicode" text at the end
print "uni[10:] = ",repr(uni[10:])
uni = u"Hello\u001A\u0BC3\u1451\U0001D10CUnicode"
print "uni = ",repr(uni)
print "len(uni) = ",len(uni)
# print the "Hello" part
print "uni[:5] = ",uni[:5]
# print the Unicode characters one at a time
print "uni[5] = ",repr(uni[5])
print "uni[6] = ",repr(uni[6])
print "uni[7] = ",repr(uni[7])
# Depending on how Python was compiled, \U characters
# may be stored as two Unicode characters -- see the
# section "A wrinkle in \U" below for more details ...
print "uni[8] = ",repr(uni[8])
print "uni[9] = ",repr(uni[9])
# print the "Unicode" text at the end
print "uni[10:] = ",repr(uni[10:])
이 샘플을 실행하면 다음과 같이 출력된다:
uni = u'Hello\x1a\u0bc3\u1451\U0001d10cUnicode'
len(uni) = 17
uni[:5] = Hello
uni[5] = u'\x1a'
uni[6] = u'\u0bc3'
uni[7] = u'\u1451'
uni[8] = u'\ud834'
uni[9] = u'\udd0c'
uni[10:] = u'Unicode'
len(uni) = 17
uni[:5] = Hello
uni[5] = u'\x1a'
uni[6] = u'\u0bc3'
uni[7] = u'\u1451'
uni[8] = u'\ud834'
uni[9] = u'\udd0c'
uni[10:] = u'Unicode'
\U의 결함
어떻게 파이썬이 컴파일되었는가에 따라, 유니코드 객체를 내부적으로 UTF-16 (2 바이트/문자) 또는 UTF-32 (4 바이트/문자) 포맷으로 저장한다. 불행하게도 이 낮은-수준의 상세는 보통의 문자열 인터페이스를 통하여 노출되어 있다.
\u03a0 같은 4-자리 (16-비트) 문자에 대해서는 아무 차이가 없다.
a = u'\u03a0'
print len(a)
위 코드는 파이썬이 어떻게 구축되었는가에 상관없이, 길이 1을 보여준다. 그리고 a[0]는 언제나 \u03a0이 된다. 그렇지만, \U0001FF00 같은 8-자리 (32-비트) 문자라면, 차이가 있다. 분명히, 32-비트 값은 16-비트 코드로 직접 표현될 수 없다. 그래서 두 쌍의 16-비트 값이 사용된다. (0xD800 - 0xDFFF 코드는 이른바 "대리 쌍(surrogate pairs)"이라고 불리우는데, 이런 두-문자 연속열을 위하여 예약되어 있다. 이런 값들은 그 자체로 사용되면 무효이다. 유니코드 규격에 의하면 말이다.)print len(a)
다음은 무슨 일이 일어나는지 보여주는 샘플 프로그램이다:
\U에 무슨 일이 일어나는가 ...
a = u'\U0001ff00'
print "Length:",len(a)
print "Chars:"
for c in a:
print repr(c)
print "Length:",len(a)
print "Chars:"
for c in a:
print repr(c)
출력결과, 'UTF-16' Python
Length: 2
Chars:
u'\ud83f'
u'\udf00'
Chars:
u'\ud83f'
u'\udf00'
출력결과, 'UTF-32' Python
Length: 1
Chars:
u'\U0001ff00'
Chars:
u'\U0001ff00'
xmlmap 없이
a = u'A\U0001ff00C\U0001fafbD'
print "Length:",len(a)
print "Chars:"
for c in a:
print repr(c)
print "Length:",len(a)
print "Chars:"
for c in a:
print repr(c)
UTF-16에서 xmlmap 없이 수행한 결과
Length: 7
Chars:
u'A'
u'\ud83f'
u'\udf00'
u'C'
u'\ud83e'
u'\udefb'
u'D'
Chars:
u'A'
u'\ud83f'
u'\udf00'
u'C'
u'\ud83e'
u'\udefb'
u'D'
xmlmap로
from gnosis.xml.xmlmap import usplit
a = u'A\U0001ff00C\U0001fafbD'
print "Length:",len(a)
print "Chars:"
for c in usplit(a):
print repr(c)
a = u'A\U0001ff00C\U0001fafbD'
print "Length:",len(a)
print "Chars:"
for c in usplit(a):
print repr(c)
UTF-16 파이썬에서 xmlmap를 가지고 수행한 결과
Length: 7
Chars:
u'A'
u'\U0001ff00'
u'C'
u'\U0001fafb'
u'D'
Chars:
u'A'
u'\U0001ff00'
u'C'
u'\U0001fafb'
u'D'
파이썬 2.0 & 2.1의 버그
그렇다. 파이썬 2.0과 2.1 이라니 "누가 신경쓸지" 궁금하실 거다. 그러나 완벽하게 이식성있는 코드를 작성하려면, 문제가 된다!
파이썬 2.0.x 그리고 2.1.x은 \uD800-\uDFFF 범위의 한개짜리-문자 코드를 다룰 때 심각한 버그가 있다.
아래의 샘플 코드가 그 문제를 보여준다:
u = unichr(0xd800)
print "Orig: ",repr(u)
# create utf-8 from '\ud800'
ue = u.encode('utf-8')
print "UTF-8: ",repr(ue)
# decode back to unicode
uu = unicode(ue,'utf-8')
print "Back: ",repr(uu)
print "Orig: ",repr(u)
# create utf-8 from '\ud800'
ue = u.encode('utf-8')
print "UTF-8: ",repr(ue)
# decode back to unicode
uu = unicode(ue,'utf-8')
print "Back: ",repr(uu)
이 코드를 파이썬 2.2 이상에서 실행하면 예상대로 결과를 돌려준다:
Orig: u'\ud800'
UTF-8: '\xed\xa0\x80'
Back: u'\ud800'
UTF-8: '\xed\xa0\x80'
Back: u'\ud800'
파이썬 2.0.x은 다음과 같이 출력한다:
Orig: u'\uD800'
UTF-8: '\240\200'
Traceback (most recent call last):
File "test_utf8_bug.py", line 9, in ?
uu = unicode(ue,'utf-8')
UnicodeError: UTF-8 decoding error: unexpected code byte
UTF-8: '\240\200'
Traceback (most recent call last):
File "test_utf8_bug.py", line 9, in ?
uu = unicode(ue,'utf-8')
UnicodeError: UTF-8 decoding error: unexpected code byte
파이썬 2.1.x은 다음과 같이 출력한다:
Orig: u'\ud800'
UTF-8: '\xa0\x80'
Traceback (most recent call last):
File "test_utf8_bug.py", line 9, in ?
uu = unicode(ue,'utf-8')
UnicodeError: UTF-8 decoding error: unexpected code byte
UTF-8: '\xa0\x80'
Traceback (most recent call last):
File "test_utf8_bug.py", line 9, in ?
uu = unicode(ue,'utf-8')
UnicodeError: UTF-8 decoding error: unexpected code byte
보시다시피, 둘 모두 단일 문자로 사용되면 u'\ud800'를 인코드하지 못한다. 0xD800 .. 0xDFF 사이의 문자들이 그 자체로 사용될 때 유효하지 않는 것은 맞지만, 사실은 파이썬에서는 홀로 사용할 수 있다.
그러나 그것이 무효한 걸, 왜 파이썬이 신경쓰는가?
좋은 예가 떠 올랐다. 완전히 우연하게도 이 자습서를 위한 코드를 만들다가 말이다. 파이썬 파일을 두개 만들어 보자:
aaa.py
x = u'\ud800'
bbb.py
import sys
sys.path.insert(0,'.')
import aaa
sys.path.insert(0,'.')
import aaa
이제 파이썬 2.0.x/2.1.x을 사용하여 bbb.py를 두 번 실행해보자 (두 번 실행될 필요가 있으므로 두 번째는 aaa.pyc를 적재할 것이다). 두 번째 실행되면 다음과 같은 에러를 맞이한다:
Traceback (most recent call last):
File "bbb.py", line 3, in ?
import aaa
UnicodeError: UTF-8 decoding error: unexpected code byte
File "bbb.py", line 3, in ?
import aaa
UnicodeError: UTF-8 decoding error: unexpected code byte
바로 그것이다. 소스에 u'\ud800' 같은 문자열이 들어 있다면, 파이썬 2.0.x/2.1.x은 자신의 바이트 코드를 .pyc 파일로부터 재적재 하지 못한다. 그런 경우 이식성있는 임시방책은 u'\ud800' 대신에 unichr(0xd800)를 사용하는 것이 될 것이다 (이것을 gnosis.xml.pickle이 해준다).
"범용 기록기"로서의 파이썬
이 시점까지 본인은 보여주기 위하여 유니코드와 UTF 사이를 변환해 왔다. 그렇지만, 파이썬은 그 보다 훨씬 더 많은 일을 선사한다. 거의 모든 멀티바이트 문자열을 유니코드로 (그리고 그 반대로) 변환할 수 있다. 이런 변환을 모두 구현하는 일은 엄청난 노고이다. 다행스럽게도 파이썬이 대신 그 일을 해준다. 그래서 우리는 사용하는 방법만 알면 된다..
그리스어 테이블을 다시 연구해 보자. 이 번에는 문자들을 유니코드와 ISO-8859-7 ("native Greek")로 나타내 보겠다.
| 문자 | 이름 | 유니코드로 | ISO-8859-7로 |
|---|---|---|---|
| Π | Greek Capital Letter Pi | 03A0 | 0xD0 |
| Σ | Greek Capital Letter Sigma | 03A3 | 0xD3 |
| Ω | Greek Capital Letter Omega | 03A9 | 0xD9 |
# {Pi}{Sigma}{Omega} as ISO-8859-7 encoded string
b = '\xd0\xd3\xd9'
# Convert to Unicode ('univeral format')
u = unicode(b, 'iso-8859-7')
print repr(u)
# ... and back to ISO-8859-7
c = u.encode('iso-8859-7')
print repr(c)
b = '\xd0\xd3\xd9'
# Convert to Unicode ('univeral format')
u = unicode(b, 'iso-8859-7')
print repr(u)
# ... and back to ISO-8859-7
c = u.encode('iso-8859-7')
print repr(c)
다음과 같이 보여준다:
u'\u03a0\u03a3\u03a9'
\xd0\xd3\xd9
\xd0\xd3\xd9
또 파이썬을 "범용 기록기"로 사용할 수 있다. ShiftJIS라는 일어 인코딩으로 된 파일을 받았다고 해보자. 그리고 EUC-JP 인코딩으로 변환하고 싶다고 해보자:
txt = ... the ShiftJIS-encoded text ...
# convert to Unicode ("universal format")
u = unicode(txt, 'shiftjis')
# convert to EUC-JP
out = u.encode('eucjp')
# convert to Unicode ("universal format")
u = unicode(txt, 'shiftjis')
# convert to EUC-JP
out = u.encode('eucjp')
물론, 이것은 호환성이 있는 문자 집합 사이를 변환할 때만 작동한다. 이런 식으로 일어 문자 세트와 그리스어 문자 세트 사이를 변환하려는 시도는 작동하지 않을 것이다.
이제 재미가 시작되리니 ... 유니코드와 실제 세계
이제 파이썬 안에서 유니코드 객체를 사용하는 방법을 낱낱히 익혔다. 멋지지 않은가? 그렇지만, 나머지 세계는 파이썬 만큼 깔끔하고 말끔하지 않다. 그래서 파이썬이 아닌 다른 세계에서는 어떻게 유니코드를 다루는지 이해할 필요가 있다. 엄청나게 어렵지는 않지만 고려해야할 특별한 경우가 많다.
지금부터는 다음을 다룰 때 일어나는 유니코드 문제를 살펴보겠다:
- 파일이름 (운영체제 종속적 문제들)
- XML
- HTML
- 네트워크 파일 (삼바(Samba))
유니코드 파일이름
너무 단순해 보인다. 그렇지 않은가? 파일 이름에 그리스어 문자를 주고 싶어서, 그냥 다음과 같이 하면:
open(unicode_name, 'w')
이론적으로는 맞다. 그것이면 충분하다. 그렇지만 이것이 작동하지 않는 경우가 무수히 많으며, 프로그램이 어떤 플랫폼에서 실행중인가에 따라 달라진다.
마이크로소프트 윈도즈
파이썬을 윈도즈 아래에서 실행시키려면 적어도 두 가지 방식이 있다. 첫째 방법은 http://www.python.org/에서 얻은 Win32 이진파일을 사용하는 것이다. 이 방법을 "윈도우즈-고유의 파이썬"이라고 칭하겠다.
다른 방법은 Cygwin에 따라오는 파이썬 버전을 사용하는 것이다. 이 파이썬 버전은 (사용자 코드가) 윈도우즈-고유의 환경처럼 보이기 보다 POSIX를 더 닮았다..
많은 경우, 두 버전은 교환가능하다. 이식성있게 파이썬 코드를 작성하는 한, 어느 인터프리터 아래에서 실행중인지 신경쓰지 않아도 된다. 그렇지만, 한가지 중요한 예외는 유니코드를 다룰 때이다. 그 때문에 어느 버전을 실행하고 있는지 구체적으로 언급할 것이다.
윈도우즈-고유의 파이썬 사용하기
익숙한 그리스어 심볼을 계속 사용해 보자:
| 샘플 유니코드 심볼 | ||
|---|---|---|
| 03A0 | Π | Greek Capital Letter Pi |
| 03A3 | Σ | Greek Capital Letter Sigma |
| 03A9 | Ω | Greek Capital Letter Omega |
우리의 샘플 유니코드 파일이름은 다음과 같이 될 것이다:
# this is: abc_{PI}{Sigma}{Omega}.txt
uname = u"abc_\u03A0\u03A3\u03A9.txt"
uname = u"abc_\u03A0\u03A3\u03A9.txt"
그 이름으로 파일을 만들어 보자. 한 줄의 텍스트를 담아서 말이다:
open(uname,'w').write('Hello world!\n')
익스플로러 창을 열면 결과를 보여준다 (클릭하면 더 큰 버전을 볼 수 있다):
파일이름이 유니코드로 찬란하게 빛나고 있다.
이제, 어떻게 os.listdir()이 이 이름과 작동하는지 살펴보자. 알아야 할 첫 번째는 os.listdir()에 두 개의 연산 모드가 있다는 것이다:
- 비-유니코드 모드,os.listdir()에 비-유니코드 문자열을, 즉 os.listdir('.')을 건네면 수행된다.
- 유니코드 모드, os.listdir()에 유니코드 문자열을, 즉 os.listdir(u'.')을 건네면 수행된다.
os.chdir('ttt')
# there is only one file in directory 'ttt'
name = os.listdir(u'.')[0]
print "Got name: ",repr(name)
print "Line: ",open(name,'r').read()
# there is only one file in directory 'ttt'
name = os.listdir(u'.')[0]
print "Got name: ",repr(name)
print "Line: ",open(name,'r').read()
이 프로그램을 실행하면 다음과 같이 출력된다:
Got name: u'abc_\u03a0\u03a3\u03a9.txt'
Line: Hello world!
Line: Hello world!
위와 비교하면, 올바른 것 같아 보인다. print repr(name)가 요구되었다는 점에 주목하자. 이름을 화면에 직접 인쇄하려고 했다면 에러가 일어났을 것이기 때문이다. 왜인가? 그렇다. 역시 파이썬은 원하는 인코딩이 ASCII 코딩이라고 간주했을 것이고, 그래서 에러와 함께 실패했을 것이다.
이제 위의 샘플을 시도해 보자. 그러나 비-유니코드 버전의 os.listdir()을 사용해서 말이다:
os.chdir('ttt')
# there is only one file in directory 'ttt'
name = os.listdir('.')[0]
print "Got name: ",repr(name)
print "Line: ",open(name,'r').read()
# there is only one file in directory 'ttt'
name = os.listdir('.')[0]
print "Got name: ",repr(name)
print "Line: ",open(name,'r').read()
다음과 같이 출력된다:
Got name: 'abc_?SO.txt'
Line:
Traceback (most recent call last):
File "c:\frank\src\unicode\t2.py", line 8, in ?
print "Line: ",open(name,'r').read()
IOError: [Errno 2] No such file or directory: 'abc_?SO.txt'
Line:
Traceback (most recent call last):
File "c:\frank\src\unicode\t2.py", line 8, in ?
print "Line: ",open(name,'r').read()
IOError: [Errno 2] No such file or directory: 'abc_?SO.txt'
이런! 무슨 일이 일어났는가? 놀라운 win32 "이중-API"의 세계에 오신 것을 환영한다.
약간의 배경지식을 설명하자면:
Windows NT/2000/XP는 언제나 바탕의 파일 시스템에 대한 파일이름을 유니코드로 쓴다 (각주 2).
그래서 이론적으로 유니코드 파일이름은 흠없이 파이썬과 작동해야 한다.
불행하게도, win32는 실제로 파일 시스템과 상호작용하는데 두 세트의 API를 제공한다. 그리고 진짜 마이크로소프트 스타일로, 둘은 호환되지 않는다. 그 두 API는 다음과 같다:
불행하게도, win32는 실제로 파일 시스템과 상호작용하는데 두 세트의 API를 제공한다. 그리고 진짜 마이크로소프트 스타일로, 둘은 호환되지 않는다. 그 두 API는 다음과 같다:
- 유니코드-인식 어플리케이션을 위한 API 한세트. 진짜 유니코드 파일이름을 돌려준다.
- 유니코드-미인식 어플리케이션을 위한 API 한세트. 진짜 유니코드 파일이름의 locale-의존적인 코딩.
- 유니코드 문자열로 os.listdir()이나 open() 등등을 호출하면, 파이썬은 유니코드 버전의 API를 호출하며, 진짜 유니코드 파일이름을 얻는다. (이는 위의 첫 API 세트에 상응한다).
- 비-유니코드 문자열로 os.listdir()이나 open() 등등을 호출하면, 파이썬은 비-유니코드 버전의 API를 호출하며, 바로 여기가 문제가 숨어 있는 곳이다. 비-유니코드 API는 MBCS라고 부르는 코덱으로 유니코드를 처리한다. MBCS는 결함이 많은 코덱이다: MBCS 이름은 유니코드로 표현될 수 있지만, 그 반대는 안된다. MBCS 코딩은 또한 현재 로케일에 따라 바뀐다. 다른 말로 해서, MBCS 멀티바이트-문자로 된 파일이름을 가지고 나의 영어 로케일 머신에서 CD를 구워, 그 CD를 일본에 보내면, 거기에서 파일이름은 완전히 다른 문자가 들어있는 것처럼 보일 것이다.
이제 배경에 있는 진실을 알았으므로, 위에서 무슨 일이 일어났는지 알 수 있다. os.listdir('.')을 사용하면, 파일 시스템에 저장된 MBCS-버전의 진짜 유니코드 이름을 얻는다. 그리고, 나의 영어-로케일 컴퓨터에서, 그리스어 문자에 대하여 정확하게 일치하는 매핑이 없으므로, 결국 "?", "S", 그리고 "O"로 결말을 맺는다. 이 때문에 이상한 결과에 다다른다. 영어 로케일에서는 MBCS API를 사용하여 그리스어-문자 파일을 열 방법이 없다(!!).
- 기준 선
-
언제나 os.listdir(), open(), 등등에 유니코드 문자열을 사용하기를 권장한다. Windows NT/2000/XP는 언제나 파일이름을 유니코드로 저장하며, 그래서 이것이 고유의 행위이다. 그리고 위에 보여준 바와 같이 가끔은 유니코드 파일 이름을 열 수 있는 유일한 방법이 될 수 있다.
위험! CygwinCygwin은 여기에서 큰 문제가 있다. (적어도, 현재로는) 유니코드를 지원하지 않는다. 다시 말해, 유니코드 버전의 win32 API를 절대로 호출하지 않는다. 그러므로, (그리스어-문자 파일이름과 같이) 어떤 파일은 Cygwin에서 여는 것이 불가능하다. os.listdir(u'.')이나 os.listdir('')을 사용한다면 문제가 되지 않는다; 언제든지 MBCS-코드된 버전을 얻기 때문이다.
이것은 파이썬-종속적인 문제가 아님에 주의하자; Cygwin에 관련된 시스템 문제이다. zsh, ls, zip, unzip, mkisofs 같은 모든 Cygwin 유틸리티는 그리스어-문자 이름을 인식하지 못할 것이며 다양한 에러를 보고할 것이다.
Unix/POSIX/Linux
언제나 파일이름을 유니코드 포맷으로 저장하는 Windows NT/2000/XP와는 다르게, POSIX 시스템은 (리눅스를 포함하여) 언제나 파일이름을 이진 문자열로 저장한다. 이것이 약간 더 유연하다. 왜냐하면 운영체제 그 자체는 파일이름에 어떤 인코딩이 사용되었는지 알 (또는 신경쓸) 필요가 없기 때문이다. 단점이라면 사용자가 책임을 지고 적절한 코딩을 위해 환경("locale")을 설정해 주어야 한다는 것이다.
로케일 설정하기
POSIX 박스가 유니코드 파일이름을 다루도록 설정하는 세부작업은 이 문서의 범위를 넘는다. 그러나 일반적으로 몇 가지 환경 변수를 설정하는 것으로 귀결된다. 나의 경우, U.S. English 로케일에서 UTF-8 코덱을 사용하고 싶었다. 그래서 다음 기동 파일들에다 몇 줄을 추가했다(젠투 리눅스와 우분투 리눅스 아래에서 이를 시도해 보았다. 물론, 모든 리눅스 시스템이 비슷하겠지만 말이다):
.bashrc에 다음과 같이 추가하였다:
LANG="en_US.utf8"
LANGUAGE="en_US.utf8"
LC_ALL="en_US.utf8"
export LANG
export LANGUAGE
export LC_ALL
LANGUAGE="en_US.utf8"
LC_ALL="en_US.utf8"
export LANG
export LANGUAGE
export LC_ALL
제대로 측정하기 위하여, 같은 줄을 .zshrc 파일에도 추가하였다.
앞 세 줄을 /etc/env.d/02locale에도 추가하였다.
절대 주의
제발 무엇을 하고 있는지 모르겠다면 맹목적으로 여러분의 시스템을 위와 같이 바꾸지 마시길 바란다. 로케일을 전환하면 파일을 읽을 수 없을 수 있다. 위의 예는 오직 ASCII 로케일로부터 UTF-8 로케일로 바꾸는 간단한 예를 보이기 위한 것이다.
POSIX 아래의 파이썬
파이썬에 관한 한, POSIX 아래에서 커다란 장점은 둘 중에 아무거나 사용해도 된다는 것이다:
os.listdir('.')
또는:
os.listdir(u'.')
두 방법 모두 open()에 건네 파일을 열 수 있는 문자열을 돌려준다. 이는 윈도우즈보다 훨씬 더 좋다. 윈도우즈는 os.listdir('.')을 사용한다면 유니코드 이름으로 조작하여 돌려준다. 이는 위에 보다시피 가끔 파일을 여는데 유효한 이름을 주는데 실패할 수 있다. POSIX/Linux 아래에서는 언제나 유효한 이름을 얻는다.
다음은 이를 보여주는 샘플 함수이다:
test_posix01.test()
def test():
# Demonstrate that listdir(u'.') and listdir('.') both
# work fine under POSIX (unlike win32)
import os
uname = u'abc_\u03a0\u03a3\u03a9.txt'
# make a tempdir so I'll only have a single file in it
os.mkdir('ttt')
os.chdir('ttt')
open(uname,'w').write("Hello unicode!\n")
# use listdir() to get name as Unicode
name = os.listdir(u'.')[0]
print "As unicode: ",repr(name)
print " Read line: ",open(name,'r').read()
# now get name as a bytestring
name = os.listdir('.')[0]
print "As bytestring: ",repr(name)
print " Read line: ",open(name,'r').read()
# Demonstrate that listdir(u'.') and listdir('.') both
# work fine under POSIX (unlike win32)
import os
uname = u'abc_\u03a0\u03a3\u03a9.txt'
# make a tempdir so I'll only have a single file in it
os.mkdir('ttt')
os.chdir('ttt')
open(uname,'w').write("Hello unicode!\n")
# use listdir() to get name as Unicode
name = os.listdir(u'.')[0]
print "As unicode: ",repr(name)
print " Read line: ",open(name,'r').read()
# now get name as a bytestring
name = os.listdir('.')[0]
print "As bytestring: ",repr(name)
print " Read line: ",open(name,'r').read()
이를 실행하면 다음과 같은 결과를 얻는다:
As unicode: u'abc_\u03a0\u03a3\u03a9.txt'
Read line: Hello unicode!
As bytestring: 'abc_\xce\xa0\xce\xa3\xce\xa9.txt'
Read line: Hello unicode!
Read line: Hello unicode!
As bytestring: 'abc_\xce\xa0\xce\xa3\xce\xa9.txt'
Read line: Hello unicode!
보시다시피, 성공적으로 파일을 읽었다. 파일이름을 유니코드 버전으로 사용하든 바이트스트링 버전으로 사용하든 상관없이 말이다.
어플리케이션 데모
기본적으로 "DOS box"와 윈도우즈 익스플로러가 있는 마이크로소프트 윈도즈 세계와는 다르게, Linux 아래에서는 어떤 터미널을 실행할지 어떤 파일 관리자를 실행할지 선택이 수 없이 많다. 이것은 한 편으로 축복이자 또 한 편으로는 저주이다: 기호에 꼭 맞는 어플리케이션을 고를 수 있다는 것은 축복이지만, 또한 모든 어플리케이션이 똑 같은 정도로 유니코드를 지원하는 것은 아니라는 점에서는 저주이다.
다음은 여러 인기 있는 어플리케이션이 지원하고 있는 것들을 조사한 것이다.
유니코드 파일이름을 지원하는 어플리케이션
개인적으로 현재 즐겨쓰는 어플은 다중-언어 터미널인 mlterm이다 (클릭하면 좀 더 큰 버전을 보실 수 있음):
GNOME 터미널 (gnome-terminal):
KDE 터미널 (konsole):
수정된 버전의 rxvt (rxvt-unicode)는 유니코드를 처리한다. 물론 내가 선택한 폰트에서 밑줄 문자에 약간 문제가 있기는 하다 ...
다음은 KDE 관리자 창(konqueror)에 보이는 그리스어-문자 파일이다:
그리고 다음은 GNOME 파일 관리자 (Nautilus)에 보이는 것이다:
XFCE 4 파일 관리자:
표준 KDE 파일 선택자는 유니코드 파일이름을 지원한다:
GNOME 파일 선택자도 마찬가지다:
유니코드 파일이름을 지원하지 않는 어플리케이션
표준 rxvt는 유니코드를 올바르게 처리하지 못한다:
Xfm 파일 관리자는 유니코드 파일이름을 처리하지 못한다:
Mac OS/X
본인은 OSX 머신이 없어서 이를 테스트하지 못했지만, 독자 여러분께서 OSX의 유니코드 지원에 관한 정보를 공헌해 주셨다.
한 독자분께서 지적해 주신 바에 의하면 os.listdir('.')와 os.listdir(u'.') 둘 모두 open()에 바로 건넬 수 있는 객체를 돌려준다. POSIX 아래에서 할 수 있듯이 말이다.
독자 하반(Hraban)은 다음과 같이 지적했다:
MacOS X는 특수한 종류의 decomposed UTF-8을 사용하여 파일이름을 저장한다고 언급해야 했습니다. 파일이름을 읽어 그것을 "보통의" UTF-8 파일에 쓰고 싶다면, 정규화 시켜야 합니다 (적어도 편집기에서 즉 나의 TeX 시스템에서 decomposed UTF-8를 이해하지 못한다면 말입니다):
filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8')
이 글을 읽고 (본인과 같이) 문제를 잘 이해하지 못하는 분들을 위하여 다음에 몇가지 참조할 것들을 보여준다: 나는 이렇게 생각했다. é와 같이 악센트가 붙은 이름을 건네면, 파일시스템에 저장하기 전에 이것을 e와 '로 분해할 것이라고 말이다 (이 행위는 유니코드 표준에 정의되어 있다).
이 섹션에 다른 것을 덧 붙일 수 있다면, 아래에 논평을 남겨 주시기 바란다!
유니코드와 HTML
파이썬으로 HTML을 만들고 있는 자신을 보게 될 것이다 (예를 들어, mod_python, CherryPy, 기타 등등을 사용할 때 말이다). 그래서 유니코드 문자를 HTML 문서에 어떻게 사용하는가?
그 해답에는 다음과 같은 쉬운 단계가 연루된다:
- <meta> 태그를 사용하여 사용자의 브라우저에 사용한 인코딩을 알려주자. (각주 3)
- HTML을 유니코드 객체로 만들자.
- 선호하는 코덱으로 HTML 바이트스트림을 쓰자.
code = 'utf-8' # make it easy to switch the codec later
html = u'<html>'
# use a <meta> tag to specify the document encoding used
html += u'<meta http-equiv="content-type" content="text/html; charset=%s">' % code
html += u'<head></head><body>'
# my actual Unicode content ...
html += u'abc_\u03A0\u03A3\u03A9.txt'
html += u'</body></html>'
# Now, you cannot write Unicode directly to a file.
# First have to either convert it to a bytestring using a codec, or
# open the file with the 'codecs' module.
# Method #1, doing the conversion yourself:
open('t.html','w').write( html.encode( code ) )
# Or, by using the codecs module:
import codecs
codecs.open('t.html','w',code).write( html )
# .. the method you use depends on personal preference and/or
# convenience in the code you are writing.
html = u'<html>'
# use a <meta> tag to specify the document encoding used
html += u'<meta http-equiv="content-type" content="text/html; charset=%s">' % code
html += u'<head></head><body>'
# my actual Unicode content ...
html += u'abc_\u03A0\u03A3\u03A9.txt'
html += u'</body></html>'
# Now, you cannot write Unicode directly to a file.
# First have to either convert it to a bytestring using a codec, or
# open the file with the 'codecs' module.
# Method #1, doing the conversion yourself:
open('t.html','w').write( html.encode( code ) )
# Or, by using the codecs module:
import codecs
codecs.open('t.html','w',code).write( html )
# .. the method you use depends on personal preference and/or
# convenience in the code you are writing.
이제 페이지 (t.html)를 파이어폭스에서 열어보자:
예상한 그대로이다!
이제, 샘플 코드로 다시 돌아가 줄을 교체해 보자:
code = 'utf-8'
를 다음과 같이...
code = 'utf-16'
... HTML 파일은 이제 UTF-16 포맷으로 씌여지겠지만, 브라우저 창에 표시되는 결과는 정확하게 똑 같을 것이다.
유니코드와 XML
XML 1.0 표준은 모든 해석기가 UTF-8 인코딩과 UTF-16 인코딩을 지원해야 한다고 요구한다. 그래서, XML 해석기라면 당연히 합법적인 UTF-8 또는 UTF-16로 인코드된 문서를 입력으로 허용할 것 같아 보일 것이다. 그렇지 않은가?
아니다!
다음 예제 프로그램을 한 번 보자:
xml = u'<?xml version="1.0" encoding="utf-8" ?>'
xml += u'<H> \u0019 </H>'
# encode as UTF-8
utf8_string = xml.encode( 'utf-8' )
xml += u'<H> \u0019 </H>'
# encode as UTF-8
utf8_string = xml.encode( 'utf-8' )
이 시점에서, utf8_string은 XML을 나타내는 완벽하게 유효한 UTF-8 문자열이다. 그래서 해석할 수 있어야 한다. 그렇지 않은가?:
from xml.dom.minidom import parseString
parseString( utf8_string )
parseString( utf8_string )
다음은 위 코드를 실행하면 일어나는 일이다:
Traceback (most recent call last):
File "t9.py", line 9, in ?
parseString( utf8_string )
File "c:\py23\lib\xml\dom\minidom.py", line 1929, in parseString
return expatbuilder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 940, in parseString
return builder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 223, in parseString
parser.Parse(string, True)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 43
File "t9.py", line 9, in ?
parseString( utf8_string )
File "c:\py23\lib\xml\dom\minidom.py", line 1929, in parseString
return expatbuilder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 940, in parseString
return builder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 223, in parseString
parser.Parse(string, True)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 43
우와 - 무슨 일이 일어났는가? 컬럼 43에서 에러가 일어났다. 컬럼 43이 무엇인지 살펴보자:
>> print repr(utf8_string[43])
'\x19'
'\x19'
유니코드 문자 U+0019 같이 보이지 않는다. 왜 그런가? XML 1.0 표준의 섹션 2.2에서 문서에 나타나도 좋은 합법적 문자 집합을 정의한다. 그 표준에 따르면:
/* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] |
[#xE000-#xFFFD] | [#x10000-#x10FFFF]
Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] |
[#xE000-#xFFFD] | [#x10000-#x10FFFF]
분명히, 합법적으로 XML 문서 안에 포함가능한 문자에 중대한 틈이 있다. 위의 코드를 파이썬 함수로 변환해 보자. 주어진 유니코드 값이 XML 스트림을 쓰는데 합법적인지 테스트하는데 사용하기 위해서 말이다:
gnosis.xml.xmlmap.raw_illegal_xml_regex()
def raw_illegal_xml_regex():
"""
I want to define a regexp to match *illegal* characters.
That way, I can do "re.search()" to find a single character,
instead of "re.match()" to match the entire string. [Based on
my assumption that .search() would be faster in this case.]
Here is a verbose map of the XML character space (as defined
in section 2.2 of the XML specification):
u0000 - u0008 = Illegal
u0009 - u000A = Legal
u000B - u000C = Illegal
u000D = Legal
u000E - u001F = Illegal
u0020 - uD7FF = Legal
uD800 - uDFFF = Illegal (See note!)
uE000 - uFFFD = Legal
uFFFE - uFFFF = Illegal
U00010000 - U0010FFFF = Legal (See note!)
Note:
The range U00010000 - U0010FFFF is coded as 2-character sequences
using the codes (D800-DBFF),(DC00-DFFF), which are both illegal
when used as single chars, from above.
Python won't let you define \U character ranges, so you can't
just say '\U00010000-\U0010FFFF'. However, you can take advantage
of the fact that (D800-DBFF) and (DC00-DFFF) are illegal, unless
part of a 2-character sequence, to match for the \U characters.
"""
# First, add a group for all the basic illegal areas above
re_xml_illegal = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])'
re_xml_illegal += u"|"
# Next, we know that (uD800-uDBFF) must ALWAYS be followed by (uDC00-uDFFF),
# and (uDC00-uDFFF) must ALWAYS be preceded by (uD800-uDBFF), so this
# is how we check for the U00010000-U0010FFFF range. There are also special
# case checks for start & end of string cases.
# I've defined this oddly due to the bug mentioned at the top of this file
re_xml_illegal += u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \
(unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff))
return re_xml_illegal
"""
I want to define a regexp to match *illegal* characters.
That way, I can do "re.search()" to find a single character,
instead of "re.match()" to match the entire string. [Based on
my assumption that .search() would be faster in this case.]
Here is a verbose map of the XML character space (as defined
in section 2.2 of the XML specification):
u0000 - u0008 = Illegal
u0009 - u000A = Legal
u000B - u000C = Illegal
u000D = Legal
u000E - u001F = Illegal
u0020 - uD7FF = Legal
uD800 - uDFFF = Illegal (See note!)
uE000 - uFFFD = Legal
uFFFE - uFFFF = Illegal
U00010000 - U0010FFFF = Legal (See note!)
Note:
The range U00010000 - U0010FFFF is coded as 2-character sequences
using the codes (D800-DBFF),(DC00-DFFF), which are both illegal
when used as single chars, from above.
Python won't let you define \U character ranges, so you can't
just say '\U00010000-\U0010FFFF'. However, you can take advantage
of the fact that (D800-DBFF) and (DC00-DFFF) are illegal, unless
part of a 2-character sequence, to match for the \U characters.
"""
# First, add a group for all the basic illegal areas above
re_xml_illegal = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])'
re_xml_illegal += u"|"
# Next, we know that (uD800-uDBFF) must ALWAYS be followed by (uDC00-uDFFF),
# and (uDC00-uDFFF) must ALWAYS be preceded by (uD800-uDBFF), so this
# is how we check for the U00010000-U0010FFFF range. There are also special
# case checks for start & end of string cases.
# I've defined this oddly due to the bug mentioned at the top of this file
re_xml_illegal += u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \
(unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff))
return re_xml_illegal
코드를 사용하여 ...
def make_illegal_xml_regex():
return re.compile( raw_illegal_xml_regex() )
c_re_xml_illegal = make_illegal_xml_regex()
return re.compile( raw_illegal_xml_regex() )
c_re_xml_illegal = make_illegal_xml_regex()
최종 결과:
gnosis.xml.xmlmap.is_legal_xml()
def is_legal_xml( uval ):
"""
Given a Unicode object, figure out if it is legal
to place it in an XML file.
"""
return (c_re_xml_illegal.search( uval ) == None)
"""
Given a Unicode object, figure out if it is legal
to place it in an XML file.
"""
return (c_re_xml_illegal.search( uval ) == None)
위의 함수는 유니코드 문자열이 있는데, 한 번에 한 문자씩 검색할 때 약간 느린 경우에 좋다. 그래서 다음은 그런 일을 해주는 대안 함수 이다 (이 함수는 앞서 정의된 usplit() 함수를 이용함에 주의하자):
gnosis.xml.xmlmap.is_legal_xml_char()
def is_legal_xml_char( uchar ):
"""
Check if a single unicode char is XML-legal.
(This is faster that running the full 'is_legal_xml()' regexp
when you need to go character-at-a-time. For string-at-a-time
of course you want to use is_legal_xml().)
USAGE NOTE:
If you want to use this in a 'for' loop,
make sure use usplit(), e.g.:
for c in usplit( uval ):
if is_legal_xml_char(c):
...
Otherwise, the first char of a legal 2-character
sequence will be incorrectly tagged as illegal, on
Pythons where \U is stored as 2-chars.
"""
# due to inconsistencies in how \U is handled (based on
# how Python was compiled) it is shorter to test for
# illegal chars than legal ones, and invert the result.
#
# (as one example: (u'\ud900' > u'\U00100000') can be True,
# depending on how Python was compiled. Testing for illegal chars
# lets us stick with the single char sequences (all 2-char
# sequences are legal for XML).
if len(uchar) == 1:
return not \
(
(uchar >= u'\u0000' and uchar <= u'\u0008') or \
(uchar >= u'\u000b' and uchar <= u'\u000c') or \
(uchar >= u'\u000e' and uchar <= u'\u001f') or \
# always illegal as single chars
(uchar >= unichr(0xd800) and uchar <= unichr(0xdfff)) or \
(uchar >= u'\ufffe' and uchar <= u'\uffff')
)
elif len(uchar) == 2:
# all 2-char codings are legal in XML
# (this looks weird, but remember that even after calling
# usplit(), \U00010000 is STILL len() of 2, usplit() just
# made it a single listitem
return True
else:
raise Exception("Must pass a single character to is_legal_xml_char")
"""
Check if a single unicode char is XML-legal.
(This is faster that running the full 'is_legal_xml()' regexp
when you need to go character-at-a-time. For string-at-a-time
of course you want to use is_legal_xml().)
USAGE NOTE:
If you want to use this in a 'for' loop,
make sure use usplit(), e.g.:
for c in usplit( uval ):
if is_legal_xml_char(c):
...
Otherwise, the first char of a legal 2-character
sequence will be incorrectly tagged as illegal, on
Pythons where \U is stored as 2-chars.
"""
# due to inconsistencies in how \U is handled (based on
# how Python was compiled) it is shorter to test for
# illegal chars than legal ones, and invert the result.
#
# (as one example: (u'\ud900' > u'\U00100000') can be True,
# depending on how Python was compiled. Testing for illegal chars
# lets us stick with the single char sequences (all 2-char
# sequences are legal for XML).
if len(uchar) == 1:
return not \
(
(uchar >= u'\u0000' and uchar <= u'\u0008') or \
(uchar >= u'\u000b' and uchar <= u'\u000c') or \
(uchar >= u'\u000e' and uchar <= u'\u001f') or \
# always illegal as single chars
(uchar >= unichr(0xd800) and uchar <= unichr(0xdfff)) or \
(uchar >= u'\ufffe' and uchar <= u'\uffff')
)
elif len(uchar) == 2:
# all 2-char codings are legal in XML
# (this looks weird, but remember that even after calling
# usplit(), \U00010000 is STILL len() of 2, usplit() just
# made it a single listitem
return True
else:
raise Exception("Must pass a single character to is_legal_xml_char")
다음은 위의 함수를 시연해 주는 상당히 광범위한 테스트 사례이다:
test_xml_legality
from xml.dom.minidom import parseString
import re
import sys
# define True/False if this Python doesn't have them
try:
a = True
except:
True = 1
False = 0
from gnosis.xml.xmlmap import *
# sanity check for testing purposes
def try_in_xml( uval ):
"Try putting the Unicode string uval into an XML doc & parsing."
xml = u'<?xml version="1.0" encoding="utf-8" ?>'
xml += u'<H>' + uval + '</H>'
#print [u for u in usplit(xml) if u >= u'\U00010000']
try:
parseString(xml.encode('utf-8'))
return True # succeeded
except:
return False # failed
# --- test cases ---
bad_unicode = [
# 0000-0008 is illegal
u'abc\u0001def',
# 000B-000C is illegal
u'abc\u000cdef',
# 000E-0019 is illegal
u'abc\u0015def',
# D800-DBFF is illegal, unless it starts a 2-char sequence
u'abc\ud900def',
# DC00-DFFF is illegal, unless it ends a 2-char sequence
u'abc\uDDDDdef',
# FFFE-FFFF is illegal
u'abc\ufffedef',
# case of D800-DBFF at end of string (next to last segment of regex)
u'abc\ud800',
# case of DC00-DFFF at start of string (last segment of regex)
u'\udc00'
]
good_unicode = [
# 0009-000A is legal
u'abc\u0009def\u000aghi',
# 000D is legal
u'abc\u000ddef',
# 0020-D7FF is legal
u'abc\u0020def\u8112ghi\ud7ffjkl',
# E000-FFFD is legal
u'abc\ue000def\uF123ghi\ufffdjkl',
# U00010000 - U0010FFFF is legal
u'abc\U00010000def\U00023456ghi\U00101234jkl'
]
if __name__ == '__main__':
print "** BAD VALUES **"
for u in bad_unicode:
# print the unicode value, test legality, and sanity check by
# putting it in an XML document & parsing it
print "%-50s %8s %1d" % (repr(u), is_legal_xml(u), try_in_xml(u))
print "\n** GOOD VALUES **"
for u in good_unicode:
# print the unicode value, test legality, and sanity check by
# putting it in an XML document & parsing it
print "%-50s %8s %1d" % (repr(u), is_legal_xml(u), try_in_xml(u))
# an all-illegal string
u = u'\u0000\u0005\u0008\u000b\u000c\u000e\u0010\u0019' + \
u'\ud800\ud900\u0000\udc00\udd00\udfff\ufffe\uffff'
print "\nTesting one char at a time ..."
print repr(u)
for c in usplit(u):
# test as a char
if is_legal_xml_char(c):
raise "ERROR(1)"
# test as a string
if is_legal_xml(c):
raise "ERROR(2)"
# stick in an XML document to double-check the above
if try_in_xml(c) != False:
raise "ERROR(3)"
print "OK\n"
# an all-legal string
u = u'\u0009\u000a\u000d\u0020\u2345\ud7ff' + \
u'\ue000\ue876\ufffd' + \
u'\U00010000\U00012345\U00100000\U0010ffff'
# subtle -- make sure it allows a handcoded 2-char sequence (this
# is the case that forces usplit() to do a full pass even if \U is
# stored as single chars)
u += u'\ud800\udc00'
print repr(u)
for c in usplit(u):
# test as a char
if not is_legal_xml_char(c):
raise "ERROR(1)"
# test as a string
if not is_legal_xml(c):
raise "ERROR(2)"
# stick in an XML document to double-check the above
if try_in_xml(c) != True:
raise "ERROR(3)"
print "OK"
import re
import sys
# define True/False if this Python doesn't have them
try:
a = True
except:
True = 1
False = 0
from gnosis.xml.xmlmap import *
# sanity check for testing purposes
def try_in_xml( uval ):
"Try putting the Unicode string uval into an XML doc & parsing."
xml = u'<?xml version="1.0" encoding="utf-8" ?>'
xml += u'<H>' + uval + '</H>'
#print [u for u in usplit(xml) if u >= u'\U00010000']
try:
parseString(xml.encode('utf-8'))
return True # succeeded
except:
return False # failed
# --- test cases ---
bad_unicode = [
# 0000-0008 is illegal
u'abc\u0001def',
# 000B-000C is illegal
u'abc\u000cdef',
# 000E-0019 is illegal
u'abc\u0015def',
# D800-DBFF is illegal, unless it starts a 2-char sequence
u'abc\ud900def',
# DC00-DFFF is illegal, unless it ends a 2-char sequence
u'abc\uDDDDdef',
# FFFE-FFFF is illegal
u'abc\ufffedef',
# case of D800-DBFF at end of string (next to last segment of regex)
u'abc\ud800',
# case of DC00-DFFF at start of string (last segment of regex)
u'\udc00'
]
good_unicode = [
# 0009-000A is legal
u'abc\u0009def\u000aghi',
# 000D is legal
u'abc\u000ddef',
# 0020-D7FF is legal
u'abc\u0020def\u8112ghi\ud7ffjkl',
# E000-FFFD is legal
u'abc\ue000def\uF123ghi\ufffdjkl',
# U00010000 - U0010FFFF is legal
u'abc\U00010000def\U00023456ghi\U00101234jkl'
]
if __name__ == '__main__':
print "** BAD VALUES **"
for u in bad_unicode:
# print the unicode value, test legality, and sanity check by
# putting it in an XML document & parsing it
print "%-50s %8s %1d" % (repr(u), is_legal_xml(u), try_in_xml(u))
print "\n** GOOD VALUES **"
for u in good_unicode:
# print the unicode value, test legality, and sanity check by
# putting it in an XML document & parsing it
print "%-50s %8s %1d" % (repr(u), is_legal_xml(u), try_in_xml(u))
# an all-illegal string
u = u'\u0000\u0005\u0008\u000b\u000c\u000e\u0010\u0019' + \
u'\ud800\ud900\u0000\udc00\udd00\udfff\ufffe\uffff'
print "\nTesting one char at a time ..."
print repr(u)
for c in usplit(u):
# test as a char
if is_legal_xml_char(c):
raise "ERROR(1)"
# test as a string
if is_legal_xml(c):
raise "ERROR(2)"
# stick in an XML document to double-check the above
if try_in_xml(c) != False:
raise "ERROR(3)"
print "OK\n"
# an all-legal string
u = u'\u0009\u000a\u000d\u0020\u2345\ud7ff' + \
u'\ue000\ue876\ufffd' + \
u'\U00010000\U00012345\U00100000\U0010ffff'
# subtle -- make sure it allows a handcoded 2-char sequence (this
# is the case that forces usplit() to do a full pass even if \U is
# stored as single chars)
u += u'\ud800\udc00'
print repr(u)
for c in usplit(u):
# test as a char
if not is_legal_xml_char(c):
raise "ERROR(1)"
# test as a string
if not is_legal_xml(c):
raise "ERROR(2)"
# stick in an XML document to double-check the above
if try_in_xml(c) != True:
raise "ERROR(3)"
print "OK"
\U 코딩에서 볼 수 있는 차이를 보여주기 위하여, 두 가지 다른 버전의 파이썬 아래에서 이를 실행하려고 했다.
먼저 파이썬 2.0에서 (나의 머신에서 2-문자 \U 인코딩을 사용하는):
** BAD VALUES **
u'abc\001def' 0 0
u'abc\014def' 0 0
u'abc\025def' 0 0
u'abc\uD900def' 0 0
u'abc\uDDDDdef' 0 0
u'abc\uFFFEdef' 0 0
u'abc\uD800' 0 0
u'\uDC00' 0 0
** GOOD VALUES **
u'abc\011def\012ghi' 1 1
u'abc\015def' 1 1
u'abc def\u8112ghi\uD7FFjkl' 1 1
u'abc\uE000def\uF123ghi\uFFFDjkl' 1 1
u'abc\uD800\uDC00def\uD84D\uDC56ghi\uDBC4\uDE34jkl' 1 1
Testing one char at a time ...
u'\000\005\010\013\014\016\020\031\uD800\uD900\000\uDC00\uDD00
\uDFFF\uFFFE\uFFFF'
OK
u'\011\012\015 \u2345\uD7FF\uE000\uE876\uFFFD\uD800\uDC00\uD808
\uDF45\uDBC0\uDC00\uDBFF\uDFFF\uD800\uDC00'
OK
u'abc\001def' 0 0
u'abc\014def' 0 0
u'abc\025def' 0 0
u'abc\uD900def' 0 0
u'abc\uDDDDdef' 0 0
u'abc\uFFFEdef' 0 0
u'abc\uD800' 0 0
u'\uDC00' 0 0
** GOOD VALUES **
u'abc\011def\012ghi' 1 1
u'abc\015def' 1 1
u'abc def\u8112ghi\uD7FFjkl' 1 1
u'abc\uE000def\uF123ghi\uFFFDjkl' 1 1
u'abc\uD800\uDC00def\uD84D\uDC56ghi\uDBC4\uDE34jkl' 1 1
Testing one char at a time ...
u'\000\005\010\013\014\016\020\031\uD800\uD900\000\uDC00\uDD00
\uDFFF\uFFFE\uFFFF'
OK
u'\011\012\015 \u2345\uD7FF\uE000\uE876\uFFFD\uD800\uDC00\uD808
\uDF45\uDBC0\uDC00\uDBFF\uDFFF\uD800\uDC00'
OK
이제 파이썬 2.3에서, 나의 머신에서는 \U를 단일 문자로 저장한다::
** BAD VALUES **
u'abc\x01def' False 0
u'abc\x0cdef' False 0
u'abc\x15def' False 0
u'abc\ud900def' False 0
u'abc\udddddef' False 0
u'abc\ufffedef' False 0
u'abc\ud800' False 0
u'\udc00' False 0
** GOOD VALUES **
u'abc\tdef\nghi' True 1
u'abc\rdef' True 1
u'abc def\u8112ghi\ud7ffjkl' True 1
u'abc\ue000def\uf123ghi\ufffdjkl' True 1
u'abc\U00010000def\U00023456ghi\U00101234jkl' True 1
Testing one char at a time ...
u'\x00\x05\x08\x0b\x0c\x0e\x10\x19\ud800\ud900\x00\udc00\udd00
\udfff\ufffe\uffff'
OK
u'\t\n\r \u2345\ud7ff\ue000\ue876\ufffd\U00010000\U00012345
\U00100000\U0010ffff\U00010000'
OK
u'abc\x01def' False 0
u'abc\x0cdef' False 0
u'abc\x15def' False 0
u'abc\ud900def' False 0
u'abc\udddddef' False 0
u'abc\ufffedef' False 0
u'abc\ud800' False 0
u'\udc00' False 0
** GOOD VALUES **
u'abc\tdef\nghi' True 1
u'abc\rdef' True 1
u'abc def\u8112ghi\ud7ffjkl' True 1
u'abc\ue000def\uf123ghi\ufffdjkl' True 1
u'abc\U00010000def\U00023456ghi\U00101234jkl' True 1
Testing one char at a time ...
u'\x00\x05\x08\x0b\x0c\x0e\x10\x19\ud800\ud900\x00\udc00\udd00
\udfff\ufffe\uffff'
OK
u'\t\n\r \u2345\ud7ff\ue000\ue876\ufffd\U00010000\U00012345
\U00100000\U0010ffff\U00010000'
OK
보시다시피 두 파이썬 버전 모두 같은 응답을 돌려준다 (단 Python 2.0은 True/False 대신에 1/0을 사용한다). 그러나 맨끝에서 repr() 코딩을 보시면 두 버전이 \U를 서로 다르게 표현하고 있다. 앞서 정의한 usplit() 함수를 사용하는 한, 코드에서 아무 차이도 보이지 않을 것이다.
좋다. 그래서 이제 어떤 문자들은 XML 파일에 둘 수 없다는 결론을 내렸다. 어떻게 이 문제를 처리할 것인가? 어쩌면 그 불법의 값들을 XML 개체로 인코드할 수 있지 않을까?
xml = u'<?xml version="1.0" encoding="utf-8" ?>'
# try to cheat and put \u0019 as an entity ...
xml += u'<H>  </H>'
# encode as UTF-8
utf8_string = xml.encode( 'utf-8' )
# parse it
from xml.dom.minidom import parseString
parseString( utf8_string )
# try to cheat and put \u0019 as an entity ...
xml += u'<H>  </H>'
# encode as UTF-8
utf8_string = xml.encode( 'utf-8' )
# parse it
from xml.dom.minidom import parseString
parseString( utf8_string )
이를 실행하면 다음과 같이 출력된다::
Traceback (most recent call last):
File "t10.py", line 11, in ?
parseString( utf8_string )
File "c:\py23\lib\xml\dom\minidom.py", line 1929, in parseString
return expatbuilder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 940, in parseString
return builder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 223, in parseString
parser.Parse(string, True)
xml.parsers.expat.ExpatError: reference to invalid character number: line 1, column 43
File "t10.py", line 11, in ?
parseString( utf8_string )
File "c:\py23\lib\xml\dom\minidom.py", line 1929, in parseString
return expatbuilder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 940, in parseString
return builder.parseString(string)
File "c:\py23\lib\xml\dom\expatbuilder.py", line 223, in parseString
parser.Parse(string, True)
xml.parsers.expat.ExpatError: reference to invalid character number: line 1, column 43
안된다! XML 1.0 표준에 의하면, 그 불법 문자들이 허용되지 않는다. 거기에서 별별 수를 다 써도 안된다. 사실, 해석기가 불법 문자들을 하나라도 허용하면, 정의대로라면 그 해석기는 XML 해석기가 아니다. XML의 핵심 아이디어는 해석기에 "용서"를 허용하지 않는 것이다. HTML 세계에 존재하는 지저분한 비호환성을 피하기 위해서 말이다.
- 그래서 어떻게 그 불법 문자들을 처리할까?
-
문자들이 불법이라는 사실 때문에, 그를 다룰 표준적인 방법은 없다. 불법 문자를 표현하기 위하여 또다른 방법을 찾는 것은 XML 저자에게 (또는 어플리케이션에) 달려 있다. 어쩌면 미래의 XML 표준에서는 이 상황에 접근하는데 도움을 줄 수 있을 것이다.
유니코드와 네트워크 공유 (삼바)
Samba 3.0 이상은 이름이 유니코드인 파일을 공유할 수 있다. 사실, 테스트는 예기치 못하게 실행되었다: 나는 그냥 (리눅스 머신에서) 삼바 공유를 윈도우즈 클라이언트에서 열었다. 그 안에 든 그리스어-문자로된 파일이름을 가진 폴더를 열었고, 그 결과는 아래와 같다:
이것이 잘 작동하지 않았다면 좀 더 복잡한 설정절차가 있었을 테지만, 나에게는 아무 것도 필요없었다. 삼바는 기본 값이 UTF-8 코딩이다. 그래서 smb.conf 파일을 수정할 필요조차 없었다.
맺는 말
여기에서 언급하지 않은 주제가 몇가지 있지만, 나중에 추가할 생각이다. 그 중에는:
- 독자적으로 코딩 변환을 정의함으로써, "불법적 XML 문자" 문제를 임시-처리하는 법을 보여주는 몇가지 예제들.
- os.listdir(u'.')이 비-유니코드 문자열을 돌려주는 것이 완전히 가능하다 (파일이름이 현재 로케일에 합법적인 코딩을 가지고 저장되지 않았다는 뜻이다). 문제는 /a-legal/b-illegal/c-legal과 같이, 합법적 이름과 불법적 이름을 혼용할 경우, os.path.join()을 사용하여 유니코드 부분과 비-유니코드 부분을 결합할 수 없다는 뜻이다. 왜냐하면 올바른 파일이름이 아니기 때문이다 (위의 예제에서, b-illegal이 유효한 유니코드 코딩을 가지고 있지 않기 때문이다). 내가 발견한 유일한 해결책은 각 경로 요소에 한 번에 하나씩 os.chdir()을 적용하는 것이었다. 파일을 열거나 디렉토리를 순회하거나 등등에 말이다. 이 문제에 관하여 섹션을 덧붙일 필요가 있다.
각주:
- 내 생각으로는, 만약 파이썬의 유니코드를 지원한 저자가 단순히 "기본 ASCII" 로직을 생략했더라면, 훨씬 더 명료했을 것이다. 그래야 초보자가 무슨 일이 일어나는지 이해하지 않을 수 없을테니까 말이다.
명시적으로 코딩하지 않고, 맹목적으로 unicode(value)를 사용하는 대신에 말이다.
이제 공정을 기하기 위해, ASCII를 기본 인코딩으로 사용하는 것은 합리적이다. 파이썬의 ASCII 코덱은 오직 0-127 사이의 코드만 받아 들이기 때문에, unicode()가 작동해야 한다면, ASCII가 거의 올바른 코덱이다. - (95/98 시기의) 더 앞의 버전은 어떻게 하는지 모르겠다. 그러나 그의 유니코드 지원은 현재 표준에 이르지 못할 것이라고 짐작한다.
-
실제로, 파이어폭스와 인터넷 익스플로러는 올바른 <meta> 태그가 없어도 올바르게 페이지를 화면에 표시할 수 있겠지만, 일반적으로 항상 올바른 <meta> 태그를 포함시키는 것이 좋다. 왜냐하면 자동-추측이 모든 플랫폼에서 또는 모든 HTML 문서에 대하여 잘 작동하는 것은 아니기 때문이다.
이 문서에 관하여 ...
저자: 프랭크 맥잉베일(Frank McIngvale)
Version: 1.3
Last Revised: Apr 22, 2007
한글판 johnsonj 2008.05.09 금
Version: 1.3
Last Revised: Apr 22, 2007
한글판 johnsonj 2008.05.09 금
Written in WikklyText.













