7
8
個人開発エンジニア応援 - 個人開発の成果や知見を共有しよう!-

【Python】高速で省メモリな新しい日本語全角/半角変換モジュール Habachen

Last updated at Posted at 2023-10-04

はじめに

日本語の全角/半角を相互変換する Python のライブラリやスニペットは様々なものが公開されています。 Pure Python のライブラリですと、 jaconv が有名ですね。最近だと、 Utsuho が活発に開発されているようです。異字体セレクタも考慮されている点が、これまでのライブラリには無かった特徴だと思います。

ネイティブ拡張を使った CPython 用の高速な日本語の全角/半角変換モジュールと言えば、 mojimoji が挙げられますが、 issue が立てられてから1年以上アクティビティがないということもあり1、文字コードの勉強も兼ねて自分で同等のものを実装してみることにしたのですが、その内にいつの間にか mojimoji よりも速いものが出来ていたので、ここに公開したいと思います。

Habachen について

文字の幅をチェンジするから「はばちぇん」です。なお、後からひらがな・カタカナの変換機能も追加したため、ライブラリ全体の実態に合わなくなってしまっています。正直、自分でもどうなんだろうというネーミングですが、開発途中から仮称として自分の中で使っていて、他に良さそうな候補も思いつかなかったので、正式名称と相成りました。C 言語で書かれています。

特徴

  • Pure Python 実装より速いのはもちろん、Cython と C++ のunordered map を用いた mojimoji よりも高速に動作します。
  • 全角/半角に加え、ひらがな/カタカナの相互変換もサポートしています。
  • 他のサードパーティーライブラリに依存しません。

注意点

  • C コンパイラが必要(な場合がある)
    バージョン 0.4.0 から、Windows, macOS, Linux 用の wheel (ビルド済みパッケージ)の提供を始めました!(2023/11/2 追記)
    環境によっては、ビルドのために C コンパイラや CPython ヘッダファイル Python.h を別途導入しておく必要があります。
  • ラテン1補助に含まれる文字はサポートしていない
    それに伴い、全角の円マーク '¥' (U+FFE5) と半角の円マーク '¥' (U+00A5) は相互変換されないことに注意してください。

インストール方法

pip install habachen

利用できる関数

4つの関数実装に対して、それぞれ3種類の名前が用意されています。基本的に、有名どころの日本語文字列変換ライブラリと同じ感覚で使用できるようになっています。

  • habachen.zenkaku_to_hankaku()
  • habachen.to_hankaku()
  • habachen.zen_to_han()
    全角を半角に変換します。シグネチャは以下の通りです:
def zenkaku_to_hankaku(
        text: str, /, *,
        ascii: bool = True, digit: bool = True, kana: bool = True) -> str:
    ...
  • habachen.hankaku_to_zenkaku()
  • habachen.to_zenkaku()
  • habachen.han_to_zen()
    半角を全角に変換します。シグネチャは以下の通りです:
def hankaku_to_zenkaku(
        text: str, /, *,
        ascii: bool = True, digit: bool = True, kana: bool = True) -> str:
    ...
  • habachen.hiragana_to_katakana()
  • habachen.to_katakana()
  • habachen.hira_to_kata()
    ひらがなをカタカナに変換します。第二引数 ignore で無視する文字を指定できます。シグネチャは以下の通りです:
def hiragana_to_katakana(
        text: str, /,
        ignore: Iterable[str] = '', *, hankaku: bool = False) -> str:
    ...
  • habachen.katakana_to_hiragana()
  • habachen.to_hiragana()
  • habachen.kata_to_hira()
    全角カタカナをひらがなに変換します。第二引数 ignore で無視する文字を指定できます。シグネチャは以下の通りです:
def katakana_to_hiragana(
        text: str, /, ignore: Iterable[str] = '') -> str:
    ...

※半角カタカナからひらがなへの直接変換はサポートしていません。

使用例

>>> import habachen
>>> habachen.zen_to_han('abc123ゼンカク')   # 全角→半角
'abc123ゼンカク'
>>> habachen.han_to_zen('abc123ハンカクモジ')         # 半角→全角
'abc123ハンカクモジ'
>>> habachen.to_katakana('ひらがな')             # ひらがな→カタカナ
'ヒラガナ'
>>> habachen.to_hiragana('カタカナ')             # カタカナ→ひらがな
'かたかな'

ドキュメント

ベンチマーク

本当に速いの?ということで簡単なベンチマークを掲載します。結果はこちら

ここでの計測に使われているものと同じデータを用います。

実行は、 Google Colab 上で行います。テキストデータは、対象の zip ファイルを一旦ローカル環境に落として解凍したものをストレージにアップロードして利用しています。
ベンチマークの方法についてです。マジックコマンドの %timeit は手軽に使えるのですが、どうも現行の仕様だと不適切な時間の測り方をしているようで(参考)、今回は標準ライブラリの timeit.repeat() を直接呼び出して、その返り値の中から最小のものを採用することにします2 3

使用する文章

夢野久作『ドグラ・マグラ』
http://www.aozora.gr.jp/cards/000096/files/2093_ruby_28087.zip

Python 本体のバージョン

Python 3.10.12

ライブラリのバージョン

  • habachen-0.3.0
  • mojimoji-0.0.12
  • jaconv-0.3.4

※ mojimoji はネイティブ拡張による実装 (Cython と C++)、jaconv は Pure Python 実装。

結果(短文 140文字)

単位はマイクロ秒 (µs) に統一してあります。

habachen mojimoji jaconv
全角→半角 1.319 µs 11.92 µs 11.22 µs
半角→全角 1.147 µs 10.15 µs 26.49 µs
ひらがな→カタカナ 0.3674 µs 11.22 µs
カタカナ→ひらがな 0.3542 µs 10.97 µs

結果(全文 468996文字)

単位はミリ秒 (ms) に統一してあります。

habachen mojimoji jaconv
全角→半角 2.607 ms 55.07 ms 40.36 ms
半角→全角 1.832 ms 33.89 ms 57.16 ms
ひらがな→カタカナ 0.711 ms 38.72 ms
カタカナ→ひらがな 0.755 ms 40.36 ms

全角→半角で、 mojimoji が jaconv よりも遅い結果になっているのが気になりますが、何度か試してみても同じような傾向でした4

データの性質や実行する環境によっては大きく異なる実行結果が出ることも考えられますが、少なくとも今回行ったベンチマークにおいては、短文・全文いずれにおいても、 habachen は他の文字列変換ライブラリの数倍から数十倍の速度で処理できることが分かりました。

なんで速いの?

高速化のために行ったことを手短に紹介します。特に目新しいことはないと思います。正直まだ最適化の余地はありそうな感じです。

  • unordered map を使わない

mojimoji では unordered map を使うことで Python 実装よりも高速化していますが、 habachen では unordered map を使わない 5ことで高速化を実現しています。

Python のオブジェクトを格納する必要がない場合、 unboxing の必要がない分、STL のコンテナは Python のものよりも高速に動作しますが、それでもオーバーヘッドは存在します。また、C++ の unordered map はハッシュテーブルとして必ずしも効率的な実装ではないというのもあります。

Habachen では、変換後の文字コードがコードポイントの単純な差分の計算だけで求まる場合にはこれを行い、そうでない場合には、文字の対応に関する情報を格納した静的配列を参照することで、テーブルの間接参照によるオーバーヘッドを削減しています。

  • 文字列型オブジェクトにインプレースで書き込む

ネイティブ拡張を作る際に Cython や pybind11 を活用することで、CPython の実装詳細に由来する煩わしさはかなり軽減できるんですが、一方で、 CPython API を直接叩くことにより他では難しい最適化が可能になることもあります。

Python の文字列型 (str) はイミュータブルですが、C レイヤーにおいて、他から参照されていない場合に限り安全に書き換えることが出来ます。入力と出力のサイズが同じケースでは、一時バッファを作らず、戻り値となる文字列型オブジェクトに直接変換後の値を入れていくことで、余計なメモリアロケーションとコピーを回避でき、若干の効率化が期待できます。ただし、最終的に出来上がったオブジェクトが CPython 文字列として「正しい」ことを拡張ライブラリ側で保証する必要があります。

含まれる文字集合に応じて、CPython の文字列型オブジェクトはいくつかの内部表現を持ちますが、Habachen では「日本語の文章に含まれる文字コードの多くは U+3000~U+FFFF の範囲内に収まっている」という一般的な傾向に基づき、出力となる文字列データの内部表現は UCS2 であると最初に仮定し、問題が無ければそのように扱います。いわゆるヒューリスティックですが、うまくいけば無駄なコピーを抑えつつ 1 パスで変換を完了できます。

最後に

半角/全角の変換は、多くの場合、 Pure Python を使ったコードでも十分高速ですし、自然言語処理だと他の部分が多くの時間を費やしていて、前処理の速度を改善しても、全体としてはほとんど時間短縮にならなかったりすることもあるかと思います。

とはいえ、同じ内容の処理であれば、速ければ速いほど良いというのもまた事実だと思いますし、気になった方は Habachen を試してもらえたらと思います。

外部リンク

  • PyPI

  • GitHub

  1. メモリ関係のバグがあるのも気になっていたところです。 詳しくはこちら(2023/10/15 追記)。

  2. Python CLI の timeit で使われているのと同じ手法です。

  3. 検証に使ったスクリプトはこちら

  4. 環境によっては、普通に mojimoji の方が jaconv よりも速いようです。

  5. 最終的に C++ ではなく C で書いています。

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8