UnicodeDecodeError beim Umleiten in eine Datei

100

Ich führe dieses Snippet zweimal im Ubuntu-Terminal aus (Codierung auf utf-8 eingestellt), einmal mit ./test.pyund dann mit ./test.py >out.txt:

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

Ohne Umleitung wird Müll gedruckt. Bei Umleitung erhalte ich einen UnicodeDecodeError. Kann jemand erklären, warum ich den Fehler nur im zweiten Fall erhalte, oder noch besser ausführlich erklären, was in beiden Fällen hinter dem Vorhang vor sich geht?

Zedoo
quelle
Diese Antwort könnte auch hilfreich sein.
Zot
Wenn ich versuche, Ihren Befund zu replizieren, erhalte ich einen UnicodeEncodeError, keinen UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Antworten:

252

Der ganze Schlüssel zu solchen Codierung Problemen ist zu verstehen , dass es im Prinzip zwei verschiedenen Konzepte von „string“ : (1) Kette von Zeichen , und (2) string / Array von Bytes. Diese Unterscheidung wurde lange Zeit aufgrund der historischen Allgegenwart von Codierungen mit nicht mehr als 256 Zeichen (ASCII, Latin-1, Windows-1252, Mac OS Roman,…) weitgehend ignoriert: Diese Codierungen ordnen eine Reihe gemeinsamer Zeichen zu Zahlen zwischen 0 und 255 (dh Bytes); Der relativ begrenzte Austausch von Dateien vor dem Aufkommen des Webs machte diese Situation inkompatibler Codierungen erträglich, da die meisten Programme die Tatsache ignorieren konnten, dass es mehrere Codierungen gab, solange sie Text produzierten, der auf demselben Betriebssystem blieb: solche Programme würden es einfach tun Behandeln Sie Text als Bytes (durch die vom Betriebssystem verwendete Codierung). Die richtige, moderne Ansicht trennt diese beiden Zeichenfolgenkonzepte anhand der folgenden zwei Punkte ordnungsgemäß:

  1. Zeichen haben meistens nichts mit Computern zu tun: Man kann sie auf eine Kreidetafel usw. zeichnen, wie zum Beispiel بايثون, 中 蟒 und 🐍. "Zeichen" für Maschinen enthalten auch "Zeichenanweisungen" wie z. B. Leerzeichen, Wagenrücklauf, Anweisungen zum Festlegen der Schreibrichtung (für Arabisch usw.), Akzente usw. Der Unicode- Standard enthält eine sehr große Zeichenliste . Es deckt die meisten bekannten Zeichen ab.

  2. Auf der anderen Seite müssen Computer abstrakte Zeichen in irgendeiner Weise darstellen: Dazu verwenden sie Arrays von Bytes (einschließlich Zahlen zwischen 0 und 255), da ihr Speicher in Byte-Blöcken vorliegt. Der erforderliche Prozess, der Zeichen in Bytes konvertiert, wird als Codierung bezeichnet . Daher benötigt ein Computer eine Codierung, um Zeichen darzustellen. Jeder auf Ihrem Computer vorhandene Text wird codiert (bis er angezeigt wird), unabhängig davon, ob er an ein Terminal gesendet wird (das auf eine bestimmte Weise codierte Zeichen erwartet) oder in einer Datei gespeichert wird. Um angezeigt oder richtig "verstanden" zu werden (beispielsweise durch den Python-Interpreter), werden Byteströme in Zeichen dekodiert . Ein paar Kodierungen(UTF-8, UTF-16,…) werden von Unicode für seine Zeichenliste definiert (Unicode definiert somit sowohl eine Liste von Zeichen als auch Codierungen für diese Zeichen - es gibt immer noch Stellen, an denen der Ausdruck "Unicode-Codierung" als Möglichkeit, auf das allgegenwärtige UTF-8 zu verweisen, dies ist jedoch eine falsche Terminologie, da Unicode mehrere Codierungen bereitstellt .

Zusammenfassend muss gesagt werden, dass Computer Zeichen intern mit Bytes darstellen müssen , und zwar durch zwei Operationen:

Kodierung : Zeichen → Bytes

Dekodierung : Bytes → Zeichen

Einige Codierungen können nicht alle Zeichen (z. B. ASCII) codieren, während (einige) Unicode-Codierungen das Codieren aller Unicode-Zeichen ermöglichen. Die Codierung ist auch nicht unbedingt eindeutig , da einige Zeichen entweder direkt oder als Kombination dargestellt werden können (z. B. eines Basiszeichens und von Akzenten).

Beachten Sie, dass das Newline- Konzept eine zusätzliche Komplikationsebene darstellt , da es durch verschiedene (Steuer-) Zeichen dargestellt werden kann, die vom Betriebssystem abhängen (dies ist der Grund für den universellen Lesemodus für Newline-Dateien in Python ).

Was ich oben als "Zeichen" bezeichnet habe, nennt Unicode ein "vom Benutzer wahrgenommenes Zeichen ". Ein einzelnes vom Benutzer wahrgenommenes Zeichen kann manchmal in Unicode dargestellt werden, indem Zeichenteile (Basiszeichen, Akzente usw.) kombiniert werden, die an verschiedenen Indizes in der Unicode-Liste gefunden werden und als " Codepunkte " bezeichnet werden. Diese Codepunkte können zu einer Form kombiniert werden ein "Graphemcluster". Unicode führt somit zu einem dritten Konzept von Zeichenfolgen, das aus einer Folge von Unicode-Codepunkten besteht, die zwischen Byte- und Zeichenfolgen liegen und näher an letzteren liegen. Ich werde sie " Unicode-Strings " nennen (wie in Python 2).

Während Python Zeichenfolgen von (vom Benutzer wahrgenommenen) Zeichen drucken kann , sind Python-Nicht-Byte-Zeichenfolgen im Wesentlichen Sequenzen von Unicode-Codepunkten , nicht von vom Benutzer wahrgenommenen Zeichen. Die Codepunktwerte werden in der Python- \uund \UUnicode-Zeichenfolgensyntax verwendet. Sie sollten nicht mit der Codierung eines Zeichens verwechselt werden (und müssen keine Beziehung dazu haben: Unicode-Codepunkte können auf verschiedene Arten codiert werden).

Dies hat eine wichtige Konsequenz: die Länge eines Python (Unicode) string ist die Anzahl der Codepunkte, die ist nicht immer die Zahl der Benutzer wahrgenommenen Zeichen : also s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) gibt 각 len 3trotz seiner einzigen benutzer wahrgenommen (Korean) mit Zeichen (weil es mit 3 Codepunkten dargestellt wird - auch wenn es nicht muss, wie print("\uac01")zeigt). Unter vielen praktischen Umständen entspricht die Länge einer Zeichenfolge jedoch der Anzahl der vom Benutzer wahrgenommenen Zeichen, da viele Zeichen normalerweise von Python als einzelner Unicode-Codepunkt gespeichert werden.

In Python 2 werden Unicode-Zeichenfolgen als "Unicode-Zeichenfolgen" ( unicodeTyp, Literalform u"…") bezeichnet, während Byte-Arrays "Zeichenfolgen" sind ( strTyp, bei dem das Array von Bytes beispielsweise mit Zeichenfolgenliteralen erstellt werden kann "…"). In Python 3 werden Unicode-Zeichenfolgen einfach als "Zeichenfolgen" ( strTyp, Literalform "…") bezeichnet, während Byte-Arrays "Bytes" ( bytesTyp, Literalform b"…") sind. Infolgedessen "🐍"[0]ergibt so etwas wie ein anderes Ergebnis in Python 2 ( '\xf0'ein Byte) und Python 3 ( "🐍"das erste und einzige Zeichen).

Mit diesen wenigen wichtigen Punkten sollten Sie in der Lage sein, die meisten Fragen im Zusammenhang mit der Codierung zu verstehen!


Normalerweise sollten Sie beim Drucken u"…" auf einem Terminal keinen Müll bekommen: Python kennt die Codierung Ihres Terminals. Tatsächlich können Sie überprüfen, welche Codierung das Terminal erwartet:

% python
Python 2.7.6 (default, Nov 15 2013, 15:20:37) 
[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print sys.stdout.encoding
UTF-8

Wenn Ihre Eingabezeichen mit der Codierung des Terminals codiert werden können, wird Python dies tun und die entsprechenden Bytes an Ihr Terminal senden, ohne sich zu beschweren. Das Terminal bemüht sich dann, die Zeichen nach dem Decodieren der Eingabebytes anzuzeigen (im schlimmsten Fall enthält die Terminalschrift einige der Zeichen nicht und druckt stattdessen eine Art Leerzeichen).

Wenn Ihre Eingabezeichen nicht mit der Codierung des Terminals codiert werden können, bedeutet dies, dass das Terminal nicht für die Anzeige dieser Zeichen konfiguriert ist. Python wird sich beschweren (in Python mit einem, UnicodeEncodeErrorda die Zeichenfolge nicht in einer Weise codiert werden kann, die zu Ihrem Terminal passt). Die einzig mögliche Lösung besteht darin, ein Terminal zu verwenden, das die Zeichen anzeigen kann (entweder indem Sie das Terminal so konfigurieren, dass es eine Codierung akzeptiert, die Ihre Zeichen darstellen kann, oder indem Sie ein anderes Terminalprogramm verwenden). Dies ist wichtig, wenn Sie Programme verteilen, die in verschiedenen Umgebungen verwendet werden können: Nachrichten, die Sie drucken, sollten im Terminal des Benutzers darstellbar sein. Manchmal ist es daher am besten, sich an Zeichenfolgen zu halten, die nur ASCII-Zeichen enthalten.

Wenn Sie jedoch die Ausgabe Ihres Programms umleiten oder weiterleiten , ist es im Allgemeinen nicht möglich, die Eingabecodierung des empfangenden Programms zu kennen, und der obige Code gibt eine Standardcodierung zurück: Keine (Python 2.7) oder UTF-8 ( Python 3):

% python2.7 -c "import sys; print sys.stdout.encoding" | cat
None
% python3.4 -c "import sys; print(sys.stdout.encoding)" | cat
UTF-8

Die Codierung von stdin, stdout und stderr kann jedoch werden gesetzt durch die PYTHONIOENCODINGUmgebungsvariable, falls erforderlich:

% PYTHONIOENCODING=UTF-8 python2.7 -c "import sys; print sys.stdout.encoding" | cat
UTF-8

Wenn der Druck auf ein Terminal nicht das liefert, was Sie erwarten, können Sie überprüfen, ob die manuell eingegebene UTF-8-Codierung korrekt ist. Zum Beispiel ist Ihr erstes Zeichen ( \u001A) nicht druckbar, wenn ich mich nicht irre .

Unter http://wiki.python.org/moin/PrintFails finden Sie eine Lösung wie die folgende für Python 2.x:

import codecs
import locale
import sys

# Wrap sys.stdout into a StreamWriter to allow writing unicode.
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) 

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

Für Python 3 können Sie eine der zuvor in StackOverflow gestellten Fragen überprüfen .

Eric O Lebigot
quelle
2
@singularity: Danke! Ich habe einige Informationen für Python 3 hinzugefügt.
Eric O Lebigot
2
Danke mann! Ich brauchte diese Erklärung so lange ... Es ist schade, dass ich Ihnen nur eine Gegenstimme geben kann.
mik01aj
3
Ich bin froh, Ihnen geholfen zu haben, @ m01! Eine der Beweggründe für das Schreiben dieser Antwort war, dass es im Internet viele Seiten über Unicode und Python gab, aber ich fand, dass sie mir, obwohl sie interessant waren, nie vollständig erlaubten, konkrete Codierungsprobleme zu lösen. Ich glaube das wirklich, indem ich die Die in dieser Antwort enthaltenen Prinzipien und die Zeit , sie bei der Lösung konkreter Codierungsprobleme zu verwenden, helfen sehr.
Eric O Lebigot
3
Dies ist zweifellos die beste Erklärung für Unicode und Python, die es je gab. Das Python Unicode HOWTO sollte durch dieses ersetzt werden.
Stantonk
1
Lassen Sie mich hier das Zeichen „Überschreiben von rechts nach links“ auf diese Tafel
zeichnen
20

Python codiert beim Schreiben in ein Terminal, eine Datei, eine Pipe usw. immer Unicode-Zeichenfolgen. Beim Schreiben in ein Terminal kann Python normalerweise die Codierung des Terminals bestimmen und korrekt verwenden. Beim Schreiben in eine Datei oder Pipe verwendet Python standardmäßig die ASCII-Codierung, sofern nicht ausdrücklich anders angegeben. Python kann gesagt werden, was zu tun ist, wenn die Ausgabe durch die PYTHONIOENCODINGUmgebungsvariable geleitet wird. Eine Shell kann diese Variable festlegen, bevor die Python-Ausgabe in eine Datei oder Pipe umgeleitet wird, damit die richtige Codierung bekannt ist.

In Ihrem Fall haben Sie 4 ungewöhnliche Zeichen gedruckt, die Ihr Terminal in seiner Schriftart nicht unterstützt hat. Hier sind einige Beispiele zur Erläuterung des Verhaltens mit Zeichen, die tatsächlich von meinem Terminal unterstützt werden (das cp437 und nicht UTF-8 verwendet).

Beispiel 1

Beachten Sie, dass der #codingKommentar die Codierung angibt, in der die Quelldatei gespeichert ist. Ich habe utf8 gewählt, um Zeichen in der Quelle zu unterstützen, die mein Terminal nicht unterstützen konnte. Die Codierung wurde an stderr umgeleitet, sodass sie angezeigt wird, wenn sie an eine Datei umgeleitet wird.

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ'
print >>sys.stderr,sys.stdout.encoding
print uni

Ausgabe (direkt vom Terminal ausführen)

cp437
αßΓπΣσµτΦΘΩδ∞φ

Python hat die Codierung des Terminals korrekt bestimmt.

Ausgabe (in Datei umgeleitet)

None
Traceback (most recent call last):
  File "C:\ex.py", line 5, in <module>
    print uni
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-13: ordinal not in range(128)

Python konnte die Codierung (Keine) nicht bestimmen, daher wurde die Standardeinstellung 'ascii' verwendet. ASCII unterstützt nur die Konvertierung der ersten 128 Zeichen von Unicode.

Ausgabe (in Datei umgeleitet, PYTHONIOENCODING = cp437)

cp437

und meine Ausgabedatei war korrekt:

C:\>type out.txt
αßΓπΣσµτΦΘΩδ∞φ

Beispiel 2

Jetzt werde ich ein Zeichen in die Quelle einfügen, das von meinem Terminal nicht unterstützt wird:

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ马' # added Chinese character at end.
print >>sys.stderr,sys.stdout.encoding
print uni

Ausgabe (direkt vom Terminal ausführen)

cp437
Traceback (most recent call last):
  File "C:\ex.py", line 5, in <module>
    print uni
  File "C:\Python26\lib\encodings\cp437.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character u'\u9a6c' in position 14: character maps to <undefined>

Mein Terminal hat das letzte chinesische Schriftzeichen nicht verstanden.

Ausgabe (direkt ausführen, PYTHONIOENCODING = 437: ersetzen)

cp437
αßΓπΣσµτΦΘΩδ∞φ?

Mit der Codierung können Fehlerbehandlungsroutinen angegeben werden. In diesem Fall wurden unbekannte Zeichen durch ersetzt ?. ignoreund xmlcharrefreplacesind einige andere Optionen. Bei Verwendung von UTF8 (das das Codieren aller Unicode-Zeichen unterstützt) werden keine Ersetzungen vorgenommen, aber die zur Anzeige der Zeichen verwendete Schriftart muss diese weiterhin unterstützen.

Mark Tolonen
quelle
Es ist nicht genau richtig, dass "Python beim Schreiben in eine Datei oder Pipe standardmäßig die 'ascii'-Codierung verwendet, sofern nicht ausdrücklich anders angegeben." Tatsächlich verwendet Python 3 UTF-8 unter Mac OS X / Fink.
Eric O Lebigot
2
Ja, Python 3 ist standardmäßig 'utf8', aber basierend auf dem Beispiel des OP verwendet er Python 2.X, das standardmäßig 'ascii' ist.
Mark Tolonen
Ich konnte durch Manipulation keine korrekte Ausgabe erhalten PYTHONIOENCODING. Das zu tun, print string.encode("UTF-8")was von @Ismail vorgeschlagen wurde, hat für mich funktioniert.
Tripleee
Sie können chinesische Schriftzeichen sehen, wenn Ihre Schrift sie unterstützt, auch wenn die chcpCodepage sie nicht unterstützt. Um dies zu vermeiden UnicodeEncodeError: 'charmap', können Sie das win-unicode-consolePaket installieren .
JFS
Mein Problem ist, dass die Python-Gitlab-CLI chinesische Zeichen gut in cmd druckt, aber die Zeichen sind Müll, nachdem sie in Dateien umgeleitet wurden. PYTHONIOENCODING=utf-8löst das Problem.
ElpieKay
12

Codieren Sie es beim Drucken

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni.encode("utf-8")

Dies liegt daran, dass wenn Sie das Skript manuell ausführen, Python es codiert, bevor es an das Terminal ausgegeben wird. Wenn Sie es weiterleiten, codiert Python es nicht selbst, sodass Sie es manuell ausführen müssen, wenn Sie E / A ausführen.

ismail
quelle
4
Die Frage, die WTH hier stellt, wird immer noch nicht beantwortet. Aus heiterem Himmel entscheidet es sich, nur bei Umleitung zu codieren, wenn dies für den Prozess vollständig transparent sein soll.
Maxim Sloyko
Warum codiert Python es nicht, wenn eine Umleitung durchgeführt wird? Überprüft und entscheidet Python explizit, dass es die Dinge anders macht, nur um schwierig zu sein?
Arafangion
1
Hat Python überhaupt eine Möglichkeit, die beiden Situationen zu unterscheiden? Ich dachte (bis jetzt ...), dass es unmöglich ist, es zu wissen.
Zedoo
4
Python kann prüfen, ob es sich bei der Ausgabe um ein Terminal handelt. Wenn die Ausgabe an eine Pipe erfolgt, ist der Terminaltyp "dumm". Ich denke, "dumm" sollte Ihnen sagen, warum Python in diesem Fall nicht versucht, etwas Automatisches zu tun, es kann fehlschlagen.
Ismail
1
Es erzeugt Mojibake, wenn die Umgebung eine Zeichenkodierung verwendet, die nicht mit utf-8 kompatibel ist (z. B. unter Windows üblich). Codieren Sie die Zeichenkodierung Ihrer Umgebung in Ihrem Skript nicht fest. Konfigurieren Sie Ihr Gebietsschema oder PYTHONIOENCODING oder installieren Sie win-unicode-console(Windows) oder akzeptieren Sie einen Befehlszeilenparameter (falls erforderlich).
JFS