UnicodeDecodeError lors de la redirection vers un fichier

100

Je lance cet extrait deux fois, dans le terminal Ubuntu (encodage défini sur utf-8), une fois avec ./test.pypuis avec ./test.py >out.txt:

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

Sans redirection, il imprime des déchets. Avec la redirection, j'obtiens un UnicodeDecodeError. Quelqu'un peut-il expliquer pourquoi j'obtiens l'erreur uniquement dans le deuxième cas, ou mieux encore donner une explication détaillée de ce qui se passe derrière le rideau dans les deux cas?

zedoo
la source
Cette réponse pourrait également être utile.
tzot le
Lorsque j'essaye de reproduire votre découverte, j'obtiens un UnicodeEncodeError, pas un UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Réponses:

252

La clé de ces problèmes d'encodage est de comprendre qu'il existe en principe deux concepts distincts de "chaîne" : (1) chaîne de caractères et (2) chaîne / tableau d' octets. Cette distinction a été longtemps ignorée du fait de l'ubiquité historique des encodages ne dépassant pas 256 caractères (ASCII, Latin-1, Windows-1252, Mac OS Roman,…): ces encodages mappent un ensemble de caractères communs à nombres entre 0 et 255 (c'est-à-dire octets); l'échange relativement limité de fichiers avant l'avènement du web rendait tolérable cette situation d'encodages incompatibles, car la plupart des programmes pouvaient ignorer le fait qu'il y avait plusieurs encodages tant qu'ils produisaient du texte qui restait sur le même système d'exploitation: de tels programmes traiter le texte comme des octets (via le codage utilisé par le système d'exploitation). La vue correcte et moderne sépare correctement ces deux concepts de chaîne, en fonction des deux points suivants:

  1. Les personnages sont pour la plupart sans rapport avec les ordinateurs : on peut les dessiner sur un tableau noir, etc., comme par exemple بايثون, 中 蟒 et 🐍. Les «caractères» pour les machines incluent également les «instructions de dessin» comme par exemple les espaces, le retour chariot, les instructions pour définir le sens d'écriture (pour l'arabe, etc.), les accents, etc. Une très grande liste de caractères est incluse dans le standard Unicode ; il couvre la plupart des personnages connus.

  2. D'un autre côté, les ordinateurs doivent représenter des caractères abstraits d'une manière ou d'une autre: pour cela, ils utilisent des tableaux d'octets (nombres compris entre 0 et 255), car leur mémoire est constituée de blocs d'octets. Le processus nécessaire qui convertit les caractères en octets est appelé encodage . Ainsi, un ordinateur nécessite un encodage pour représenter des caractères. Tout texte présent sur votre ordinateur est encodé (jusqu'à ce qu'il soit affiché), qu'il soit envoyé à un terminal (qui attend des caractères encodés d'une manière spécifique), ou enregistré dans un fichier. Afin d'être affichés ou correctement «compris» (par exemple, par l'interpréteur Python), les flux d'octets sont décodés en caractères. Quelques encodages(UTF-8, UTF-16,…) sont définis par Unicode pour sa liste de caractères (Unicode définit donc à la fois une liste de caractères et des encodages pour ces caractères - il y a encore des endroits où l'on voit l'expression «encodage Unicode» comme un façon de faire référence à l'omniprésent UTF-8, mais c'est une terminologie incorrecte, car Unicode fournit plusieurs encodages).

En résumé, les ordinateurs doivent représenter en interne des caractères avec des octets , et ils le font via deux opérations:

Encodage : caractères → octets

Décodage : octets → caractères

Certains encodages ne peuvent pas encoder tous les caractères (par exemple, ASCII), tandis que (certains) encodages Unicode vous permettent d'encoder tous les caractères Unicode. Le codage n'est pas non plus nécessairement unique , car certains caractères peuvent être représentés soit directement, soit sous forme de combinaison (par exemple d'un caractère de base et d'accents).

Notez que le concept de nouvelle ligne ajoute une couche de complication , car il peut être représenté par différents (contrôle) caractères qui dépendent du système d'exploitation (ce qui est la raison de Python en mode lecture de fichier universel newline ).

Maintenant, ce que j'ai appelé « caractère » ci - dessus est ce que Unicode appelle un « caractère perçu par l' utilisateur ». Un seul caractère perçu par l'utilisateur peut parfois être représenté en Unicode en combinant des parties de caractère (caractère de base, accents,…) trouvés à différents index dans la liste Unicode, qui sont appelés " points de code " - ces points de codes peuvent être combinés pour former un "cluster de graphèmes". Unicode conduit ainsi à un troisième concept de chaîne, constitué d'une séquence de points de code Unicode, qui se situe entre des chaînes d'octets et de caractères, et qui est plus proche de ces dernières. Je les appellerai " chaînes Unicode " (comme dans Python 2).

Alors que Python peut imprimer des chaînes de caractères (perçus par l'utilisateur), les chaînes Python non octets sont essentiellement des séquences de points de code Unicode , et non de caractères perçus par l'utilisateur. Les valeurs de point de code sont celles utilisées dans la syntaxe de chaîne Python \uet \UUnicode. Ils ne doivent pas être confondus avec le codage d'un caractère (et ne doivent pas avoir de relation avec lui: les points de code Unicode peuvent être codés de différentes manières).

Cela a une conséquence importante: la longueur d'une chaîne Python (Unicode) est son nombre de points de code, qui n'est pas toujours son nombre de caractères perçus par l'utilisateur : ainsi s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) donne 각 len 3malgré savoir un seul utilisateur perçu (coréen) caractère (car il est représenté avec 3 points de code - même si ce n'est pas obligatoire, comme le print("\uac01")montre). Cependant, dans de nombreuses circonstances pratiques, la longueur d'une chaîne correspond au nombre de caractères perçus par l'utilisateur, car de nombreux caractères sont généralement stockés par Python en tant que point de code Unicode unique.

En Python 2 , les chaînes Unicode sont appelées… "Chaînes Unicode" ( unicodetype, forme littérale u"…"), tandis que les tableaux d'octets sont des "chaînes" ( strtype, où le tableau d'octets peut par exemple être construit avec des chaînes littérales "…"). Dans Python 3 , les chaînes Unicode sont simplement appelées "chaînes" ( strtype, forme littérale "…"), tandis que les tableaux d'octets sont des "octets" ( bytestype, forme littérale b"…"). En conséquence, quelque chose comme "🐍"[0]donne un résultat différent dans Python 2 ( '\xf0', un octet) et Python 3 ( "🐍", le premier et le seul caractère).

Avec ces quelques points clés, vous devriez être capable de comprendre la plupart des questions liées à l'encodage!


Normalement, lorsque vous imprimez u"…" sur un terminal , vous ne devriez pas avoir de déchets: Python connaît l'encodage de votre terminal. En fait, vous pouvez vérifier le codage attendu par le terminal:

% 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

Si vos caractères d'entrée peuvent être encodés avec l'encodage du terminal, Python le fera et enverra les octets correspondants à votre terminal sans se plaindre. Le terminal fera alors de son mieux pour afficher les caractères après avoir décodé les octets d'entrée (au pire, la police du terminal n'a pas certains des caractères et imprimera une sorte de blanc à la place).

Si vos caractères d'entrée ne peuvent pas être codés avec le codage du terminal, cela signifie que le terminal n'est pas configuré pour afficher ces caractères. Python se plaindra (en Python avec un UnicodeEncodeErrorcar la chaîne de caractères ne peut pas être encodée d'une manière qui convient à votre terminal). La seule solution possible est d'utiliser un terminal capable d'afficher les caractères (soit en configurant le terminal pour qu'il accepte un encodage pouvant représenter vos caractères, soit en utilisant un programme de terminal différent). Ceci est important lorsque vous distribuez des programmes qui peuvent être utilisés dans différents environnements: les messages que vous imprimez doivent être représentables dans le terminal de l'utilisateur. Parfois, il est donc préférable de s'en tenir aux chaînes qui ne contiennent que des caractères ASCII.

Cependant, lorsque vous redirigez ou redirigez la sortie de votre programme, il n'est généralement pas possible de savoir quel est le codage d'entrée du programme récepteur, et le code ci-dessus renvoie un codage par défaut: Aucun (Python 2.7) ou 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

Le codage de stdin, stdout et stderr peut cependant être défini via la PYTHONIOENCODINGvariable d'environnement, si nécessaire:

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

Si l'impression sur un terminal ne produit pas ce que vous attendez, vous pouvez vérifier que le codage UTF-8 que vous avez mis manuellement est correct; par exemple, votre premier caractère ( \u001A) n'est pas imprimable, si je ne me trompe pas .

Sur http://wiki.python.org/moin/PrintFails , vous pouvez trouver une solution comme la suivante, pour 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

Pour Python 3, vous pouvez vérifier l' une des questions posées précédemment sur StackOverflow.

Eric O Lebigot
la source
2
@singularity: Merci! J'ai ajouté quelques infos pour Python 3.
Eric O Lebigot
2
Merci mec! J'avais besoin de cette explication depuis si longtemps ... C'est dommage que je ne puisse vous donner qu'un seul vote pour.
mik01aj du
3
Je suis heureux d'avoir aidé, @ m01! L'une des motivations pour écrire cette réponse était qu'il y avait de nombreuses pages sur le Web sur Unicode et Python, mais j'ai trouvé que malgré leur intérêt, elles ne m'ont jamais complètement permis de résoudre des problèmes d'encodage concrets ... Je crois vraiment qu'en gardant à l'esprit le les principes trouvés dans cette réponse et prendre le temps de les utiliser pour résoudre des problèmes de codage concrets aide beaucoup.
Eric O Lebigot
3
C'est de loin la meilleure explication de l'unicode et de python. Le HOWTO Python Unicode devrait être remplacé par ceci.
stantonk
1
Ici, permettez-moi de dessiner le caractère «de remplacement de droite à gauche» sur ce tableau…
icktoofay
20

Python encode toujours les chaînes Unicode lors de l'écriture dans un terminal, un fichier, un tube, etc. Lors de l'écriture dans un terminal, Python peut généralement déterminer l'encodage du terminal et l'utiliser correctement. Lors de l'écriture dans un fichier ou un tube, Python utilise par défaut l'encodage 'ascii', sauf indication contraire explicite. Python peut savoir quoi faire lors du transfert de la sortie via la PYTHONIOENCODINGvariable d'environnement. Un shell peut définir cette variable avant de rediriger la sortie Python vers un fichier ou un tube afin que le codage correct soit connu.

Dans votre cas, vous avez imprimé 4 caractères inhabituels que votre terminal ne prend pas en charge dans sa police. Voici quelques exemples pour aider à expliquer le comportement, avec des caractères qui sont réellement pris en charge par mon terminal (qui utilise cp437, pas UTF-8).

Exemple 1

Notez que le #codingcommentaire indique le codage dans lequel le fichier source est enregistré. J'ai choisi utf8 pour pouvoir prendre en charge les caractères dans la source que mon terminal ne pouvait pas. L'encodage est redirigé vers stderr afin qu'il puisse être vu lorsqu'il est redirigé vers un fichier.

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

Sortie (exécuter directement à partir du terminal)

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

Python a correctement déterminé l'encodage du terminal.

Sortie (redirigé vers le fichier)

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 n'a pas pu déterminer le codage (Aucun) donc utilisé par défaut «ascii». ASCII ne prend en charge que la conversion des 128 premiers caractères d'Unicode.

Sortie (redirigé vers le fichier, PYTHONIOENCODING = cp437)

cp437

et mon fichier de sortie était correct:

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

Exemple 2

Maintenant, je vais ajouter un caractère dans la source qui n'est pas pris en charge par mon terminal:

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

Sortie (exécuter directement à partir du terminal)

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>

Mon terminal n'a pas compris ce dernier caractère chinois.

Sortie (exécuter directement, PYTHONIOENCODING = 437: remplacer)

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

Les gestionnaires d'erreurs peuvent être spécifiés avec l'encodage. Dans ce cas, les caractères inconnus ont été remplacés par ?. ignoreet xmlcharrefreplacequelques autres options. Lorsque vous utilisez UTF8 (qui prend en charge l'encodage de tous les caractères Unicode), les remplacements ne seront jamais effectués, mais la police utilisée pour afficher les caractères doit toujours les prendre en charge.

Mark Tolonen
la source
Ce n'est pas tout à fait vrai que "Lors de l'écriture dans un fichier ou un tube, Python utilise par défaut l'encodage 'ascii', sauf indication contraire explicite.". En fait, Python 3 utilise UTF-8, sur Mac OS X / Fink.
Eric O Lebigot
2
Oui, Python 3 utilise par défaut «utf8», mais d'après l'exemple de l'OP, il utilise Python 2.X, qui par défaut est «ascii».
Mark Tolonen
Je n'ai pas pu obtenir une sortie correcte en manipulant PYTHONIOENCODING. Faire print string.encode("UTF-8")comme suggéré par @Ismail a fonctionné pour moi.
tripleee
vous pouvez voir les caractères chinois si votre police les prend en charge même si la chcppage de codes ne les prend pas en charge. Pour éviter UnicodeEncodeError: 'charmap', vous pouvez installer le win-unicode-consolepackage.
jfs
Mon problème est que la CLI python-gitlab imprime bien les caractères chinois dans cmd mais les caractères sont des déchets après avoir été redirigés dans des fichiers. PYTHONIOENCODING=utf-8résout le problème.
ElpieKay
12

Encodez-le lors de l'impression

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

En effet, lorsque vous exécutez le script manuellement, python l'encode avant de le sortir du terminal, lorsque vous le dirigez, python ne l'encode pas lui-même, vous devez donc encoder manuellement lors des E / S.

ismail
la source
4
Il ne répond toujours pas à la question que WTH continue ici. Pourquoi, à l'improviste, il décide d'encoder uniquement lorsqu'il est redirigé, alors que cela est censé être complètement transparent pour le processus.
Maxim Sloyko
Pourquoi Python ne l'encode-t-il pas lors de la redirection? Est-ce que python vérifie et décide explicitement qu'il fera les choses différemment juste pour être difficile?
Arafangion
1
python a-t-il même un moyen de distinguer les deux situations? Je pensais (jusqu'à présent ...) qu'il n'y avait aucun moyen de le savoir.
zedoo
4
Python peut vérifier si la sortie est un terminal, si sa sortie vers un tube, alors le type de terminal sera "stupide". Je suppose que "stupide" devrait vous dire pourquoi Python n'essaye pas de faire quoi que ce soit d'automatiquement dans ce cas, il peut échouer.
ismail
1
il produit mojibake si l'environnement utilise un encodage de caractères incompatible avec utf-8 (par exemple, il est courant sous Windows). Ne codez pas en dur le codage des caractères de votre environnement dans votre script. Configurez vos paramètres régionaux, ou PYTHONIOENCODING, ou installez win-unicode-console(Windows), ou acceptez un paramètre de ligne de commande (si vous devez).
jfs