Définition du codage correct lors de la canalisation de la sortie standard en Python

343

Lors de la transmission de la sortie d'un programme Python, l'interpréteur Python est confus au sujet de l'encodage et le définit sur Aucun. Cela signifie un programme comme celui-ci:

# -*- coding: utf-8 -*-
print u"åäö"

fonctionnera correctement lorsqu'il est exécuté normalement, mais échouera avec:

UnicodeEncodeError: le codec 'ascii' ne peut pas coder le caractère u '\ xa0' en position 0: l'ordinal n'est pas dans la plage (128)

lorsqu'il est utilisé dans une séquence de tuyaux.

Quelle est la meilleure façon de faire fonctionner cela lors de la tuyauterie? Puis-je simplement lui dire d'utiliser l'encodage du shell / système de fichiers / tout ce qui est utilisé?

La suggestion que j'ai vue jusqu'à présent est de modifier directement votre site.py, ou de coder en dur le codage par défaut en utilisant ce hack:

# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
print u"åäö"

Existe-t-il une meilleure façon de faire fonctionner la tuyauterie?

Joakim Lundborg
la source
1
Voir aussi stackoverflow.com/questions/4545661/…
ShreevatsaR
2
Si vous rencontrez ce problème sous Windows, vous pouvez également l'exécuter chcp 65001avant d'exécuter votre script. Cela peut avoir des problèmes, mais cela aide souvent et ne nécessite pas beaucoup de saisie (moins que set PYTHONIOENCODING=utf_8).
Tomasz Gandor
La commande chcp est différente de la définition du PYTHONIOENCODING. Je pense que chcp est juste une configuration pour le terminal lui-même et n'a rien à voir avec l'écriture dans un fichier (ce que vous faites lorsque vous canalisez stdout). Essayez setx PYTHONENCODING utf-8de le rendre permanent si vous souhaitez enregistrer la saisie.
ejm
J'ai rencontré un problème quelque peu connexe et j'ai trouvé une solution ici -> stackoverflow.com/questions/48782529/…
bkrishna2006

Réponses:

162

Votre code fonctionne lorsqu'il est exécuté dans un script car Python code la sortie dans le codage utilisé par votre application de terminal. Si vous canalisez, vous devez le coder vous-même.

Une règle d'or est la suivante: utilisez toujours Unicode en interne. Décodez ce que vous recevez et encodez ce que vous envoyez.

# -*- coding: utf-8 -*-
print u"åäö".encode('utf-8')

Un autre exemple didactique est un programme Python pour convertir entre ISO-8859-1 et UTF-8, mettant tout en majuscule entre les deux.

import sys
for line in sys.stdin:
    # Decode what you receive:
    line = line.decode('iso8859-1')

    # Work with Unicode internally:
    line = line.upper()

    # Encode what you send:
    line = line.encode('utf-8')
    sys.stdout.write(line)

La définition du codage par défaut du système est une mauvaise idée, car certains modules et bibliothèques que vous utilisez peuvent s'appuyer sur le fait qu'il est ASCII. Ne le fais pas.

nosklo
la source
11
Le problème est que l'utilisateur ne veut pas spécifier explicitement l'encodage. Il veut juste utiliser Unicode pour IO. Et l'encodage qu'il utilise doit être un encodage spécifié dans les paramètres régionaux, pas dans les paramètres de l'application du terminal. AFAIK, Python 3 utilise un encodage local dans ce cas. Changer sys.stdoutsemble être une manière plus agréable.
Andrey Vlasovskikh
4
L'encodage / décodage de chaque chaîne de manière explicite est susceptible de provoquer des bogues lorsqu'un appel d'encodage ou de décodage est manquant ou ajouté une fois à beaucoup quelque part. Le codage de sortie peut être défini lorsque la sortie est un terminal, il peut donc être défini lorsque la sortie n'est pas un terminal. Il existe même un environnement LC_CTYPE standard pour le spécifier. C'est un mais en python qu'il ne respecte pas ça.
Rasmus Kaj
65
Cette réponse est fausse. Vous ne devez pas convertir manuellement sur chaque entrée et sortie de votre programme; c'est cassant et complètement incontrôlable.
Glenn Maynard
29
@Glenn Maynard: alors quelle est IYO la bonne réponse? Il est plus utile de nous dire que de simplement dire 'Cette réponse est fausse'
smci
14
@smci: la réponse est de ne pas modifier votre script, définissez PYTHONIOENCODINGsi vous redirigez la
sortie
168

Tout d'abord, concernant cette solution:

# -*- coding: utf-8 -*-
print u"åäö".encode('utf-8')

Il n'est pas pratique d'imprimer explicitement avec un encodage donné à chaque fois. Ce serait répétitif et sujet aux erreurs.

Une meilleure solution consiste à changer sys.stdoutau début de votre programme, à encoder avec un encodage sélectionné. Voici une solution que j'ai trouvée sur Python: Comment est choisi sys.stdout.encoding? , en particulier un commentaire de "toka":

import sys
import codecs
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
Craig McQueen
la source
7
malheureusement, changer sys.stdout pour accepter uniquement unicode casse beaucoup de bibliothèques qui s'attendent à ce qu'il accepte des bytestrings encodés.
nosklo
6
nosklo: Comment cela peut-il fonctionner de manière fiable et automatique lorsque la sortie est un terminal?
Rasmus Kaj
3
@Rasmus Kaj: définissez simplement votre propre fonction d'impression unicode et utilisez-la chaque fois que vous souhaitez imprimer unicode: def myprint(unicodeobj): print unicodeobj.encode('utf-8')- vous détectez automatiquement l'encodage du terminal en inspectant sys.stdout.encoding, mais vous devez considérer le cas où il se trouve None(c'est-à-dire lors de la redirection de la sortie vers un fichier) vous avez donc besoin d'une fonction distincte de toute façon.
nosklo
3
@nosklo: Cela n'oblige pas sys.stdout à accepter uniquement Unicode. Vous pouvez passer à la fois str et unicode à un StreamWriter.
Glenn Maynard du
9
Je suppose que cette réponse était destinée à python2. Soyez prudent avec cela sur le code qui est destiné à prendre en charge à la fois python2 et python3 . Pour moi, ça casse des choses lorsqu'il est exécuté sous python3.
wim
130

Vous pouvez essayer de changer la variable d'environnement "PYTHONIOENCODING" en "utf_8". J'ai écrit une page sur mon calvaire avec ce problème .

Tl; dr de l'article de blog:

import sys, locale, os
print(sys.stdout.encoding)
print(sys.stdout.isatty())
print(locale.getpreferredencoding())
print(sys.getfilesystemencoding())
print(os.environ["PYTHONIOENCODING"])
print(chr(246), chr(9786), chr(9787))

vous donne

utf_8
False
ANSI_X3.4-1968
ascii
utf_8
ö ☺ ☻
daveagp
la source
2
Changement sys.stdout.encoding ne fonctionne pas peut - être, mais en changeant sys.stdout fonctionne: sys.stdout = codecs.getwriter(encoding)(sys.stdout). Cela peut être fait à partir du programme python, donc l'utilisateur n'est pas obligé de définir une variable env.
blueFast
7
@ jeckyll2hide: PYTHONIOENCODINGfonctionne. L'interprétation des octets en tant que texte est définie par l' environnement utilisateur . Votre script ne doit pas supposer et dicter à l'environnement utilisateur le codage de caractères à utiliser. Si Python ne récupère pas les paramètres automatiquement, vous PYTHONIOENCODINGpouvez le définir pour votre script. Vous ne devriez pas en avoir besoin à moins que la sortie ne soit redirigée vers un fichier / pipe.
jfs
8
+1. Honnêtement, je pense que c'est un bug Python. Lorsque je redirige la sortie, je veux ces mêmes octets qui seraient sur le terminal, mais dans un fichier. Ce n'est peut-être pas pour tout le monde mais c'est un bon défaut. Se planter dur sans explication sur une opération triviale qui "fonctionne normalement" est généralement un mauvais défaut.
SnakE
@SnakE: la seule façon pour moi de rationaliser pourquoi l'implémentation de Python imposerait intentionnellement un choix à toute épreuve et permanent d'encodage sur stdout au démarrage, pourrait être afin d'empêcher que des trucs mal encodés ne sortent plus tard. Ou le changer n'est qu'une fonctionnalité non implémentée, auquel cas permettre à l'utilisateur de la modifier plus tard serait une demande de fonctionnalité Python raisonnable.
daveagp
2
@daveagp Mon point est que le comportement de mon programme ne devrait pas dépendre de sa redirection ou non --- sauf si je le veux vraiment, auquel cas je l'implémente moi-même. Python se comporte contrairement à mon expérience avec tout autre outil de console. Cela viole le principe de la moindre surprise. Je considère cela comme un défaut de conception, sauf s'il existe une justification très solide.
SnakE
62
export PYTHONIOENCODING=utf-8

faire le travail, mais ne peut pas le définir sur python lui-même ...

ce que nous pouvons faire, c'est vérifier si le paramètre n'est pas défini et dire à l'utilisateur de le définir avant d'appeler le script avec:

if __name__ == '__main__':
    if (sys.stdout.encoding is None):
        print >> sys.stderr, "please set python env PYTHONIOENCODING=UTF-8, example: export PYTHONIOENCODING=UTF-8, when write to stdout."
        exit(1)

Mise à jour pour répondre au commentaire: le problème existe juste lors du canalisation vers stdout. J'ai testé dans Fedora 25 Python 2.7.13

python --version
Python 2.7.13

chat b.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-
import sys

print sys.stdout.encoding

en cours d'exécution ./b.py

UTF-8

en cours d'exécution ./b.py | Moins

None
Sérgio
la source
2
Cette vérification ne fonctionne pas dans Python 2.7.13. sys.stdout.encodingest automatiquement défini en fonction de la LC_CTYPEvaleur locale.
amphetamachine du
1
mail.python.org/pipermail/python-list/2011-June/605938.html l'exemple y fonctionne toujours, c'est-à-dire lorsque vous utilisez ./a.py> out.txt sys.stdout.encoding est None
Sérgio
J'ai eu un problème similaire avec un script de synchronisation de Backblaze B2 et l'exportation PYTHONIOENCODING = utf-8 a résolu mon problème. Python 2.7 sur Debian Stretch.
0x3333
5

J'ai eu un problème similaire la semaine dernière . C'était facile à corriger dans mon IDE (PyCharm).

Voici ma solution:

À partir de la barre de menus de PyCharm: Fichier -> Paramètres ... -> Éditeur -> Encodages de fichiers, puis définissez: "Encodage IDE", "Encodage de projet" et "Encodage par défaut pour les fichiers de propriétés" TOUS en UTF-8 et elle travaille maintenant comme un charme.

J'espère que cela t'aides!

CLaFarge
la source
4

Une version aseptisée discutable de la réponse de Craig McQueen.

import sys, codecs
class EncodedOut:
    def __init__(self, enc):
        self.enc = enc
        self.stdout = sys.stdout
    def __enter__(self):
        if sys.stdout.encoding is None:
            w = codecs.getwriter(self.enc)
            sys.stdout = w(sys.stdout)
    def __exit__(self, exc_ty, exc_val, tb):
        sys.stdout = self.stdout

Usage:

with EncodedOut('utf-8'):
    print u'ÅÄÖåäö'
Tompa
la source
2

Je pourrais "l'automatiser" avec un appel à:

def __fix_io_encoding(last_resort_default='UTF-8'):
  import sys
  if [x for x in (sys.stdin,sys.stdout,sys.stderr) if x.encoding is None] :
      import os
      defEnc = None
      if defEnc is None :
        try:
          import locale
          defEnc = locale.getpreferredencoding()
        except: pass
      if defEnc is None :
        try: defEnc = sys.getfilesystemencoding()
        except: pass
      if defEnc is None :
        try: defEnc = sys.stdin.encoding
        except: pass
      if defEnc is None :
        defEnc = last_resort_default
      os.environ['PYTHONIOENCODING'] = os.environ.get("PYTHONIOENCODING",defEnc)
      os.execvpe(sys.argv[0],sys.argv,os.environ)
__fix_io_encoding() ; del __fix_io_encoding

Oui, il est possible d'obtenir une boucle infinie ici si ce "setenv" échoue.

jno
la source
1
intéressant, mais une pipe ne semble pas en être heureuse
n611x007
2

Je pensais juste que je mentionnerais quelque chose ici que j'ai dû consacrer longtemps à expérimenter avant de finalement réaliser ce qui se passait. Cela peut être si évident pour tout le monde ici qu'ils n'ont pas pris la peine de le mentionner. Mais cela m'aurait aidé s'ils l'avaient fait, donc selon ce principe ...!

NB: J'utilise spécifiquement Jython , v 2.7, donc il est possible que cela ne s'applique pas à CPython ...

NB2: les deux premières lignes de mon fichier .py sont:

# -*- coding: utf-8 -*-
from __future__ import print_function

Le mécanisme de construction de chaîne "%" (AKA "opérateur d'interpolation") provoque également des problèmes ADDITIONNELS ... Si le codage par défaut de "l'environnement" est ASCII et que vous essayez de faire quelque chose comme

print( "bonjour, %s" % "fréd" )  # Call this "print A"

Vous n'aurez aucune difficulté à exécuter dans Eclipse ... Dans une CLI Windows (fenêtre DOS), vous constaterez que l'encodage est la page de codes 850 (mon système d'exploitation Windows 7) ou quelque chose de similaire, qui peut gérer au moins les caractères accentués européens, donc il va travailler.

print( u"bonjour, %s" % "fréd" ) # Call this "print B"

fonctionnera également.

Si, OTOH, vous dirigez vers un fichier à partir de la CLI, l'encodage stdout sera None, qui sera par défaut ASCII (sur mon système d'exploitation de toute façon), qui ne pourra gérer aucune des impressions ci-dessus ... (encodage redouté Erreur).

Alors vous pourriez penser à rediriger votre stdout en utilisant

sys.stdout = codecs.getwriter('utf8')(sys.stdout)

et essayez d'exécuter dans la tuyauterie CLI vers un fichier ... Très bizarrement, l'impression A ci-dessus fonctionnera ... Mais l'impression B ci-dessus générera l'erreur de codage! Cependant, les éléments suivants fonctionneront correctement:

print( u"bonjour, " + "fréd" ) # Call this "print C"

La conclusion à laquelle je suis parvenu (provisoirement) est que si une chaîne qui est spécifiée pour être une chaîne Unicode utilisant le préfixe "u" est soumise au mécanisme de gestion%, elle semble impliquer l'utilisation du codage d'environnement par défaut, indépendamment de si vous avez défini stdout pour rediriger!

La façon dont les gens gèrent cela est une question de choix. Je souhaiterais la bienvenue à un expert Unicode pour dire pourquoi cela se produit, si je me trompe d'une manière ou d'une autre, quelle est la solution préférée à cela, si cela s'applique également à CPython , si cela se produit en Python 3, etc., etc.

Mike rongeur
la source
Ce n'est pas étrange, c'est parce que "fréd"c'est une séquence d'octets et non une chaîne Unicode, donc le codecs.getwriterwrapper le laissera tranquille. Vous avez besoin d'un leader u, ou from __future__ import unicode_literals.
Matthias Urlichs
@MatthiasUrlichs OK ... merci ... Mais je trouve juste l'encodage l'un des aspects les plus exaspérants de l'informatique. D'où tirez-vous votre compréhension? Par exemple, je viens de poster une autre question sur l'encodage ici: stackoverflow.com/questions/44483067/… : il s'agit de Java, Eclipse, Cygwin & Gradle. Si votre expertise va aussi loin, aidez-moi ... surtout je voudrais savoir où en savoir plus!
mike rodent
1

J'ai rencontré ce problème dans une application héritée, et il était difficile d'identifier où ce qui était imprimé. Je me suis aidé avec ce hack:

# encoding_utf8.py
import codecs
import builtins


def print_utf8(text, **kwargs):
    print(str(text).encode('utf-8'), **kwargs)


def print_utf8(fn):
    def print_fn(*args, **kwargs):
        return fn(str(*args).encode('utf-8'), **kwargs)
    return print_fn


builtins.print = print_utf8(print)

En plus de mon script, test.py:

import encoding_utf8
string = 'Axwell Λ Ingrosso'
print(string)

Notez que cela modifie TOUS les appels à imprimer pour utiliser un encodage, donc votre console imprimera ceci:

$ python test.py
b'Axwell \xce\x9b Ingrosso'
cesseur
la source
1

Sous Windows, j'ai eu ce problème très souvent lors de l'exécution d'un code Python à partir d'un éditeur (comme Sublime Text), mais pas si vous l'exécutiez à partir de la ligne de commande.

Dans ce cas, vérifiez les paramètres de votre éditeur. Dans le cas de SublimeText, cela l'a Python.sublime-buildrésolu:

{
  "cmd": ["python", "-u", "$file"],
  "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
  "selector": "source.python",
  "encoding": "utf8",
  "env": {"PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
}
Basj
la source