HOME > 電算 > Python と文字コード

Python と文字コード

Python で行き当たりばったりに日本語処理をしてきたが、これではいかんと思っていくらかまとめてみた。(当然、このメモには間違いや誤解があろうと思う)

「python は『abc』を使うのが基本で『あいう』を使うのが例外だ」と考えると、「あいう」に対してできないことが多すぎて難しく見える。しかし、「『あいう』に対して使えるようなやり方なら『abc』にも問題なく使えて、ただ、『abc』には特権的に無作法な書き方が許されているのだ」と考えれば、ずっと簡単に理解できるように思う。

まとめ
ユニコードは「文字コード」(バイト表現と文字の対応関係)ではない
utf-8 は「文字コード」である
Python の unicode 型は「文字コード」にとらわれない型で、CPU やメモリ上で用いられる
Python の str 型は「文字コード」に則ったバイト列で、出入力時にはこれを用いる

unicode 型オブジェクトの作成

「あ」のユニコードのコードポイントは 16 進で 3042 であり、10 進にすると 12354 である。 この 12354 を使って、「あ」を表す unicode 型を作ると、

>>> unichr(12354)
u'\u3042'

となる。このオブジェクトの型を確かめてみる。

>>> import types
>>> type(unichr(12354))
< type 'unicode'>

たしかに unicode 型である。

ターミナルへの出力

ためしに、この unicode 型を対話環境からターミナルに出力してみる。

>>> import sys
>>> sys.stdout.write(unichr(12354))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: 
ordinal not in range(128)

エラーが出た。エラーの意味はおいおい見ていくことにして、ここでは unicode 型はそのまま出力できないと考えておく。

これは、

>>> sys.stdout.write(unichr(12354).encode('utf-8'))
あ

とすると出力できる。

ターミナルなりファイルなりに出力するためには、unicode 型では駄目で、具体的なコーディングシステムに則ったバイト列にエンコードしてやらなくてはならない。私のターミナルは utf-8 だったので、ここでは utf-8 にエンコードした。

対話環境で print 文に unicode 型オブジェクトを与えると、適宜うまいバイト列にエンコードしてターミナルに出力する。たとえば、

>>> print unichr(12354)
あ

と正しく出力される。ただし、(少なくとも私の環境では)このやり方は、リダイレクトを使ってファイルに書き込もうとする場合にはうまくいかない。

$ echo "print unichr(12354)" > test.py
$ python test.py
$ あ
$ python test.py > test.txt
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    print unichr(12354)
UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: 
ordinal not in range(128)

私の Python は私のターミナルが utf-8 であることを知っていて、標準出力に出すときは気を利かせて utf-8 にエンコードしたが、ファイルをつくるときには、私がどういうコーディングシステムを考えているかまでは「知ったことじゃない」というわけなのだろう。(python のやつは、尻が標準出力につながっているか、ファイルにつながっているかで挙動を変えるのだ。)

もちろん、print 文は、エンコードされたバイト列も出力できる。

>>> print unichr(12354).encode('utf-8')
あ

こちらなら、リダイレクトを使ってファイルに書き込むこともできる。

ところで、バイト列にエンコードされた「あ」は、どういう種類のオブジェクトか。

>>> import types
>>> type(unichr(12354).encode('utf-8'))
<type 'str'>

str 型のオブジェクトであることがわかる。「str 型は 1 バイト文字用」というのは誤解である。

ユニコード型オブジェクトは、utf-8 などでエンコードして str 型オブジェクトにしてからでないと、ファイルに出力することができないというのが原則と考えることにする。

ターミナルからの入力

ターミナルからリテラルを入力すると、そのターミナルのコーディングシステムにしたがったバイト列が得られる。

>>> 'あ'
'\xe3\x81\x82'

私のターミナルは utf-8 なので、ひらがな一文字に対して 3 バイトのバイト列が入力された。 このバイト列から unicode 型のオブジェクトを作成するには、

>>> unicode('あ',encoding='utf-8')
u'\u3042'

のようにする。ここで、encoding='utf-8' というのは、第一引数 'あ' が utf-8 でエンコードされたバイト列であると考えたうえで unicode 型のオブジェクトに変換せよ、という指示である。

python では、このように、バイト列を unicode 型のオブジェクトに変換することをデコードするという。(デコードするときに encoding を指定するので、ちとややこしい)

unicode 型 ←デコード ← str 型(バイト列)
→エンコード→

なお、上でやっていることは、

>>> 'あ'.decode('utf-8')
u'\u3042'

とやっても同じである。

さらに、私の対話環境では、ターミナルからの入力が utf-8 であることが仮定されているので、たんに、

>>> u'あ'
u'\u3042'

とやっても同じ結果が得られる。

ファイルからの出入力

以上、utf-8 のターミナルから、標準出入力を用いていろいろ調べてみた。ファイルへの出入力も基本的にはこれと変わらない。たとえば、

>>> import codecs
>>> f=codecs.open('a.txt', 'r', 'utf-8')
>>> l=f.readline()
>>> f.close()
>>> l
u'\u3042\n'
>>> type(l)
<type 'unicode'>
>>> print l.encode('utf-8')
あ

のようにすると、ファイルのコーディングシステムを指定して開くことができ、その結果ファイルを読み取ってできたオブジェクトは自動的に unicode 型にデコードされる。

また、プログラムファイルから読み取られるリテラルは、冒頭のあたりで当該ファイルのコーディングシステムとして指定したところに則って解釈される。

$ cat test.py
#coding: euc-jp
print type('あ')
print type(u'あ')
print u'あ'.encode('utf-8')

という utf-8 のファイルがあったとする。冒頭に #coding: euc-jp という誤りをわざと入れてある。そのまま実行すると、

$ python test.py
  File "test.py", line 2
SyntaxError: 'euc_jp' codec can't decode bytes in position 12-13: illegal multibyte sequence

エラーが出る。そこで、このプログラムファイルを、冒頭のコーディングシステム指定と同じ euc に変換してから実行してみる。

$ nkf -e test.py > test_euc.py
$ python test_euc.py
<type 'str'>
<type 'unicode'>
あ

euc で書かれたリテラル 'あ'は euc によってエンコードされたバイト列(str 型)になり、u'あ' は unicode 型にデコードされている。上の例の最後のところで、この unicode 型を再び utf-8 にエンコードして、utf-8 のターミナルに表示させている。

ASCII 特権

もともと Python は ASCII 文字列だけを扱っていたので、いまだに ASCII に定義のある文字は、日本の文字に比べると、さまざまな省略をして使うことができる。

コードポイントと呼ばれる整数で unicode 型オブジェクトを作る場合を考える。もし「あ」を作るならば

>>> unichr(12354)
u'\u3042'

のように数字が表示されるが、「a」を作ると、

>>> unichr(97)
u'a'

人が見てわかる文字が表示される。

対話環境でリテラルを入力したとき、日本語だと

>>> 'あ'
'\xe3\x81\x82'

のように、バイト並びを示すものがエコーされたが、これが ASCII に定義のある字だと

>>> 'a'
'a'

のように人が見てわかるものが表示される。

下のように、「あ」の unicode 型オブジェクトを標準出力に write するとエラーを起こすが、

>>> sys.stdout.write(unichr(12354))
Traceback (most recent call last):
  File "<stdin>", line 1, in ≶module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128)

「a」の unicode 型オブジェクトを標準出力に write すると、勝手にデコードされて、うまく表示される。

>>> sys.stdout.write(chr(97))
a

リテラルとして入力したバイト並びを unicode オブジェクトにするときに、文字が日本語だと

>>> unicode('あ')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)

のようにやっては駄目で、

>>> unicode('あ',encoding='utf-8')
u'\u3042'

のように第一引数のコーディングシステムを教えてやらないと駄目だが、これが ASCII に定義のある範囲だと、

>>> unicode('a')
u'a'

のように黙って処理してくれる。

ASCII 特権のひとつに chr() 関数がある。これは、ASCII のコード(整数)を引数として、いきなりバイト列を作成する。引数は 0 から 127 までに限定されている。

>>> chr(97)
'a'

これと同じようなことを、「あ」でやろうとすると、

>>> unichr(12354).encode('utf-8')
'\xe3\x81\x82'

とするしかない。

+ 演算子を用いてユニコード型オブジェクト同士を連結したり、エンコードされたバイト列同士を連結できたりする。

ためしに、ASCII 範囲にあるバイト列と unicode 型オブジェクトを連結してみる。

>>> chr(97) + unichr(12355)
u'a\u3043'

この行儀の悪いコードがエラーを出さないのも ASCII 特権だろう。

もしバイト列が「a」ではなく「あ」であったなら

>>> unichr(12354).encode('utf-8') + unichr(12355)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: 
ordinal not in range(128)

このようにエラーが出る。

文字列の長さを知ろうとして len() 関数を使うには、ひらがな等は unicode 型にしておかないと駄目だが、ASCII なら大丈夫というのも当然といえば当然ながら、ASCII の特権のひとつだろう。

挙げていくときりがないが、とにかく ASCII にしか認められない特権を使ったコードに慣れてしまうと、日本語の扱いが例外的に難しいもののように思える。しかし、複数バイト文字でも OK な書き方が基本で、ASCII 限定で妙な特権があるのだと考えれば(それが歴史的に見て正しい理解かどうかは別として)日本語の使いこなしが簡単に思えるのではなかろうか。

実用の場面で起こる問題を見てみる(1) sqlite3

コーディングシステムの問題が出てくるのは、標準出入力やファイルだけではない。ライブラリを読み込んで使ったようなときに、エンコードされた str 型オブジェクトとして文字列をやりとりするのか、unicode 型オブジェクトとしてやりとりするのかという問題がしじゅう起こる。

sqlite3 は、簡単な SQL データベースで、python でもモジュールを組み込んで簡単に使えるので普及している。

次の例は、unichr(12354) で作成した unicode 型オブジェクト「あ」をデータとしてテーブルに挿入しているところ

import sqlite3
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("create table test (u)")
cur.execute("insert into test(u) values (?)",(unichr(12354),))
cur.execute("select * from test")
r=cur.fetchone()
print r[0]
print type(r[0])

select では、unicode 型オブジェクトが得られる。

ここで、unichr(12354) の代わりに unichr(12354).encode('utf-8') とやって、utf-8 でエンコードされた str オブジェクトを与えると、

sqlite3.ProgrammingError: You must not use 8-bit bytestrings unless you use a text_factory that can interpret 8-bit bytestrings (like text_factory = str). It is highly recommended that you instead just switch your application to Unicode strings.

というエラーが出る。バイト列を挿入してはいけない(ユニコード型のオブジェクトを挿入せよ)というのである。ただし、chr(97) のように、ASCII の範囲のバイト列を使うとエラーは出ない(たぶん latin-1 までくらいは平気かな)。この場合にも select したときには unicode 型オブジェクトが得られる。

実用の場面で起こる問題を見てみる(2) tweepy

コーディングシステムの問題が出てくるのは、標準出入力やファイルだけではない。ライブラリを読み込んで使ったようなときに、どんなオブジェクトが得られるのかという問題がしじゅう起こる。

tweepy は twitter の API を扱うためのライブラリである。twitter はユニコードに対応しているのだが、tweepy はユニコード型オブジェクトを得るのだろうか、それともバイト列を得るのだろうか。これを見てみる。

#!/usr/bin/env python
import tweepy
sqlite3file='/home/hoshino/diary/twitter_log.sqlite'
credential = {
    'CONSUMER_KEY': 'xxxxxxxxxxxxxxxxxxxxx',
    'CONSUMER_SECRET': 'xxxxxxxxxxxxxxxxxxxxx',
    'ACCESS_KEY': 'xxxxxxxxxxxxxxxxxxxxx',
    'ACCESS_SECRET': 'xxxxxxxxxxxxxxxxxxxxx'}
maxcount=-1
auth = tweepy.OAuthHandler(credential['CONSUMER_KEY'], credential['CONSUMER_SECRET'])
auth.set_access_token(credential['ACCESS_KEY'], credential['ACCESS_SECRET'])
api = tweepy.API(auth)
t=api.home_timeline(count=1)
print type(t[0].author.screen_name)
print type(t[0].text)

スクリーンネームは str 型で、投稿内容は unicode 型で得られている。

Python3

Python における文字の取り扱いは、Python3 で劇的に変わる。簡単にいうと、これまでの str 型がなくなり、unicode 型に統一されるような具合である。当然後方互換性がなくなるので、普及にはいましばらく時間がかかるかもしれない。

参考にしたページ

http://www.python.jp/doc/2.5/lib/encodings-overview.html
http://docs.python.org/release/3.0.1/howto/unicode.html
(和訳 http://www.geocities.jp/tan9ent/unicode.html)
(Python3 の場合)
http://d.hatena.ne.jp/fgshun/20090901/1251818730

――目次――
HOME雑文写真壁紙馬鹿読書語学
│├英語
│└日本語電算地理
│└白地図ブログ