今さらPython3 (44) - 文字列の話

ページ数で言うと約1/3終了。今日から第7章に突入。

入門 Python 3

入門 Python 3

Unicode

昔、Python2.7で入門 自然言語処理という本を使って、ゴニョゴニョしたり、あとは中国語の単語の処理なんかをやっていたときに、Unicodeには随分と悩まされた記憶あり。Python3系ではこのあたりが随分変わったということなので、興味津々。

一応本で紹介されているリンクもさらっとチェック。
Code Charts
面 (文字コード) - Wikipedia

小難しい説明を読んでも、あまりピンと来ず、\uとか\xなんちゃらというのが出てきたのは覚えているという程度の認識なんだが、ここも実例を通して学んだ方が早そうだ。

>>> def unicode_test(value):
...     import unicodedata
...     name = unicodedata.name(value)
...     value2 = unicodedata.lookup(name)
...     print('value="%s", name="%s", value2="%s"' % (value, name, value2))
... 
>>> unicode_test('A')
value="A", name="LATIN CAPITAL LETTER A", value2="A"
>>> unicode_test('a')
value="a", name="LATIN SMALL LETTER A", value2="a"
>>> unicode_test('$')
value="$", name="DOLLAR SIGN", value2="$"
>>> unicode_test('\u00a2')
value="¢", name="CENT SIGN", value2="¢"
>>> unicode_test('\u20ac')
value="€", name="EURO SIGN", value2="€"
>>> unicode_test('\u2603')
value="☃", name="SNOWMAN", value2="☃"
>>> unicode_test('☃')
value="☃", name="SNOWMAN", value2="☃"

unicodedata.name()は、Pythonの中で使われている名称を返し、unicodedata.lookup()は逆に名称から文字を返す。コードを覚えるよりも名称の方が覚えやすいだろということか。ユーロマークだったり雪だるまなんかはキーボードから入れられなくても、\uxxxxで正しく指定すれば出力できると。

雪だるまもそうだけど、特殊な文字を含む場合にどう保存するのかというは悩ましいところで、まったく本に書いてあるとおりで、コピーして貼り付けて上手く表示されたらラッキーぐらいしかアイディアがなかった。

>>> place = 'café'
>>> place
'café'

(ちなみにMacの場合は、Option+eでもう1回eを押したら出てくる。)

Character Name Index

これによると、E WITH ACUTE, LATIN SMALL LETTERというのに00E9というコードが割当たっているので、

>>> unicode_test('\u00E9')
value="é", name="LATIN SMALL LETTER E WITH ACUTE", value2="é"

こう出る。でも、nameのところが、さっきのUnicodeのページと少し違っているので、

>>> import unicodedata
>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
'é'
>>> unicodedata.lookup('E WITH ACUTE, LATIN SMALL LETTER')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: "undefined character name 'E WITH ACUTE, LATIN SMALL LETTER'"
>>> 

こんな結果になる。理由は分からないが、カンマを削除して、カンマの後ろの部分を前に持ってくるというルールになっているそうだ。

>>> place = 'caf\u00e9'
>>> place
'café'
>>> place = 'caf\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> place
'café'
>>> 

どこかから文字をコピーしてきて、それのコードを返してくれれば便利だなと思うんだけど、そのような関数はなさげ。(英語だから見落としている可能性もあるけど)

6.5. unicodedata — Unicode Database — Python 3.4.4rc1 documentation

でも、実際問題nameが拾えれば問題なと思われ。

>>> u_umlaut = '\N{LATIN SMALL LETTER U WITH DIAERESIS}'
>>> u_umlaut
'ü'
>>> drink = 'Gew' + u_umlaut + 'rztraminer'
>>> print('Now I can finally have my', drink, 'in a', place)
Now I can finally have my Gewürztraminer in a café
>>> 

なるほど。ちなみに中国語のピンインの声調記号なんかの場合は、

>>> unicode_test('ī')
value="ī", name="LATIN SMALL LETTER I WITH MACRON", value2="ī"
>>> unicode_test('í')
value="í", name="LATIN SMALL LETTER I WITH ACUTE", value2="í"
>>> unicode_test('ǐ')
value="ǐ", name="LATIN SMALL LETTER I WITH CARON", value2="ǐ"
>>> unicode_test('ì')
value="ì", name="LATIN SMALL LETTER I WITH GRAVE", value2="ì"
>>> 

という感じでnameを調べておいて、これを全部の母音*4声で用意しておけば、

>>> a3 = '\N{LATIN SMALL LETTER A WITH CARON}'
>>> a3
'ǎ'
>>> i2 = '\N{LATIN SMALL LETTER I WITH ACUTE}'
>>> i2
'í'

‘你好’という単語のピンインをこんな感じで出せば良いと。

>>> ni2hao3 = 'n' + i2 + 'h' + a3 + 'o'
>>> ni2hao3
'níhǎo'
>>> 

データ上は声調記号をいちいち書くのが面倒なので、ni2hao3みたいな感じで登録する事が多いので、表示の時だけ声調記号を置く母音を特定して(これは明確なルールがある)、その母音を声調記号付きのモノに置き換えるという関数を作っておけば良さそうだね。

脱線したので戻ります。

>>> len('$')
1
>>> len('\U0001f47b')
1
>>> 

len()は、ユニコードの文字数をきちんと数えてくれるのね。

エンコード

Pythonの中の世界のUnicodeと外の世界のUnicodeを行き来する。まずは外の世界に行くためにエンコードする方。

>>> snowman = '\u2603'
>>> len(snowman)
1
>>> ds = snowman.encode('utf-8')
>>> len(ds)
3
>>> ds
b'\xe2\x98\x83'
>>> 

過去記事にあると思うけど、昔、Filemakerと行き来するときに苦労したところだね。snowmanはPythonの世界では\u2603と表現されているけど、それを外の世界では標準語になっているutf-8に変換すると、b'\xe2\x98\x83'になる。

実際にはutf-8しか使わないと思うけど、asciiとかも試してみよう。

>>> ds = snowman.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\u2603' in position 0: ordinal not in range(128)

怒られる。ということで、encode()には変換できなかったときに対応を第2引数として指定できる。

>>> ds = snowman.encode('ascii', 'ignore')
>>> ds
b''
>>> snowman.encode('ascii', 'replace')
b'?'
>>> snowman.encode('ascii', 'backslashreplace')
b'\\u2603'
>>> snowman.encode('ascii', 'xmlcharrefreplace')
b'&#9731;'
>>> 

変換が出来なくて?まみれになったときの絶望感とか思い出してくるねw。

デコード

今度は、外部ソースから取り込んできてPythonの世界のUnicodeに変換してやる方。

>>> place = 'caf\u00e9'
>>> place
'café'
>>> type(place)
<class 'str'>
>>> place_bytes = place.encode('utf-8')
>>> place_bytes
b'caf\xc3\xa9'
>>> type(place_bytes)
<class 'bytes'>
>>> len(place_bytes)
5

placeにcaféという文字列を入れて、いったんUTF-8エンコードしてplace_bytesに格納。なぜ5bytesかというと、最初のcafは1文字ずつ、最後のéを2バイト使って表現しているからという事らしい。ここまでが下準備なので、ここからデコードするわけですね。

>>> place2 = place_bytes.decode('utf-8')
>>> place2
'café'
>>>
>>> hello = 'こんにちは'
>>> hello_SJ = hello.encode('shift-jis')
>>> hello_SJ
b'\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd'
>>> hello_SJ.decode('shift-jis')
'こんにちは'
>>>  

ソース側のエンコードが何かを確認して、それをdecode()の引数で正しく指定してやれば、想定した文字列が返ってくるはず。日本語なので、念のためShift-JISでも試してみたけど大丈夫そうだね。

>>> place3 = place_bytes.decode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)
>>>
>>> place4 = place_bytes.decode('latin-1')
>>> place4
'café'
>>> place5 = place_bytes.decode('windows-1252')
>>> place5
'café'
>>>  

これを見る限り、本でも指摘しているけど、できるだけPythonに取り込む前の段階でUTF-8のフォーマットにしておいた方が、後々楽だなというのが感想。

これらはお勧めリンク。個人的には2つめが分かりやすいかなと思った。
Unicode HOWTO — Python 3.5.1 documentation
Ned Batchelder: Pragmatic Unicode
www.joelonsoftware.com

(つづく)