UCS-2 か UCS-4 かによって len や unichr の戻り値が変わることがある

  • 1
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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