1. ホーム
  2. python

[解決済み] なぜPythonはデフォルトのエンコーディングがASCIIなのにunicodeの文字を表示するのですか?

2022-05-16 04:02:38

質問

Python 2.6シェルからです。

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 

私は、"é" という文字はASCIIの一部ではないし、エンコーディングを指定していないので、print文の後に何かちんぷんかんぷんなものかErrorが発生すると思っていたのです。ASCIIがデフォルトのエンコーディングであることの意味を理解していないのでしょう。

EDIT

に編集を移しました。 回答 セクションに移動し、提案されたとおりに受け入れました。

どのように解決するのですか?

さまざまな回答からの断片のおかげで、私たちは説明を縫うことができると思います。

u'\xe9' という Unicode 文字列を出力しようとすると、Python は暗黙的に sys.stdout.encoding に現在格納されているエンコーディング方式を使ってその文字列をエンコードしようとします。Pythonは実際には、開始された環境からこの設定を拾います。もし環境から適切なエンコーディングが見つからなければ、その時だけ、その環境での デフォルトの ASCIIに戻ります。

例えば、私はエンコーディングのデフォルトがUTF-8であるbashシェルを使用しています。そこからPythonを起動すると、その設定を拾い上げて使用します。

$ python

>>> import sys
>>> print sys.stdout.encoding
UTF-8

ちょっとPythonシェルを終了して、bashの環境をインチキなエンコーディングで設定してみましょう。

$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.

その後、再び python シェルを起動し、実際にデフォルトの ascii エンコーディングに戻ることを確認します。

$ python

>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968

ビンゴ!

もし今、ascii以外のunicode文字を出力しようとすると、素敵なエラーメッセージが表示されるはずです。

>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' 
in position 0: ordinal not in range(128)


Pythonを終了して、bashシェルを破棄しましょう。

Pythonが文字列を出力した後に何が起こるかを観察してみましょう。このために、まずグラフィックターミナル (私は Gnome Terminal を使っています) 内で bash シェルを起動し、出力を ISO-8859-1 または latin-1 でデコードするようにターミナルを設定します (グラフィックターミナルには、通常 文字エンコードを設定する というオプションがあります)。これは実際の シェル環境の のエンコーディングは変更されません。 ターミナル が与えられた出力をデコードする方法を変えるだけで、 ウェブブラウザが行うのと同じようなものです。したがって、シェルの環境とは無関係にターミナルのエンコーディングを変更することができます。では、シェルから Python を起動し、sys.stdout.encoding がシェル環境のエンコーディング (私の場合は UTF-8) に設定されていることを確認しましょう。

$ python

>>> import sys

>>> print sys.stdout.encoding
UTF-8

>>> print '\xe9' # (1)
é
>>> print u'\xe9' # (2)
é
>>> print u'\xe9'.encode('latin-1') # (3)
é
>>>

(1) pythonはバイナリ文字列をそのまま出力し、端末はそれを受け取ってその値をlatin-1文字マップにマッチングさせようとします。latin-1では、0xe9または233は文字"é"を生成するので、ターミナルはそれを表示します。

(2) python は 暗黙のうちに Unicode文字列をsys.stdout.encodingで現在設定されているスキーム、この例では"UTF-8"でエンコードします。UTF-8でエンコードした結果、バイナリ文字列は「' \xc3㎟'」となります(後の説明参照)。ターミナルはこのストリームを受け取って、latin-1を使って0xc3a9をデコードしようとしますが、latin-1は0から255まであるので、ストリームを1バイトずつしかデコードできないのです。0xc3a9は2バイトなので、latin-1デコーダはこれを0xc3 (195) と0xa9 (169) と解釈し、2文字を生成する。Ãと©の2文字が生成されます。

(3) pythonはunicodeのコードポイントu' \xe9' (233)をlatin-1スキームでエンコードしています。latin-1コードポイントの範囲は0-255で、その範囲内のUnicodeと全く同じ文字を指していることがわかりました。したがって、その範囲内のユニコードコードポイントは、latin-1方式でエンコードされたとき、同じ値になります。つまり、u' \xe9' (233) を latin-1 でエンコードすると、バイナリ文字列 '\xe9' も生成されます。Terminalはその値を受け取り、latin-1文字マップ上でマッチングを試みます。(1)と同様に、"é"となり、これが表示されます。

ここで、ターミナルのエンコーディング設定をドロップダウンメニューからUTF-8に変更してみましょう(Webブラウザのエンコーディング設定を変更するのと同じです)。Python を停止したり、シェルを再起動したりする必要はありません。ターミナルのエンコーディングはPythonのものと一致するようになりました。もう一度印刷を試してみましょう。

>>> print '\xe9' # (4)

>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)

>>>

(4) pythonの出力は バイナリ の文字列をそのまま出力します。TerminalはそのストリームをUTF-8でデコードしようとします。しかし、UTF-8 は値 0xe9 を理解しないため(後の説明を参照)、ユニコード コード ポイントに変換することができません。コードポイントが見つからず、文字も出力されません。

(5) pythonが試したのは 暗黙のうちに でエンコードしようとします。まだ "UTF-8"です。結果として得られるバイナリ文字列は '\xc3

(6) python が Unicode 文字列を latin-1 でエンコードすると、同じ値 '\xe9' を持つバイナリ文字列が生成されます。これも端末では(4)の場合とほぼ同じです。

結論 - Pythonはデフォルトのエンコーディングを考慮することなく、非ユニコード文字列を生データとして出力します。ターミナルは、現在のエンコーディングがデータに一致する場合、それらを表示するだけです。 - Python は sys.stdout.encoding で指定されたスキームを使用してエンコードした後に Unicode 文字列を出力します。 - Pythonはシェルの環境からその設定を取得します。 - ターミナルはそれ自身のエンコーディングの設定に従って出力を表示します。 - ターミナルのエンコーディングはシェルのエンコーディングとは独立しています。


unicode、UTF-8、latin-1についての詳細です。

Unicode は基本的に文字の表で、いくつかのキー (コード ポイント) は慣習的にいくつかの記号を指すように割り当てられています。たとえば、慣習的にキー 0xe9 (233) は記号 'é' を指す値であると決められています。ASCIIとUnicodeは0から127まで、latin-1とUnicodeは0から255まで、同じコードポイントを使用します。つまり、0x41 は ASCII、latin-1、Unicode で 'A' を指し、0xc8 は latin-1 と Unicode で 'Ü' を指し、0xe9 は latin-1 と Unicode で 'é' を指します。

電子デバイスで作業するとき、Unicode コードポイントは電子的に表現される効率的な方法を必要とします。それが、エンコーディングのスキームです。様々な Unicode 符号化スキームが存在します(utf7、UTF-8、UTF-16、UTF-32)。最も直感的でわかりやすい符号化方式は、Unicodeマップにあるコードポイントの値をそのまま電子的な値として使うことですが、現在Unicodeには100万以上のコードポイントがあり、中には3バイトで表現しなければならないコードポイントもあることになります。テキストで効率的に作業するには、1 対 1 のマッピングはかなり非現実的です。なぜなら、実際の必要性に関係なく、1 文字あたり最低 3 バイトの、まったく同じ量のスペースにすべてのコード ポイントを格納しなければならないからです。

ほとんどのエンコード方式は、必要なスペースに関して欠点があり、最も経済的なものは、すべての Unicode コードポイントをカバーしていません。例えば、ascii は最初の 128 をカバーするだけですが、latin-1 は最初の 256 をカバーします。また、より包括的であろうとするものは、一般的なquot;cheap"キャラクタであっても必要以上のバイトを必要とするため、結局は無駄なものとなってしまうのです。たとえばUTF-16は、Asciiの範囲にある文字も含めて、1文字あたり最低2バイトを使います(65文字の「B」は、UTF-16でも2バイトのストレージを必要とします)。UTF-32 はさらに無駄が多く、すべての文字を 4 バイトで保存します。

UTF-8 は偶然にもこのジレンマを巧みに解決し、可変量のバイトスペースでコードポイントを格納できるスキームを備えています。そのエンコーディング戦略の一部として、UTF-8 はコードポイントにフラグビットを混入し、(おそらくデコーダーに)そのスペース要件とその境界を示すようにしました。

ascii 範囲 (0-127) の unicode コード ポイントの UTF-8 エンコーディング。

0xxx xxxx  (in binary)

  • x は、エンコード中にコードポイントを格納するために予約された実際のスペースを示します。
  • 先頭の 0 は、UTF-8 デコーダに対して、このコードポイントが 1 バイトしか必要としないことを示すフラグです。
  • をエンコードする際、UTF-8 はその特定の範囲内のコード ポイントの値を変更しません (つまり、UTF-8 でエンコードされた 65 は 65 でもあります)。Unicode と ASCII が同じ範囲で互換性があることを考慮すると、付随的に UTF-8 と ASCII もその範囲で互換性があることになります。

例:'B' の Unicode コードポイントは '0x42' またはバイナリで 0100 0010 です(前述のように、ASCII でも同じです)。UTF-8 でエンコードした後は、次のようになります。

0xxx xxxx  <-- UTF-8 encoding for Unicode code points 0 to 127
*100 0010  <-- Unicode code point 0x42
0100 0010  <-- UTF-8 encoded (exactly the same)

127 以上の Unicode コードポイント (non-ascii) の UTF-8 エンコーディング。

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)

  • 先頭のビット '110' は UTF-8 デコーダに対して、2 バイトでエンコードされたコードポイントの先頭を示し、一方 '1110' は 3 バイト、11110 は 4 バイトなどを示します。
  • 内側の '10' フラグビットは、内部バイトの開始を知らせるために使用されます。
  • は、エンコード後に Unicode コードポイント値が格納されるスペースをマークします。

例: 'é' Unicode コードポイントは 0xe9 (233) です。

1110 1001    <-- 0xe9

UTF-8はこの値をエンコードするとき、127より大きく2048より小さい値であると判断し、2バイトでエンコードするようにします。

110x xxxx 10xx xxxx   <-- UTF-8 encoding for Unicode 128-2047
***0 0011 **10 1001   <-- 0xe9
1100 0011 1010 1001   <-- 'é' after UTF-8 encoding
C    3    A    9

UTF-8エンコード後の0xe9ユニコードコードポイントは0xc3a9になります。これは、まさに端末がそれを受け取る方法です。もしあなたの端末が latin-1 (非 Unicode レガシー エンコードの 1 つ) を使用して文字列をデコードするように設定されている場合、é と表示されますが、これは latin-1 の 0xc3 が Ã、0xa9 が © を指すという偶然によります。