今まで利用していたPythonの文字列はすべてアルファベットと数字で構成されていました。今回は日本語のような「マルチバイト文字」を使ってみたいと思います。

基数

いきなり脱線なのですが、文字コードの話をする前に「基数」の話をしてしまいましょうか。私たちが普段使う10進数を思い出してください。0、1、2…… と数が大きくなっていき、8、9となると次は桁があがって10になります。このとき、ひとつの桁で表現できる数は0~9の10個であり、この1桁で表現できる数を「基数」と呼びます。そしてその基数Nで表現される数え方を「N進数」と呼びます。たとえば、0~9の10個であれば10進数、0~7の8個であれば8進数になります。

では、0~1で表現されるのは、基数が何で何進数と呼ばれているでしょうか。はい、基数が2なので、2進数ですね。コンピュータが理解可能なのは01だけという話を何度もしていますが、要するにコンピュータは2進数を使うということです。

ただ、人間は01だけで構成された数字を見せられても、あまりピンときませんよね。たとえば、適当に打った01000100010011110010という2進数の大きさがどれほどなのか、一瞬では把握できません。これを10進数に直すと279794になり、およそ28万であるということがわかります。

2進数を人が読みやすい10進数に変換するのは結構骨が折れます。たとえば、01001101という2進数を10進数に変換するには、

(1 x 2^0) + (0 x 2^1) + (1 x 2^3) + (1 x 2^4) + (0 x 2^5) + (0 x 2^6) + (1 x 2^7) + (0 x 2^8)
 = 1 + 0 + 4 + 8 + 0 + 0 + 64 + 0
 = 77

# 補足
# N^MはNのM乗という意味。べき乗をテキストで表現できるので便利な書き方
# Python的に書くとN ** M
# 2^0 は1, 2^1は2, 2^2は4,……, 2^8は128

というように、各桁の値(0 or 1)に2 の“桁-1”乗をかけた値を足しあわせていきます。10進数から2進数への変換はこの逆で、割り算を繰り返すようなことをするのですが、省略します(笑)。調べればいくらでも情報は出てくるはずです。演習を頑張ってみてください。

以上のように「2進数は桁が大きくなりわかりづらい」ものの「10進数は2進数と相性が悪い」という問題があるので、コンピュータでは2進数と相性がよい16進数がよく使われます。0~9までの数字だと16パターンを表現できないので、アルファベットを使って、

0
1
2
…
8
9
A : 10
B : 11
C : 12
D : 13
E : 14
F : 15
10
11
…

として16パターンの数を表現します。この16進数は2進数と非常に相性がよく、2進数の4桁をちょうど1桁で表現できます。0000~1111は10進数でいうと0~15なので、ちょうど16進数の0~Fにピッタリあてはまるのです。1バイトは8ビット、つまり01が8桁なので、16進数2桁で表現できます。

最後に、変換に便利なPythonの関数を紹介します。10進数から2進数、8進数、16進数への変換は専用の関数を使います。

>>> bin(100)  # 2進数
'0b1100100'
>>> oct(100)  # 8進数
'0144'
>>> hex(100)  # 16進数
'0x64'

先頭に何かついていますが、0bは2進数、0xは16進数という表明に使います。8進数は0だけを先頭につけるというルールもありますが、桁埋めの0と混同しないように注意してください。

次に、N進数から10進数への変換です。実はこれはすでに使ったことがある関数を使います。

>>> int('100')
100
>>> int('1100100', 2)  # 2進数
100
>>> int('0144', 8)     # 8進数
100
>>> int('64', 16)      # 16進数
100
>>> int('6B', 16)      # A,B,C……も使える
107
>>> int('212', 3)      # あまり使わないN進数も一応使える
23

intの第一引数に数字のもとになる文字列を入れ、第二引数に基数を指定します。第二引数を省略すると10進数として扱われます。基数とN進数に関しては以上です。

文字コード ASCII

さて、話が長くなりましたが文字コードの話を始めましょう。コンピュータは、突き詰めると01しか理解できないので、文字も最終的には01に対応付けられます。

その「文字と01の対応関係」を決めるのが文字コードと呼ばれているもので、アルファベットと数字のみを利用する場合は「ASCIIコード」と呼ばれる文字コードが使われることが多いです。たとえばASCIIコードだと「01100001」は「a」に対応し、その次の「01100010」は「b」に対応しています。ただ、先にお伝えしたように2進数だと桁が長いので、一般的には16進数で文字コードを表現します。aとbはそれぞれ以下のようになります。

>>> hex(int('01100001', 2))  # a
'0x61'
>>> hex(int('01100010', 2))  # b
'0x62'

以下にASCIIコードの一部を記載します。

ASCIIコード表 資料:Wikipedia「ASCII」より引用

見てもらうとわかりますが、文字1文字が1バイトに対応していますね。1バイトは8ビットなので2の8乗パターンの組み合わせ、つまり0~255の256パターンが存在します。アルファベットや数字、改行などのいくつかの特殊記号だけであれば256パターンもあれば表現できます。

1バイトで表現できないマルチバイト文字

ただ、よく考えてみてください。日本語はどう考えても1バイト=256個じゃ足りないですね。そこで日本語を扱うときは複数バイトを使います。2バイトにするだけでも65536パターン、3バイトにすれば16777216パターンの組み合わせが表現できます。

この複数バイトの01と文字のマッピングをする文字コードにはいくつかの種類があります。日本で有名なのは、Shift-JISやUTF-8、EUC-JPあたりでしょうか。文字コードが違えば、01に変換したデータも変わってきます。

確認のために、

This is test.
Hello Python.
あいうえお。

というテキストをASCII、Shift-JIS(SJIS)、UTF-8でファイルに書き込み、それをバイナリエディタを使って01(実際は16進数)で見てみます。なお、日本語あいうえおはASCIIには対応してないので削っています。上からASCII、SJIS、UTF-8という順です。

バイナリエディタを使って上記のテキストを変換したもの。上からASCII、SJIS、UTF-8

英語の部分は変わっていませんが、強調している日本語の部分は文字コードごとに違っているのがわかりますね。

ここからわかることは「どの文字コードで書かれているか」ということがわからないと、エディタやPythonは適切に文字を扱うことができないということです。たとえば、SJISのファイルをUTF-8として読み込もうとすれば解釈できずに文字化けが発生します。当然、ひとつのファイルのなかでさまざまな文字コードを織り交ぜるということはできません。ファイルのなかで利用する文字コードは必ず統一して下さい。文字コードがどのようなものか、正しく文字コードを認識できることがいかに大切かということを理解してもらえたら幸いです。なお、自分で文字コードの変更を試されたい場合は文字コードを変更できるエディタやnkfコマンドなどを利用すればよいと思います。

余談ですが、もしどの文字コードを使ってもよいのであれば、UTF-8が今だと一般的かもしれません。10年前だとShift-JISだとかEUC-JPあたりも見たのですが、今のプログラマはあまり好んで使わないかもしれません。私は制約がない限り、すべてUTF-8でコードもドキュメントも統一するようにしています。

文字コードの宣言

先ほどお伝えしたように、そもそもPythonが書かれたテキストファイルがどの文字コードを利用しているかということを正しく認識される必要があります。そのため、ASCII以外を使う場合は「この文字コードを使います」とファイルの先頭で宣言をするのが通例です。

それは以下のように行います。なお、IDLEでの日本語の扱いは正直あまりよいとはいえないので、なんらかの高機能なテキストエディタを利用して記述し、適切な文字コードに設定されたプロンプトなり、ターミナルなりで実行してください(文字コードの設定も演習にあるので、先にそちらをやっていただいてもかまいません)。

# coding: utf-8 

print('hello python')
print('あいうえお python')

上記の「utf-8」と書かれている場所が文字コードの宣言です。utf-8と宣言しているので、utf-8以外で書かれているとトラブルが発生します。注意してください。ここを「shift-jis」や「euc-jp」などに変えると、その文字コードとして解釈されます。宣言の先頭が#から始まっていることから理解してもらえると思いますが、この行はコメントアウトされているのでPythonはプログラムとしては解釈しません。

これを実行すると以下のように表示されます(ターミナルの文字コードをUTF-8にしておく必要があります)。

% python test.py
hello python
あいうえお python

試しにファイルで利用される文字コードはそのままで、宣言をshift-jisに変更してみます。

# coding: shift-jis

print('hello python')
print('あいうえお python')

これを実行してみます。

% python test.py
  File "test.py", line 4
SyntaxError: 'shift_jis' codec can't decode bytes in position 15-16: illegal multibyte sequence

エラーが出て、「shift-jis のコーデックをデコードできないよ。その原因は……」と怒られてしまっていますね。このように文字コードAを文字コードBとして読み込もうとするとトラブルが発生してしまいます。トラブルはこのようなエラーであったり、場合によっては文字化けだったりします。

ではファイルの文字コードをShift-JISに変更して実行してみます。これでファイルの文字コードと冒頭の宣言の文字コードが一致します。

# UTF-8 から Shift-JISに変更
YUIITO-M-64WZ% nkf -s --overwrite /Users/yuichi/Desktop/test.py 

# 実行(ターミナルはUTF-8モード)
YUIITO-M-64WZ% python test.py                                   
hello python
?????????? python

# 実行(ターミナルは Shift-JISモード)
YUIITO-M-64WZ% python test.py
hello python
あいうえお python

文字コードの変更後はとりあえずエラーは出なくなっていますね。Pythonはなんとか処理できているようです。ただ、ターミナルの出力が文字化けして?になってしまっています。これはPythonの出力がShift-JISで、ターミナルがUTF-8としてそれを解釈しようとするためです。もちろんどの文字コードとして解釈しようとするかは環境次第です。

環境設定などからターミナルの文字コードを Shift-JISにすると、ターミナルがPythonから渡された出力を解釈できるようになるので、きちんと表示してくれています。

かなり面倒くさいと思いますが、これが文字コードの扱いというものです。Python以外でもこのような問題を避けるため、必要な箇所以外はソースコードに日本語を書かないほうが楽かもしれないです。

Unicode文字列

Python 2と3では日本語(中国語などのほかのマルチバイト文字も含む)の扱いが異なります。3はほとんど英語と同じ感覚で使えますが、2では文字列型の亜種である「Unicode文字列型」を使うことで日本語を扱います。なお、Unicode文字列型ではなく単なる文字列型として日本語を扱うことも可能です。ただ、その際Pythonは日本語としてではなく単なるバイトの配列として扱うので、先の例のようなprint文以上のことはやらせないのがいいと思います。

Unicode文字列の宣言は簡単です。文字列の先頭にuとつけてあげればいいだけです。

# coding: utf-8 

print(type('hello'))      # <type 'str'>
print(type(u'hello'))     # <type 'unicode'>
print(type('あいうえお'))   # <type 'str'>
print(type(u'あいうえお'))  # <type 'unicode'>

通常の文字列とUnicode文字列の違いを少し見てみましょう。たとえば文字列長の取得です。

# coding: utf-8 

print(len('あいうえお'))   # 15
print(len(u'あいうえお'))  # 5

日本語をUnicodeでない文字列として扱った際、lenで文字列長を取得したら正しい値が得られていないことがわかりますね。これは先に説明したように、単なるバイト配列として扱われているためです。一方、Unicode文字列は正しい長さが取得できています。特に理由がない限り、ASCIIコード以外を使う場合はUnicode文字列を利用したほうがよいです。

文字列長以外にも影響はあります。たとえば文字列の探索の意味合いも変わってきます。文字列の探索は、文字というより単なるバイト配列の探索です。 Unicode文字列は本当にUnicodeの文字を探索しています。

文字列とUnicodeを併用するとエラーになるので注意してください。

# coding: utf-8 

text = 'あいうえお'
print('う' in text)   # True

utext = u'あいうえお'
print(u'う' in utext) # True

print(u'う' in text)  # Error
# UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: 
# ordinal not in range(128)

上記のような通常の文字列とUnicode文字列を組み合わせる場合は、文字列をUnicode文字列に変換します。変換の仕方は文字列のdecode関数かunicode関数に文字列を引数として与えるかのいずれかとなります。

# coding: utf-8 

text = 'あいうえお'
print(u'う' in text.decode('utf-8'))    # True
print(u'う' in unicode(text, 'utf-8'))  # True

文字コードの変換も可能です。

# coding: utf-8 

utext = u'あいうえお'
print(len(utext.encode('utf-8')))  # 15

text = utext.encode('shift-jis')
print(len(text))                   # 10

stext = text.decode('shift-jis')
print(len(utext))                  # 5

上記の例ではUnicode文字列を、UTF-8およびShift-JISとしてバイナリ配列にencodeしています。UTF-8と同様にShift-JISのバイト配列(文字列)もUnicodeに変換することが可能です。

Pythonで日本語のテキストファイルを読み書きする

今まではプログラムのなかに直接日本語を書き、それを使うだけでした。ほかにはファイルの読み書きあたりに利用されることが多いです。

ファイルの入出力にはcodecsパッケージのopen関数を使うのが簡単です。これを使うと通常のファイル入出力の手順と大差なくマルチバイト文字を扱えます。

あいうえお
abcde
かきくけこ

と書かれたutf8.txtというファイルを読み込み処理してみます。

import codecs
f = codecs.open('utf8.txt', 'r', 'utf-8')  # read file
for line in f:
    print(type(line))
    print(line),
f.close()

説明が必要なのは codecs.open関数だけですね。第一引数でファイル名、第二引数でオープンのモード(今回はread)、そして第三の引数でファイルの文字コードを指定しています。それ以外は通常のファイル読み込みとほとんど同じです。

これを出力してみます。

% python test.py
<type 'unicode'>
あいうえお
<type 'unicode'>
abcde
<type 'unicode'>
かきくけこ

ファイルオブジェクトから取得してきている一行はUnicode文字列になっています。

次に書き込みをしてみます。せっかくなので、このutf8のファイルをShift-JISで書きだしてみましょうか。

import codecs
f_in = codecs.open('utf8.txt', 'r', 'utf-8')
f_out = codecs.open('sjis.txt', 'w', 'sjis')  # write file
for line in f_in:
    f_out.write(line)  # write
f_in.close()
f_out.close()

ファイルのオープン時にオープンモードをwにすることで書き込みファイルとして開いています。オープンしたファイルに対してUnicode文字列をwriteしてあげればファイルに文字列が追加されます。

結果を確認してみます。“nkf -g ファイル名”とすると、そのファイルのエンコーディングの判定ができます。

% python test.py
% nkf -g utf8.txt 
UTF-8
% nkf -g sjis.txt 
Shift_JIS

書き込みファイルsjis.txtの文字コードがSJISと判定されていますね。UTF8のファイルを読み取り、それを解釈、SJISとして書き込むという動作がうまく動いています。


演習1

10進数を2進数に変換する関数を既存の関数を使わずに自作してください。引数に整数を受け取り、01で構成される文字列を返します。

演習2

コマンドプロンプトかターミナルのエンコードをUTF-8に変更し、Python から“あいうえお”と出力してください。私が確認した限り、文字コードの変更方法は調べれば簡単にやり方が出てきました。

演習3

Unicode文字列“今日はいい天気ですね。おはようございます。ハロー”という文字列を記号“。”で分割してください。

演習4

Pythonで以下のコマンドを作ってください。

python convert_codec.py FILE_NAME FILE_CODEC WRITE_CODEC

コマンドライン引数に読み込むファイル名とその文字コード、そして書き出す文字コードを与えます。すると FILE_NAME.WRITE_CODEC.txt というファイル名を指定したコーデックで書き出します。

たとえば、

python convert_codec.py utf8.txt utf-8 sjis

とするとutf8.txt.sjis.txtというファイル名のファイルが作成されます。その中身は utf8.txtと同じですが、文字コードがsjisになっています。

※解答はこちらをご覧ください。


次回はPythonでシェルを操作する方法について扱います。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら