Warum druckt Python Unicode-Zeichen, wenn die Standardcodierung ASCII ist?

139

Aus der Python 2.6-Shell:

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

Ich habe erwartet, dass nach der print-Anweisung entweder Kauderwelsch oder ein Fehler auftritt, da das Zeichen "é" nicht Teil von ASCII ist und ich keine Codierung angegeben habe. Ich glaube, ich verstehe nicht, was ASCII als Standardcodierung bedeutet.

BEARBEITEN

Ich habe die Bearbeitung in den Bereich Antworten verschoben und sie wie vorgeschlagen akzeptiert.

Michael Ekoka
quelle
6
Es wäre ziemlich schön, wenn Sie diese Bearbeitung stattdessen in eine Antwort verwandeln und akzeptieren könnten .
Mercator
2
Das Drucken '\xe9'in einem für UTF-8 konfigurierten Terminal wird nicht gedruckt é. Es wird ein Ersatzzeichen (normalerweise ein Fragezeichen) gedruckt, da \xe9es sich nicht um eine gültige UTF-8-Sequenz handelt (es fehlen zwei Bytes, die diesem führenden Byte hätten folgen sollen). Es wird mit Sicherheit nicht stattdessen als Latin-1 interpretiert.
Martijn Pieters
2
@MartijnPieters Ich vermute, Sie haben möglicherweise den Teil überflogen, in dem ich angegeben habe, dass das Terminal bei der Ausgabe \xe9zum Drucken in ISO-8859-1 (latin1) dekodiert ist é.
Michael Ekoka
2
Ah ja, ich habe diesen Teil vermisst; Das Terminal hat eine Konfiguration, die sich von der Shell unterscheidet. Prüfen.
Martijn Pieters
Ich habe die Antwort durchgesehen, aber tatsächlich habe ich die Zeichenfolge ohne das Präfix u für Python 2.7. Warum wird dieser immer noch als Unicode behandelt? (meine sys.getdefaultencoding () ist ascii)
dtc

Antworten:

104

Ich denke, wir können dank der Kleinigkeiten aus verschiedenen Antworten eine Erklärung zusammenstellen.

Beim Versuch, eine Unicode-Zeichenfolge u '\ xe9' zu drucken, versucht Python implizit, diese Zeichenfolge mit dem derzeit in sys.stdout.encoding gespeicherten Codierungsschema zu codieren. Python übernimmt diese Einstellung tatsächlich aus der Umgebung, aus der es initiiert wurde. Wenn keine ordnungsgemäße Codierung aus der Umgebung gefunden werden kann, wird erst dann die Standardcodierung ASCII wiederhergestellt.

Zum Beispiel verwende ich eine Bash-Shell, deren Codierung standardmäßig UTF-8 ist. Wenn ich Python davon starte, nimmt es diese Einstellung auf und verwendet sie:

$ python

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

Lassen Sie uns für einen Moment die Python-Shell verlassen und die Bash-Umgebung mit einer falschen Codierung festlegen:

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

Starten Sie dann die Python-Shell erneut und stellen Sie sicher, dass sie tatsächlich zu ihrer Standard-ASCII-Codierung zurückkehrt.

$ python

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

Bingo!

Wenn Sie jetzt versuchen, ein Unicode-Zeichen außerhalb von ASCII auszugeben, sollten Sie eine nette Fehlermeldung erhalten

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

Beenden wir Python und verwerfen die Bash-Shell.

Wir werden nun beobachten, was passiert, nachdem Python Strings ausgegeben hat. Dazu starten wir zuerst eine Bash-Shell in einem Grafikterminal (ich verwende Gnome Terminal) und stellen das Terminal so ein, dass die Ausgabe mit ISO-8859-1 (auch bekannt als Latin-1) dekodiert wird (Grafikterminals haben normalerweise die Option, Zeichen zu setzen Codierung in einem ihrer Dropdown-Menüs). Beachten Sie, dass dies die Codierung der tatsächlichen Shell-Umgebung nicht ändert , sondern nur die Art und Weise, wie das Terminal selbst die ausgegebene Ausgabe dekodiert, ähnlich wie dies bei einem Webbrowser der Fall ist. Sie können daher die Codierung des Terminals unabhängig von der Shell-Umgebung ändern. Starten wir dann Python von der Shell aus und stellen Sie sicher, dass sys.stdout.encoding auf die Codierung der Shell-Umgebung eingestellt ist (UTF-8 für mich):

$ 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 gibt die Binärzeichenfolge unverändert aus, das Terminal empfängt sie und versucht, ihren Wert mit der Latin-1-Zeichenzuordnung abzugleichen. In Latin-1 ergibt 0xe9 oder 233 das Zeichen "é", und so wird das Terminal angezeigt.

(2) Python versucht, die Unicode-Zeichenfolge implizit mit dem derzeit in sys.stdout.encoding festgelegten Schema zu codieren. In diesem Fall handelt es sich um "UTF-8". Nach der UTF-8-Codierung lautet die resultierende Binärzeichenfolge '\ xc3 \ xa9' (siehe spätere Erläuterung). Das Terminal empfängt den Stream als solchen und versucht, 0xc3a9 mit Latin-1 zu decodieren, aber Latin-1 geht von 0 auf 255 und decodiert daher jeweils nur 1 Byte Streams. 0xc3a9 ist 2 Bytes lang, der Latin-1-Decoder interpretiert es daher als 0xc3 (195) und 0xa9 (169) und ergibt 2 Zeichen: Ã und ©.

(3) Python codiert den Unicode-Codepunkt u '\ xe9' (233) mit dem Latin-1-Schema. Es stellt sich heraus, dass der Bereich für Latin-1-Codepunkte zwischen 0 und 255 liegt und auf genau dasselbe Zeichen wie Unicode in diesem Bereich verweist. Daher ergeben Unicode-Codepunkte in diesem Bereich den gleichen Wert, wenn sie in Latin-1 codiert werden. U '\ xe9' (233), das in Latin-1 codiert ist, ergibt also auch die Binärzeichenfolge '\ xe9'. Das Terminal empfängt diesen Wert und versucht, ihn auf der Latin-1-Zeichenkarte abzugleichen. Genau wie in Fall (1) ergibt es "é" und das wird angezeigt.

Lassen Sie uns nun die Codierungseinstellungen des Terminals aus dem Dropdown-Menü in UTF-8 ändern (so wie Sie die Codierungseinstellungen Ihres Webbrowsers ändern würden). Sie müssen Python nicht stoppen oder die Shell neu starten. Die Codierung des Terminals stimmt jetzt mit der von Python überein. Versuchen wir es noch einmal:

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

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

>>>

(4) Python gibt eine Binärzeichenfolge unverändert aus. Das Terminal versucht, diesen Stream mit UTF-8 zu dekodieren. UTF-8 versteht den Wert 0xe9 jedoch nicht (siehe spätere Erklärung) und kann ihn daher nicht in einen Unicode-Codepunkt konvertieren. Kein Codepunkt gefunden, kein Zeichen gedruckt.

(5) Python versucht, die Unicode-Zeichenfolge implizit mit den Angaben in sys.stdout.encoding zu codieren. Immer noch "UTF-8". Die resultierende Binärzeichenfolge lautet '\ xc3 \ xa9'. Das Terminal empfängt den Stream und versucht, 0xc3a9 auch mit UTF-8 zu dekodieren. Es ergibt den Rückcodewert 0xe9 (233), der auf der Unicode-Zeichenkarte auf das Symbol "é" zeigt. Terminal zeigt "é" an.

(6) Python codiert eine Unicode-Zeichenfolge mit Latin-1 und liefert eine Binärzeichenfolge mit demselben Wert '\ xe9'. Auch für das Terminal ist dies so ziemlich das Gleiche wie in Fall (4).

Schlussfolgerungen: - Python gibt Nicht-Unicode-Zeichenfolgen als Rohdaten aus, ohne die Standardcodierung zu berücksichtigen. Das Terminal zeigt sie nur an, wenn die aktuelle Codierung mit den Daten übereinstimmt. - Python gibt Unicode-Zeichenfolgen aus, nachdem sie mit dem in sys.stdout.encoding angegebenen Schema codiert wurden. - Python erhält diese Einstellung aus der Umgebung der Shell. - Das Terminal zeigt die Ausgabe gemäß seinen eigenen Codierungseinstellungen an. - Die Codierung des Terminals ist unabhängig von der der Shell.


Weitere Details zu Unicode, UTF-8 und Latin-1:

Unicode ist im Grunde eine Zeichentabelle, in der einige Schlüssel (Codepunkte) herkömmlicherweise zugewiesen wurden, um auf einige Symbole zu verweisen. zB durch Konvention wurde entschieden, dass Schlüssel 0xe9 (233) der Wert ist, der auf das Symbol 'é' zeigt. ASCII und Unicode verwenden dieselben Codepunkte von 0 bis 127 wie Latin-1 und Unicode von 0 bis 255. Das heißt, 0x41 zeigt auf 'A' in ASCII, Latin-1 und Unicode, 0xc8 zeigt auf 'Ü' in Latin-1 und Unicode, 0xe9 zeigt in Latin-1 und Unicode auf 'é'.

Bei der Arbeit mit elektronischen Geräten benötigen Unicode-Codepunkte eine effiziente Möglichkeit, elektronisch dargestellt zu werden. Darum geht es bei Codierungsschemata. Es gibt verschiedene Unicode-Codierungsschemata (utf7, UTF-8, UTF-16, UTF-32). Der intuitivste und einfachste Codierungsansatz wäre, einfach den Wert eines Codepunkts in der Unicode-Karte als Wert für seine elektronische Form zu verwenden. Unicode verfügt derzeit jedoch über mehr als eine Million Codepunkte, was bedeutet, dass einige von ihnen 3 Bytes benötigen ausgedrückt. Um effizient mit Text arbeiten zu können, wäre eine 1: 1-Zuordnung eher unpraktisch, da alle Codepunkte unabhängig von ihrem tatsächlichen Bedarf auf genau demselben Speicherplatz mit mindestens 3 Bytes pro Zeichen gespeichert werden müssten.

Die meisten Codierungsschemata weisen Mängel hinsichtlich des Platzbedarfs auf, die wirtschaftlichsten decken nicht alle Unicode-Codepunkte ab, zum Beispiel deckt ASCII nur die ersten 128 ab, während Latin-1 die ersten 256 abdeckt. Andere, die versuchen, umfassender zu sein, enden ebenfalls verschwenderisch sein, da sie mehr Bytes als nötig benötigen, selbst für übliche "billige" Zeichen. UTF-16 verwendet beispielsweise mindestens 2 Bytes pro Zeichen, einschließlich derjenigen im ASCII-Bereich ('B', das 65 ist, erfordert immer noch 2 Bytes Speicher in UTF-16). UTF-32 ist noch verschwenderischer, da alle Zeichen in 4 Bytes gespeichert werden.

UTF-8 hat das Dilemma mit einem Schema, das Codepunkte mit einer variablen Anzahl von Byte-Leerzeichen speichern kann, geschickt gelöst. Als Teil seiner Codierungsstrategie schnürt UTF-8 Codepunkte mit Flag-Bits, die (vermutlich für Decoder) ihren Platzbedarf und ihre Grenzen angeben.

UTF-8-Codierung von Unicode-Codepunkten im ASCII-Bereich (0-127):

0xxx xxxx  (in binary)
  • Die x zeigen den tatsächlichen Speicherplatz an, der zum "Speichern" des Codepunkts während der Codierung reserviert ist
  • Die führende 0 ist ein Flag, das dem UTF-8-Decoder anzeigt, dass dieser Codepunkt nur 1 Byte benötigt.
  • Beim Codieren ändert UTF-8 den Wert der Codepunkte in diesem bestimmten Bereich nicht (dh 65, die in UTF-8 codiert sind, sind ebenfalls 65). In Anbetracht der Tatsache, dass Unicode und ASCII auch in demselben Bereich kompatibel sind, sind UTF-8 und ASCII übrigens auch in diesem Bereich kompatibel.

Beispiel: Der Unicode-Codepunkt für 'B' ist '0x42' oder 0100 0010 in Binärform (wie gesagt, es ist dasselbe in ASCII). Nach der Codierung in UTF-8 wird es:

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)

UTF-8-Codierung von Unicode-Codepunkten über 127 (nicht ASCII):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • Die führenden Bits '110' zeigen dem UTF-8-Decoder den Beginn eines in 2 Bytes codierten Codepunkts an, während '1110' 3 Bytes anzeigt, 11110 4 Bytes anzeigt und so weiter.
  • Die inneren '10'-Flag-Bits werden verwendet, um den Beginn eines inneren Bytes zu signalisieren.
  • Wiederum markieren die x den Bereich, in dem der Unicode-Codepunktwert nach der Codierung gespeichert wird.

Beispiel: 'é' Unicode-Codepunkt ist 0xe9 (233).

1110 1001    <-- 0xe9

Wenn UTF-8 diesen Wert codiert, wird festgestellt, dass der Wert größer als 127 und kleiner als 2048 ist. Daher sollte er in 2 Byte codiert werden:

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

Der 0xe9-Unicode-Code zeigt nach der UTF-8-Codierung auf 0xc3a9. Genau so empfängt das Terminal es. Wenn Ihr Terminal so eingestellt ist, dass Zeichenfolgen mit Latin-1 (einer der Nicht-Unicode-Legacy-Codierungen) dekodiert werden, wird à © angezeigt, da 0xc3 in Latin-1 nur auf à und 0xa9 auf © zeigt.

Michael Ekoka
quelle
6
Hervorragende Erklärung. Jetzt verstehe ich UTF-8!
Doktor Coder
2
Okay, ich habe Ihren gesamten Beitrag in ungefähr 10 Sekunden durchgelesen. Es hieß: "Python ist scheiße, wenn es um Codierung geht."
Andrew
Tolle Erklärung. Könnten Sie diese Frage beantworten?
Maggyero
26

Wenn Unicode-Zeichen auf Standard gedruckt werden, sys.stdout.encodingwird verwendet. Es wird angenommen, dass sich ein Nicht-Unicode-Zeichen in befindet, sys.stdout.encodingund es wird nur an das Terminal gesendet. Auf meinem System (Python 2):

>>> import unicodedata as ud
>>> import sys
>>> sys.stdout.encoding
'cp437'
>>> ud.name(u'\xe9') # U+00E9 Unicode codepoint
'LATIN SMALL LETTER E WITH ACUTE'
>>> ud.name('\xe9'.decode('cp437')) 
'GREEK CAPITAL LETTER THETA'
>>> '\xe9'.decode('cp437') # byte E9 decoded using code page 437 is U+0398.
u'\u0398'
>>> ud.name(u'\u0398')
'GREEK CAPITAL LETTER THETA'
>>> print u'\xe9' # Unicode is encoded to CP437 correctly
é
>>> print '\xe9'  # Byte is just sent to terminal and assumed to be CP437.
Θ

sys.getdefaultencoding() wird nur verwendet, wenn Python keine andere Option hat.

Beachten Sie, dass Python 3.6 oder höher Codierungen unter Windows ignoriert und Unicode-APIs verwendet, um Unicode in das Terminal zu schreiben. Keine UnicodeEncodeError-Warnungen und das richtige Zeichen wird angezeigt, wenn die Schriftart dies unterstützt. Auch wenn die Schriftart dies nicht unterstützt, können die Zeichen vom Terminal in eine Anwendung mit einer unterstützenden Schriftart ausgeschnitten und eingefügt werden, und dies ist korrekt. Aktualisierung!

Mark Tolonen
quelle
8

Die Python-REPL versucht, die zu verwendende Codierung aus Ihrer Umgebung zu ermitteln. Wenn es etwas Vernünftiges findet, funktioniert alles einfach. Es ist, wenn es nicht herausfinden kann, was los ist, dass es Fehler macht.

>>> print sys.stdout.encoding
UTF-8
Ignacio Vazquez-Abrams
quelle
3
Wie würde ich aus Neugier sys.stdout.encoding in ascii ändern?
Michael Ekoka
2
@ TankorSmash Ich komme TypeError: readonly attributeauf 2.7.2
Kos
4

Sie haben eine Codierung angegeben, indem Sie eine explizite Unicode-Zeichenfolge eingeben. Vergleichen Sie die Ergebnisse, wenn Sie das uPräfix nicht verwenden .

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

>>> 

In diesem Fall \xe9übernimmt Python Ihre Standardcodierung (Ascii) und druckt so ... etwas Leeres.

Mark Rushakoff
quelle
1
also wenn ich gut verstehen, wenn ich Unicode - Strings ausdrucken (die Codepunkte), Python geht davon aus, dass ich eine Ausgabe in utf-8, statt nur versucht , mir codiert will geben , was es könnte in ascii gewesen sein?
Michael Ekoka
1
@mike: AFAIK was du gesagt hast ist richtig. Wenn es tut die Unicode - Zeichen gedruckt werden , sondern als ASCII codiert, wäre alles kommen verstümmelt und wahrscheinlich alle Anfänger fragen würden : „Wie kommt kann ich nicht Unicode - Text ausdrucken?“
Mark Rushakoff
2
Danke dir. Eigentlich bin ich einer dieser Anfänger, aber ich komme von Leuten, die etwas Verständnis für Unicode haben, weshalb mich dieses Verhalten ein bisschen abschreckt.
Michael Ekoka
3
R., nicht korrekt, da '\ xe9' nicht im ASCII-Zeichensatz enthalten ist. Nicht-Unicode-Zeichenfolgen werden mit sys.stdout.encoding gedruckt. Unicode-Zeichenfolgen werden vor dem Drucken in sys.stdout.encoding codiert.
Mark Tolonen
0

Für mich geht das:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
user3611630
quelle
1
Billiger schmutziger Hack, der unweigerlich etwas anderes kaputt macht. Es ist nicht schwer, es richtig zu machen!
Chris Johnson
0

Gemäß Python-Standard- / impliziten Zeichenfolgencodierungen und -konvertierungen :

  • Wenn printing unicode, ist es encoded mit <file>.encoding.
    • Wenn das encodingnicht gesetzt ist, wird das unicodeimplizit in konvertiert str(da der Codec dafür ist sys.getdefaultencoding(), dh asciialle nationalen Zeichen würden a verursachen UnicodeEncodeError).
    • Für Standard-Streams wird das encodingaus der Umgebung abgeleitet. Es wird normalerweise für ttyStreams festgelegt (aus den Gebietsschemaeinstellungen des Terminals), wird jedoch wahrscheinlich nicht für Pipes festgelegt
      • Daher print u'\xe9'ist es wahrscheinlich, dass a erfolgreich ist, wenn die Ausgabe an ein Terminal erfolgt, und fehlschlägt, wenn es umgeleitet wird. Eine Lösung besteht darin, encode()den String vor dem printIng mit der gewünschten Codierung zu versehen .
  • Beim printIng strwerden die Bytes unverändert an den Stream gesendet. Welche Glyphen das Terminal anzeigt, hängt von den Ländereinstellungen ab.
ivan_pozdeev
quelle