概要 Python の codecs
モジュールの IncrementalDecoder
を使えば、送られてくるバイト列を、文字境界を気にすることなく逐次デコードしていくことができます。
ソケット、シリアル通信、パイプなどを使ってテキストのデータを送受信することはよくある。このとき、テキストは当然のことながら、なんらかのエンコーディング(符号化方式)でバイト列として表現されてストリームを流れてゆく。
たとえば、u'モンティ・パイソン'
という文字列を送信したいとする。エンコーディングは Shift_JIS にしよう。Python インタプリタを立ち上げて確認してみると、送信するバイト列は次のようになる。
C:\...>python Python 2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)] on win 32 Type "help", "copyright", "credits" or "license" for more information. >>> s = u'モンティ・パイソン' >>> x = s.encode('shift_jis') >>> x '\x83\x82\x83\x93\x83e\x83B\x81E\x83p\x83C\x83\\\x83\x93' >>>
この '\x83\x82\x83\x93\x83e\x83B\x81E\x83p\x83C\x83\\\x83\x93'
というのがエンコードされた文字列だ。
さて、ここでソケットやシリアル通信などのストリームを受信するときに忘れてはならない事実を思い出そう。それは、受信した時点でデータのすべてがそろっているとは限らないということである。今回の例では、Shift_JIS にエンコードされた u'モンティ・パイソン'
は 18 バイトのバイト列('\x83\x82\x83\x93\x83e\x83B\x81E\x83p\x83C\x83\\\x83\x93'
)になっている。一回の受信でこの 18 バイトぜんぶを受信できるとは限らないのだ。受信できるのは先頭から 10 バイトまでかもしれないし、13 バイトかもしれない。ぜんぶかもしれない。残りのバイト列はストリームから再びデータを読み取ったときに手に入る。受信したデータは、逐次文字列にデコード(復号)して、あなたのアプリケーションの表示ルーチンに渡さなければならない。
ここでいやな予感がするはずだ。先頭から 13 バイトを受信したとしたら、手元にあるのは '\x83\x82\x83\x93\x83e\x83B\x81E\x83p\x83'
というバイト列だ。これをデコードしてみると……
>>> a = x[:13] >>> a.decode('shift_jis') Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'shift_jis' codec can't decode byte 0x83 in position 12: inc omplete multibyte sequence >>>
例外が発生してしまう! バイト列の最後の '\x83'
が、Shift_JIS では単独で存在することを許されない、半端なバイトになってしまっているからだ。
このように、ひとつの文字を複数のバイト列で表すようなエンコーディングでは、受け取ったデータがかならずしも文字の境界にそろっているとは限らない。
さて、じつは Shift_JIS には、(データの最初の文字境界が正しければ)先頭の 1 バイトを見ればそれが 1 バイト表現の文字か 2 バイト表現の文字の前半分かを判別できるという特徴がある(「日本語と文字コード」)。1 バイト目が 0x81 から 0x9F または 0xE0 から 0xEF であれば、その文字は 2 バイト表現で、後続の 1 バイトとあわせてひとつの文字を表現している。それ以外であれば、制御文字か、ASCII の英数字か、半角カタカナか、いずれにしてもその 1 バイト単独でひとつの文字を表している。この知識を利用すれば、文字列の境界を見つけることはけしてむずかしくはない。先頭から受信したデータをパースする関数を書いてもいいし、正規表現でマッチングさせてもいい。半端なバイトがついてきているとわかったら、そこでデータを切り取って、あまった 1 バイトは次の受信データの先頭につなげるために取っておく。昔はみんな C でこういうコードをたくさん書いていたのではないかと思われる。
しかしこういうバッファリング機構を実装してしまうと、受信するデータのエンコーディングが変わったときに、バッファリング機構のコードもそれにあわせて書き換えなければならない。エンコーディングが Shift_JIS じゃなくて UTF-8 になったら? 「このソフト、中国語のテキストも送れるよね?」と言われたら? Big5 の仕様を調べる気になんてなる? せっかく Python でエンコードやデコードに楽をしてるのだから、ここでも Python なら、きっとなにかやってくれてるはず!……と信じて探してみたら、codecs
モジュールに IncrementalDecoder
というものが見つかった。これこそ、ほしかったものそのもので、やってほしかったことのすべてを引き受けてくれる。もっと早く知っとくべきだった。おなじ思いをほかの人には味わってほしくないので、いまこれを書いている。
そういうわけで、IncrementalDecoder
の使いかたを以下で説明します。
IncrementalDecoder
はクラスで、実際の逐次デコードにはそのインスタンスを使う。だが、まずは自分が使いたいエンコーディングを扱う IncrementalDecoder
クラスを手に入れなければならない。そのための関数が、codecs.getincrementaldecoder(encoding)
で、これは encoding を扱うコーデックを探し出して、それを処理する IncrementalDecoder
クラスを返すというものになっている。いわゆるファクトリ関数だ。このためコードはちょっと回りくどく見えるかもしれない。
>>> import codecs >>> Decoder = codecs.getincrementaldecoder('shift_jis') >>> decoder = Decoder()
モジュールをインポートしたあとは一行で次のように書いてもおなじこと。
>>> decoder = codecs.getincrementaldecoder('shift_jis')()
あとは、ストリームから受信したバイト列をなにも考えずに decoder
の decode(object[, final])
メソッドに食わせていけばよい。
>>> decoder.decode(x[:13]) u'\u30e2\u30f3\u30c6\u30a3\u30fb\u30d1' >>> decoder.decode(x[13:]) u'\u30a4\u30bd\u30f3'
このように、文字境界の途中で切れていても、つながっているところまでの文字列をデコードして返してくれる。
省略可能な第二引数の final は、それがデータの終端であるときに True
を指定する。このときに、まだ中途半端なデータが残っていると、このメソッドは UnicodeDecodeError
の例外を発生させる。
>>> decoder.decode(x[:13], True) Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'shift_jis' codec can't decode byte 0x83 in position 12: inc omplete multibyte sequence
これは自然なことといえる。
さいごに、UnicodeDecodeError
と対をなすものとして、IncrementalEncoder
というクラスと codecs.getincrementalencoder(encoding)
という関数もある。しかし、これらにはあまり用があるようには思えない。送信する側のテキストは当然送信側が完全な Unicode 文字列として保持しているはずだから。
Python 開発コミュニティと、さまざまな解説記事やサンプルコードを公開されている方々に感謝します。
内容については正確さを心がけていますが、知識不足により間違ったことを書いてしまっているかもしれません。記事の内容によって生じた損害等について作者はその責任を負いません。ご了承ください。記事に関するご指摘やご意見ご感想はいつでも歓迎です。ゲストブックやメールなどでご連絡ください。
なお、「文字」「文字の境界」といった言いかたは Unicode においては正確を期した表現ではありませんが、記事の趣旨から、ここではそれらにはあえて立ち入らないであります、ご容赦ください。
この文書は、クリエイティブ・コモンズ・ライセンスの下でライセンスされています。また記事に掲載しているサンプルコードはすべてパブリックドメインに置きます。
Copyright 2011 Masaaki Shibata <mshibata at emptypage.jp>