- Unicode HOWTO — Python 3.10.0b2 ドキュメント(日本語)
Unicode HOWTO — Python 3.10.0 documentation(英語) - 文字符号化方式 - Wikipedia
- Unicode C0制御文字と基本ラテン文字 - CyberLibrarian
- UTF-8 - Wikipedia
- 「あ」を UTF-8 エンコード - Qiita
- 文字コード入門 - とほほのWWW入門 - とほほのWWW入門
- ISO/IEC 8859 - Wikipedia
- JIS X 0201 - Wikipedia
- JIS X 0208 - Wikipedia
- EUC-JP - Wikipedia
まとめ
|
||||||||||||||||||
特定の流儀での文字列のコードネーム列は Python で以下のように確認できる(何バイトかわかりやすくするために 16 進数表示で 2 桁ごとに空白を挿入しているが、sep オプションが有効なのは Python 3.8 以降なのでこれより古いバージョンではこのオプションを取る必要がある)。 |
>>> 'ありがとう'.encode('utf-8').hex(sep=' ') # utf-8 では 15 バイト 'e3 81 82 e3 82 8a e3 81 8c e3 81 a8 e3 81 86' >>> 'ありがとう'.encode('shift_jis').hex(sep=' ') # shift_jis では 10 バイト '82 a0 82 e8 82 aa 82 c6 82 a4' >>> 'アリガトウ'.encode('shift_jis').hex(sep=' ') # さらに半角なら 6 バイト 'b1 d8 b6 de c4 b3' >>> 'ありがとう'.encode('latin-1').hex(sep=' ') # 平仮名は latin-1 のメンバーではない UnicodeEncodeError: 'latin-1' codec can't encode characters in position 0-4: ordinal not in range(256) >>> 'Dankeschön'.encode('latin-1').hex(sep=' ') # オー・ウムラウトは latin-1 のメンバーである '44 61 6e 6b 65 73 63 68 f6 6e' >>> 'Dankeschön'.encode('ascii').hex(sep=' ') # オー・ウムラウトは ascii のメンバーではない UnicodeEncodeError: 'ascii' codec can't encode character '\xf6' in position 8: ordinal not in range(128) >>> 'Thanks'.encode('ascii').hex(sep=' ') # アルファベットは ascii のメンバーである '54 68 61 6e 6b 73'
Unicode というのは世界中のすべての文字を会員登録させて会員番号(コードポイント)をふることを目指している会なのですね。会員番号は 0~0x10FFFF(0x は16進数のプレフィックスですね)の整数値であると。例えば平仮名の「あ」の会員番号は 0x3042(10進数だと 12354)ですね。特に Unicode のコードポイントであることを明示するために U+3042 と表記することもあるのですね。
>>> chr(12354) # コードポイントを長さ1の文字列に変換 'あ' >>> ord('あ') # 長さ1の文字列からコードポイントに変換 12354 >>> hex(ord('あ')) '0x3042'
では Unicode さえあれば文字列を整数列にできるので計算機で文字列を扱うことができますね…って違うのですか? えっと、計算機さんはデータをバイト列―0~256 の整数の列と考えていいですよね―にしてあげなければ利用できないのですね? 会員番号は16の6乗までしかないですから、256の3乗までしかないわけで、3バイトずつ並べればいいのでは? 例えば「ありがとう」は以下の15バイトにします。
00 30 42 00 30 8a 00 30 4c 00 30 68 00 30 46
これで一体何の不満が*1…「無駄が多い」? た、確かに、日本語はともかく、数字やアルファベットやカンマやピリオドはみんな会員番号 122 までにいますから、英語の文章はほとんど1文字が1バイトで済むはずですね…すべての文字を3バイトで表現するのはあまりに非効率です…。それ以外に、「C言語の strlen() のような関数と互換性がない」? C言語の strlen() は、文字型ポインタを文字列の先頭からゼロバイト 00 (8 ビット文字符号化方式における「ヌル文字」)に到達するまでインクリメントすることで文字列の長さを計測するのですよね*2。あっ…上の「ありがとう」には途中に 00 が含まれていますね…ゼロバイトが文字列の終端を意味していません…。
「あ」さんの会員番号は 0x3042 でしたが、UTF-8 式でのコードネームもこっそりみてみましょう。
>>> 'あ'.encode('utf-8') b'\xe3\x81\x82'
3バイト列「e3 81 82」ですか…会員番号を素朴に3バイト列にした「00 30 42」とは結構違いますね…UTF-8 とは一体どんな変換規則になっているのでしょうか? …ウィキペディアをみて自分で再現すると以下ですね(簡単のため2進数表記を出力します)。会員番号が 0x7f を超える皆さんは会員番号を2進数にしてからいくつかの区間にばらしてパターンにあてはめるのがミソですね。
def unicode_to_utf8(i): if i <= 0x7f: # 会員番号 ~0x7f は単に8桁の2進数に(1バイト) return f'{i:08b}' elif i <= 0x7ff: # 会員番号 ~0x7ff は11桁の2進数にして以下にはめる(2バイト) b = f'{i:011b}' return '110' + b[:5] + '10' + b[5:] elif i <= 0xffff: # 会員番号 ~0xffff は16桁の2進数にして以下にはめる(3バイト) b = f'{i:016b}' return '1110' + b[:4] + '10' + b[4:10] + '10' + b[10:] elif i <= 0x10ffff: # 会員番号 ~0x10ffff は21桁の2進数にして以下にはめる(4バイト) b = f'{i:021b}' return '11110' + b[:3] + '10' + b[3:9] + '10' + b[9:15] + '10' + b[15:] raise ValueError('Unicode のコードポイントの最大値を超えています!') # 検算 for c in ['a', 'あ', '🍪']: print(c) print(unicode_to_utf8(ord(c))) print(format(int(c.encode('utf-8').hex(), 16), 'b').zfill(8))
検算結果もばっちりです。
a 01100001 01100001 あ 111000111000000110000010 111000111000000110000010 🍪 11110000100111111000110110101010 11110000100111111000110110101010
それに、ウィキペディアのビットパターン表をみて「あ」のコードネームがコードポイント「00 30 42」とは結構異なってみえる「e3 81 82」になる理由も腑に落ちました。つまり、UTF-8 式でのコードネームは必ず以下のいずれかになるので、「データ中のあるバイトをみただけで、それが『ここから何バイト続く文字の開始バイトである』or 『文字の開始バイトではない』とわかる」が実現されています。バイトの先頭が 0 であったら「この1バイトで1文字だな」と思えばいいし、110 であったら「このバイトと次のバイトの2バイトで1文字だな」と思えばいいし、10 であったら「このバイトは文字の開始バイトではない」と思えばいいわけです。
# Unicode のメンバーの文字たちのコードネームは以下のいずれか! 0xxxxxxx <- 会員番号が2進数で7桁以下ならこう 110xxxxx 10xxxxxx <- 会員番号が2進数で11桁以下ならこう 1110xxxx 10xxxxxx 10xxxxxx <- 会員番号が2進数で16桁以下ならこう 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx <- 会員番号が2進数で21桁以下ならこう
しかし、よく考えると「開始バイトではない」の識別に 10 と2桁を割くのはもったいないのではないでしょうか。以下のエンコーディングであればコードネームが2バイト以下で済むメンバーの数が上のやり方の2倍、コードネームが3バイト以下で済むメンバーの人数が4倍です。
# メンバーの文字たちのコードネームを以下のいずれかにするのではだめなのか? 10xxxxxx <- 会員番号が2進数で6桁以下ならこう 110xxxxx 0xxxxxxx <- 会員番号が2進数で12桁以下ならこう 1110xxxx 0xxxxxxx 0xxxxxxx <- 会員番号が2進数で18桁以下ならこう 11110xxx 0xxxxxxx 0xxxxxxx 0xxxxxxx <- 会員番号が2進数で24桁以下ならこう
ですがこのエンコーディングでは以下の問題がありますね。だから採用されていないのでしょう。
- コードネームが 2, 3 バイト以下のメンバーの数を増やせる一方で、コードネームが1バイトのメンバーの数は 128 から 64 に減ってしまう。会員番号 64~127 にいるアルファベットの大文字小文字が 1 バイトで表現できなくなってしまう弊害が大きいと思われる。
- 会員番号 0 の「ヌル文字」がゼロバイトにならないので「ゼロバイトを発見することで文字列の終端位置を把握する」という手法が取れなくなる(ただし、これだけなら会員番号 0 のみゼロバイトを当てれば解決すると思われる)。
それで「Python ソースコードのデフォルトエンコーディングは UTF-8」とありますね。「Python はソースコードにかかれた文字を UTF-8 でバイト列にして理解していきます」ということなのでしょうか。なので Unicode 文字をソースコードに記述することができるのですね。Unicode 文字であれば UTF-8 によってバイト列にできますから。他方、ソースコードを英数字のみ(ASCII のみ)にしつつ Unicode 文字を表現することもできるのですね。"\u3042" と記述しても "あ" と記述しても Python さんは e3 81 82 というバイト列として理解するのでしょう。以下のコードはコメントが日本語なので台無しですが。
print('\u3042') # 「あ」(会員番号が16進数で4桁以下ならこうかける) print('\U00003042') # 「あ」(すべての文字はこうかける) print('\U0001f36a') # クッキーの絵文字(会員番号が4桁超えなのでこのかき方しかできない) print('\N{HIRAGANA LETTER A}') # 「あ」を名称で記述 print('\N{COOKIE}') # クッキーの絵文字を名称で記述
あ あ 🍪 あ 🍪
そして文字列をバイト列にする関数が str.encode(), バイト列を文字列にする関数が bytes.decode() とのことですが、これらは第1引数に文字符号化方式、第2引数に「変換できない文字/バイトがあった場合の対応方法」を指定できるのですね。
str.encode() | bytes.decode() | |
---|---|---|
strict | 例外を送出する。 | 例外を送出する。 |
ignore | エンコードできなかった文字を単に抜く。 | デコードできなかったバイトを単に抜く。 |
replace | エンコードできなかった文字を 3f(ASCII や UTF-8 等で疑問符)にする。 | デコードできなかったバイトをREPLACEMENT CHARACTER にする。 |
backslashreplace | エンコードできなかった文字を \uXXXX あるいは \UXXXXXXXX(XXXX, XXXXXXXX は Unicode コードポイントの16進数表記)としてからエンコードする(Unicode 文字でなければそもそも Python のソースコードに記述できない)。 | デコードできなかったバイトを \xXX(XX はそのバイトの16進数表記)という文字列にする。 |
namereplace | エンコードできなかった文字を \N{hoge}(hoge は Unicode におけるその文字の名称)としてからエンコードする(Unicode 文字でなければそもそも Python のソースコードに記述できない)。 | 《 指定不可 》 |
xmlcharrefreplace | エンコードできなかった文字を XXXX;(XXXXX は Unicode コードポイントの10進数表記)としてからエンコードする(Unicode 文字でなければそもそも Python のソースコードに記述できない)。 | 《 指定不可 》 |