Mac OS X の標準の Python 2 は UCS-2 でビルドされているため、Linux ディストリービョンで広く採用されている UCS-4 の場合と比べて、標準関数の len や unichr が返す値が異なることがある。
UCS-2 のビルドオプションでのふるまい
ビルドオプションが UCS-2 である場合、U+10000 とそれ以降の文字が含まれる場合、len をそのまま使って文字数を求めることはできない。homebrew で導入した場合も USC-2 でビルドされる。
ビルドオプションに UCS-2 が指定されたかどうかを調べるには sys.maxunicode の値を使う。
>>> import sys
>>> 0xFFFF == sys.maxunicode
True
次の文字列 (U+20BB7 U+91CE U+5BB6) に len を適用すると、戻り値は4になる。
>>> str = u'?野家'
>>> 4 == len(str)
True
U+20BB7 の内部表現はサロゲートペアの U+D842 U+DFB7 となる。
>>> 0xD842 == ord(str[0])
True
>>> 0xDFB7 == ord(str[1])
True
UCS-2 を考慮して文字数を求める
上位サロゲートの範囲は U+D800 から U+DBFF までであることを踏まえて、文字数を求めてみよう。コードをかんたんにするために、上位サロゲートもしくは下位サロゲートが孤立する場合は考慮しない。UCS-4 であれば、for ループを使うことができる。
# -*- coding: utf-8 -*-
import sys
def utf8_len(str):
length = 0
if sys.maxunicode > 0xFFFF:
for c in str:
length += 1
return length
code_units = len(str)
pos = 0
cp = -1
while pos < code_units:
cp = ord(str[pos])
length += 1
if cp > 0xD7FF and 0xDC00 > cp:
pos += 2
else:
pos += 1
return length
先ほどの文字列を再度試してみよう。
str = u'?野家'
print(3 == utf8_len(str))
練習として、少しコードを修正して、1文字ずつコールバックを適用する関数を定義してみよう。
# -*- coding: utf-8 -*-
import sys
def utf8_each_char(str, func):
if sys.maxunicode > 0xFFFF:
for c in str:
func(c)
else:
code_units = len(str)
pos = 0
buf = ''
cp = -1
while pos < code_units:
buf =str[pos]
cp = ord(buf)
if cp > 0xD7FF and 0xDC00 > cp:
buf += str[pos+1]
func(buf)
pos += 2
else:
func(buf)
pos += 1
1文字ずつ表示してみよう。ラムダ式で print を使うには print_function をファイルの冒頭でインポートする必要がある。
from __future__ import print_function
str = u'?野家'
f = lambda c: print(c)
utf8_each_char(str, f)
UCS-2 を考慮してコードポイントから文字を生成する
USC-2 の制約はコードポイントの整数から文字を生成する unichr も受け、0x10000 とそれ以降の整数を受けつけない。
>>> unichr(0x20BB7)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: unichr() arg not in range(0x10000) (narrow Python build)
Unicode エスケープシーケンスは UCS-2 の影響を受けない。
>>> print(u"\U00020BB7")
?
UCS-2 の制約を考慮したユーザー関数を定義すると次のようになる。
# -*- coding: utf-8 -*-
import sys
def utf8_chr(cp):
if 0xFFFF < sys.maxunicode or cp < 0x10000:
return unichr(cp)
cp -= 0x10000
high = cp >> 10 | 0xD800
low = cp & 0x3FF | 0xDC00
return unichr(high) + unichr(low)
print(utf8_chr(0x20BB7))
print(utf8_chr(0x91CE))