今川館

都内勤務の地味OLです

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() の逆です。引数が unicodePython が 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

f:id:imagawa_yakata:20161216105738p:plain

★El CapitalのMac

f:id:imagawa_yakata:20161216105742p:plain

El CapitanのMacでダメなのは、Pythonのビルドの条件のせいらしい。(以下)

Pythonでサロゲートペアの範囲の数値実体参照を文字に戻す - 西尾泰和のはてなダイアリー

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')
𩸽