Pythonでサロゲートペア -- ほっけの逆襲
Pythonでサロゲートペアを処理するとどうなるか
http://qiita.com/YusukeHirao/items/2f0fb8d5bbb981101be0
既に他の記事で上記記事に登場する「𩸽」(ほっけ)というサロゲートペアの文字をGoで扱う話しを書いたが、Pythonで処理するとどうなるのか試してみた。
Python2は寛容
Python2は3に比べて寛容である。printに渡しても取り敢えずエラーにならず表示される。
>>> print u'\uD867\uDE3D' 𩸽 >>> x = u'\uD867\uDE3D' >>> y = u'𩸽'
但し、表示される文字は同じだが、文字列比較では同値ではないと評価される
>>> x == y False
更に不思議なことに、こちらはordが結果を返すのに
>>> ord(y) 171581 こちらはエラーになってしまう >>> ord(x) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: ord() expected a character, but string of length 2 found
http://docs.python.jp/2/library/functions.html#ord
ord(c)(原文)
長さ 1 の与えられた文字列に対し、その文字列が unicode オブジェクトならば Unicode コードポイントを表す整数を、 8 ビット文字列ならばそのバイトの値を返します。たとえば、 ord('a') は整数 97 を返し、 ord(u'\u2020') は 8224 を返します。この値は 8 ビット文字列に対する chr() の逆であり、 unicode オブジェクトに対する unichr() の逆です。引数が unicode で Python が UCS2 Unicode 対応版ならば、その文字のコードポイントは両端を含めて [0..65535] の範囲に入っていなければなりません。この範囲から外れると文字列の長さが 2 になり、 TypeError が送出されることになります。
サロゲートペアはlenで長さを調べると、「2」が返却される。
※unicode型をlenに渡しているので「文字数」を取得するはずが、「2」を返す点が問題。
str型をlenに渡すとバイト数を返すのでここでの問題対象外。
>>> len(x) 2 >>> len(y) 1
参考:
>>> len(u'あ') # unicode型: 文字数 1 >>> len('あ') # str型: バイト数 3
「Unicode番号から文字を生成する」
当然、chrに渡すと範囲外なのでエラー。
>>> chr(0x29E3D) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: chr() arg not in range(256
unichrは正しく値を返してくれる。
>>> unichr(0x29E3D) u'\U00029e3d' >>> print unichr(0x29E3D) 𩸽
しかも、他の文字を試すとUbuntu16のVMでは化けて表示されるが、Macだとエラーになった。
★Ubuntu16.04のVM
★El CapitalのMac
Python3だと扱いが難しくなる
Python3ではそもそもu'\uD867\uDE3D'をprintに渡すとエラーになる。
Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'utf-8' codec can't encode character '\ud867' in position 0: surrogates not allowed
Python3は、最初からサロゲートペアを許さないポリシーのようだ。
そのため、'surrogatepass', 'suroogateescape'といったエラーオプションを指定して何とか解決する。
具体的には以下stackoverflowの回答の通り、UTF-16でサロゲートペアを許可した状態で一度byteに変換し、strに戻すと大丈夫らしい。
How to work with surrogate pairs in Python? - Stack Overflow
>>> "\ud867\ude3d".encode('utf-16', 'surrogatepass').decode('utf-16') '𩸽' |< ちなみに、Unicodeコードポイントでリテラル表記してあげれば大丈夫である。 >|text| >>> print('\U00029e3d') 𩸽