読者です 読者をやめる 読者になる 読者になる

ユニコード文字列が半角カナであるかどうかを判定する

Python

 id:Voluntasさん発の半角カナを判定するやり方について、id:odzさんが他のパターンを紹介されている*1ので、一応、試してみました。
 「一応」というのは、このケースでは、実行速度の面で、「正規表現>>>>超えられない壁>>>>それ以外」となることは明らかだからです。Bob Ippolitoさんが、どこかで「Cみたいに一文字一文字判定するコード書くな、ヴォケ!正規表現つかえや!(意訳)」と書いていた記憶があるのですが、すぐには見つかりませんでした。まあ、それはいいか。
 なお、以下のコードはすべて、入力値がユニコード文字列であることを前提とし、入力値が半角カナだけで構成されているかを判定しています。


 set/frozensetを使って判定するパターン。半角カナでないものが出現した時点で処理を打ち切れるので速そうですね。うんうん。

_KANA = frozenset(range(0xFF61, 0xFFA0))
def a(value):
    for c in value:
        if ord(c) not in _KANA:
            return False
    return True


 リスト内包表現/ジェネレーター式を使うパターン。id:Voluntasさん、id:odzさんが紹介しているパターンと大筋は同じ。ただ、下のコードは、同じ処理内容としては、最も短く、速いと思います。
 リスト内包表現を使ってるから、これはきっと速いよ。うんうん。

_KANA = frozenset(range(0xFF61, 0xFFA0))
def b(value):
    return not [True for c in value if ord(c) not in _KANA]


 長い文字列、半角カナの出現頻度が低い文字列ならば、ジェネレーター式を使った次のコードの方がよいかもしれません。関数型言語みたいでよい感じですね。うんうん。

_KANA = frozenset(range(0xFF61, 0xFFA0))
def b2(value):
    return not any(True for c in value if ord(c) not in _KANA)


 最後に正規表現を使うパターン。

import re
han_kana = re.compile(u'^[\uFF61-\uFF9F]+$')
def c(value):
    return han_kana.match(value) is not None


 上のa, b, b2, cをtimeitで測定してみました。

from timeit import Timer

ta = Timer(u"a(u'アアア')", "from __main__ import a, _KANA")
tb = Timer(u"b(u'アアア')", "from __main__ import b, _KANA")
tb2 = Timer(u"b2(u'アアア')", "from __main__ import b2, _KANA")
tc = Timer(u"c(u'アアア')", "from __main__ import c, han_kana")

print ta.timeit(), tb.timeit(), tb2.timeit(), tc.timeit()


 結果。

a 5.7661678791
b 5.71169304848
b2 7.8643078804
c 2.74786806107


 2倍の差が「超えられない壁」かどうかはともかく、おおむね予想通り。もっと現実に即した値を使ってベンチマークを採ると、b, b2はさらに成績が悪くなります。


【追記】

_K = frozenset([unichr(x) for x in xrange(0xFF61, 0xFFA0)])
def a2(value):
    for c in value:
        if c not in _K:
            return False
    return True

 これだと、ordを呼ばなくてよい分、多少速いのですが、やっぱり正規表現の方が速いです。そもそも、正規表現エンジンがやっていることそのものを、Pythonのコードにしているわけだから、速いわけがない。

*1:このままでは正しく動かないよ!