Mengatur penyandian yang benar saat memipakan stdout dengan Python

343

Saat menyalurkan output dari program Python, juru bahasa Python menjadi bingung tentang penyandian dan mengaturnya ke None. Ini berarti program seperti ini:

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

akan berfungsi dengan baik saat dijalankan secara normal, tetapi gagal dengan:

UnicodeEncodeError: 'ascii' codec tidak dapat menyandikan karakter u '\ xa0' di posisi 0: ordinal tidak dalam jangkauan (128)

bila digunakan dalam urutan pipa.

Apa cara terbaik untuk membuat ini berfungsi saat pemipaan? Bisakah saya katakan saja untuk menggunakan pengkodean apa pun shell / filesystem / apa pun yang menggunakan?

Saran yang saya lihat sejauh ini adalah untuk memodifikasi situs Anda.py secara langsung, atau hardcoding defaultencoding menggunakan hack ini:

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

Apakah ada cara yang lebih baik untuk membuat perpipaan bekerja?

Joakim Lundborg
sumber
1
Lihat juga stackoverflow.com/questions/4545661/…
ShreevatsaR
2
Jika Anda memiliki masalah ini di windows, Anda juga dapat menjalankan chcp 65001sebelum menjalankan skrip Anda. Ini dapat memiliki masalah, tetapi seringkali membantu, dan tidak memerlukan banyak pengetikan (kurang dari set PYTHONIOENCODING=utf_8).
Tomasz Gandor
Perintah chcp tidak sama dengan pengaturan PYTHONIOENCODING. Saya pikir chcp hanyalah konfigurasi untuk terminal itu sendiri dan tidak ada hubungannya dengan menulis ke file (yang adalah apa yang Anda lakukan ketika mem-piping stdout). Cobalah setx PYTHONENCODING utf-8membuatnya permanen jika Anda ingin menyimpan pengetikan.
ejm
Saya menghadapi masalah yang agak terkait, dan menemukan solusi di sini -> stackoverflow.com/questions/48782529/…
bkrishna2006

Jawaban:

162

Kode Anda berfungsi saat dijalankan dalam skrip karena Python mengkodekan output ke apa pun yang menggunakan aplikasi terminal Anda. Jika Anda mengirim pipa, Anda harus menyandikannya sendiri.

Aturan praktisnya adalah: Selalu gunakan Unicode secara internal. Dekode apa yang Anda terima, dan sandi apa yang Anda kirim.

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

Contoh didaktik lain adalah program Python untuk mengkonversi antara ISO-8859-1 dan UTF-8, membuat semuanya huruf besar di antaranya.

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)

Mengatur pengkodean default sistem adalah ide yang buruk, karena beberapa modul dan pustaka yang Anda gunakan dapat bergantung pada fakta bahwa itu adalah ASCII. Jangan lakukan itu.

nosklo
sumber
11
Masalahnya adalah bahwa pengguna tidak ingin menentukan penyandian secara eksplisit. Dia hanya ingin menggunakan Unicode untuk IO. Dan pengkodean yang ia gunakan haruslah pengkodean yang ditentukan dalam pengaturan lokal, bukan dalam pengaturan aplikasi terminal. AFAIK, Python 3 menggunakan pengkodean lokal dalam kasus ini. Mengubah sys.stdoutsepertinya cara yang lebih menyenangkan.
Andrey Vlasovskikh
4
Pengkodean / penguraian setiap string secara jelas terikat untuk menyebabkan bug ketika panggilan pengkodean atau penguraian sandi hilang atau ditambahkan sekali ke suatu tempat. Pengkodean keluaran dapat diatur ketika output adalah terminal, sehingga dapat diatur ketika output bukan terminal. Bahkan ada lingkungan LC_CTYPE standar untuk menentukannya. Ini adalah tetapi dengan python bahwa itu tidak menghormati ini.
Rasmus Kaj
65
Jawaban ini salah. Anda seharusnya tidak mengkonversi secara manual pada setiap input dan output dari program Anda; itu rapuh dan benar-benar tidak dapat dipelihara.
Glenn Maynard
29
@ Glenn Maynard: jadi apa jawaban yang tepat untuk IYO? Akan lebih bermanfaat untuk memberi tahu kami daripada hanya mengatakan 'Jawaban ini salah'
smci
14
@smci: jawabannya adalah jangan memodifikasi skrip Anda, tetapkan PYTHONIOENCODINGjika Anda mengarahkan ulang stdout skrip ke Python 2.
jfs
168

Pertama, mengenai solusi ini:

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

Tidak praktis untuk mencetak secara eksplisit dengan penyandian yang diberikan setiap waktu. Itu akan menjadi berulang dan rawan kesalahan.

Solusi yang lebih baik adalah mengubah sys.stdoutpada awal program Anda, untuk menyandikan dengan penyandian yang dipilih. Berikut adalah salah satu solusi yang saya temukan di Python: Bagaimana sys.stdout.encoding dipilih? , khususnya komentar oleh "toka":

import sys
import codecs
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
Craig McQueen
sumber
7
Sayangnya, mengubah sys.stdout untuk menerima hanya unicode memecah banyak perpustakaan yang mengharapkannya menerima bytestrings yang disandikan.
nosklo
6
nosklo: Lalu bagaimana cara kerjanya dengan andal dan otomatis ketika output adalah terminal?
Rasmus Kaj
3
@Rasmus Kaj: cukup tentukan fungsi pencetakan unicode Anda sendiri dan gunakan setiap kali Anda ingin mencetak unicode: def myprint(unicodeobj): print unicodeobj.encode('utf-8')- Anda secara otomatis mendeteksi pengkodean terminal dengan memeriksa sys.stdout.encoding, tetapi Anda harus mempertimbangkan kasus di mana itu berada None(yaitu ketika mengarahkan output ke file) jadi Anda memerlukan fungsi yang terpisah pula.
nosklo
3
@nosklo: Ini tidak membuat sys.stdout hanya menerima Unicode. Anda bisa meneruskan str dan unicode ke StreamWriter.
Glenn Maynard
9
Saya menganggap jawaban ini ditujukan untuk python2. Hati-hati dengan ini pada kode yang dimaksudkan untuk mendukung python2 dan python3 . Bagi saya itu melanggar hal-hal ketika dijalankan di bawah python3.
wim
130

Anda mungkin ingin mencoba mengubah variabel lingkungan "PYTHONIOENCODING" menjadi "utf_8". Saya telah menulis sebuah halaman tentang cobaan saya dengan masalah ini .

Tl; dr dari posting 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))

Memberi anda

utf_8
False
ANSI_X3.4-1968
ascii
utf_8
ö ☺ ☻
daveagp
sumber
2
Mengubah sys.stdout.encoding mungkin tidak bekerja, tetapi mengubah sys.stdout tidak bekerja: sys.stdout = codecs.getwriter(encoding)(sys.stdout). Ini dapat dilakukan dari dalam program python, sehingga pengguna tidak dipaksa untuk mengatur variabel env.
blueFast
7
@ jeckyll2hide: PYTHONIOENCODINGtidak berfungsi. Bagaimana byte ditafsirkan sebagai teks didefinisikan oleh lingkungan pengguna . Skrip Anda tidak boleh mengasumsikan dan menentukan lingkungan pengguna pengkodean karakter apa yang digunakan. Jika Python tidak mengambil pengaturan secara otomatis maka PYTHONIOENCODINGdapat diatur untuk skrip Anda. Anda seharusnya tidak membutuhkannya kecuali output diarahkan ke file / pipa.
jfs
8
+1. Jujur saya pikir itu adalah bug Python. Ketika saya mengarahkan output, saya ingin byte yang sama itu berada di terminal, tetapi dalam file. Mungkin ini bukan untuk semua orang tapi ini standar yang bagus. Menabrak keras tanpa penjelasan tentang operasi sepele yang biasanya "hanya berfungsi" adalah default yang buruk.
SnakE
@SnakE: satu-satunya cara saya dapat merasionalisasi mengapa implementasi Python secara sengaja akan menegakkan pilihan besi dan pilihan permanen pengkodean pada stdout pada saat startup, mungkin untuk mencegah hal-hal buruk yang dikodekan keluar nanti. Atau mengubahnya hanyalah fitur yang tidak diterapkan, dalam hal ini memungkinkan pengguna untuk mengubahnya nanti akan menjadi permintaan fitur Python yang masuk akal.
daveagp
2
@daveagp Maksud saya adalah, perilaku program saya tidak harus bergantung pada apakah itu diarahkan atau tidak --- kecuali saya benar-benar menginginkannya, dalam hal ini saya mengimplementasikannya sendiri. Python berperilaku bertentangan dengan pengalaman saya dengan alat konsol lainnya. Ini melanggar prinsip kejutan terkecil. Saya menganggap ini cacat desain kecuali ada alasan yang sangat kuat.
Snake
62
export PYTHONIOENCODING=utf-8

melakukan pekerjaan itu, tetapi tidak dapat mengaturnya di python itu sendiri ...

yang dapat kami lakukan adalah memverifikasi jika tidak mengatur dan memberi tahu pengguna untuk menyetelnya sebelum skrip panggilan dengan:

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)

Perbarui untuk membalas komentar: masalah baru saja ada ketika mengirim pesan ke stdout. Saya menguji dalam Fedora 25 Python 2.7.13

python --version
Python 2.7.13

kucing b.py

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

print sys.stdout.encoding

menjalankan ./b.py

UTF-8

menjalankan ./b.py | kurang

None
Sérgio
sumber
2
Pemeriksaan itu tidak berfungsi di Python 2.7.13. sys.stdout.encodingdiatur secara otomatis berdasarkan nilai LC_CTYPElokal.
amfetamachine
1
mail.python.org/pipermail/python-list/2011-June/605938.html contoh masih berfungsi, yaitu ketika Anda menggunakan ./a.py> out.txt sys.stdout.encoding Tidak Ada
Sérgio
Saya memiliki masalah yang sama dengan skrip sinkronisasi dari Backblaze B2 dan ekspor PYTHONIOENCODING = utf-8 memecahkan masalah saya. Python 2.7 pada Debian Stretch.
0x3333
5

Saya memiliki masalah serupa minggu lalu . Itu mudah untuk diperbaiki di IDE saya (PyCharm).

Inilah perbaikan saya:

Mulai dari bilah menu PyCharm: File -> Settings ... -> Editor -> File Encodings, kemudian atur: "IDE Encoding", "Project Encoding" dan "Encoding default untuk file properti" ALL ke UTF-8 dan dia sekarang bekerja seperti pesona.

Semoga ini membantu!

CLaFarge
sumber
4

Versi yang bisa diperdebatkan dari jawaban 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

Pemakaian:

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

Saya bisa "mengotomatisasi" itu dengan panggilan ke:

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

Ya, mungkin untuk mendapatkan loop tak terbatas di sini jika "setenv" ini gagal.

jno
sumber
1
menarik, tetapi sebuah pipa sepertinya tidak senang dengan hal ini
n611x007
2

Saya hanya berpikir saya akan menyebutkan sesuatu di sini yang saya harus menghabiskan waktu lama untuk bereksperimen sebelum saya akhirnya menyadari apa yang sedang terjadi. Ini mungkin sangat jelas bagi semua orang di sini sehingga mereka tidak repot-repot menyebutkannya. Tapi itu akan membantu saya jika mereka melakukannya, maka pada prinsip itu ...!

NB: Saya menggunakan Jython secara khusus, v 2.7, jadi mungkin saja ini tidak berlaku untuk CPython ...

NB2: dua baris pertama file .py saya di sini adalah:

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

Mekanisme konstruksi string "%" (AKA "interpolasi operator") menyebabkan masalah TAMBAHAN juga ... Jika pengkodean default "lingkungan" adalah ASCII dan Anda mencoba melakukan sesuatu seperti

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

Anda tidak akan kesulitan menjalankan di Eclipse ... Di Windows CLI (jendela DOS) Anda akan menemukan bahwa penyandiannya adalah kode halaman 850 (OS Windows 7 saya) atau yang serupa, yang setidaknya dapat menangani karakter aksen Eropa, jadi akan bekerja.

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

juga akan bekerja.

Jika, OTOH, Anda mengarahkan ke file dari CLI, pengkodean stdout akan menjadi None, yang akan menjadi standar ASCII (pada OS saya), yang tidak akan dapat menangani salah satu dari cetakan di atas ... (pengkodean menakutkan kesalahan).

Jadi, Anda mungkin berpikir untuk mengarahkan ulang stdout Anda dengan menggunakan

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

dan coba jalankan dalam pemipaan CLI ke file ... Anehnya, cetak A di atas akan berfungsi ... Tapi cetak B di atas akan membuang kesalahan penyandian! Namun berikut ini akan berfungsi OK:

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

Kesimpulan saya datang ke (sementara) adalah bahwa jika sebuah string yang ditentukan untuk menjadi string Unicode menggunakan awalan "u" diajukan ke mekanisme penanganan% tampaknya melibatkan penggunaan pengkodean lingkungan default, terlepas dari apakah Anda telah mengatur stdout untuk mengarahkan ulang!

Bagaimana orang berurusan dengan ini adalah masalah pilihan. Saya akan menyambut pakar Unicode untuk mengatakan mengapa ini terjadi, apakah saya salah dalam beberapa hal, apa solusi yang disukai untuk ini, apakah itu juga berlaku untuk CPython , apakah itu terjadi pada Python 3, dll., Dll.

mike rodent
sumber
Itu tidak aneh, itu karena "fréd"urutan byte dan bukan string Unicode, sehingga codecs.getwriterpembungkus akan meninggalkannya sendiri. Anda membutuhkan pemimpin u, atau from __future__ import unicode_literals.
Matthias Urlichs
@MatthiasUrlichs OK ... terima kasih ... Tapi saya hanya menemukan pengkodean salah satu aspek IT yang paling menyebalkan. Dari mana Anda mendapatkan pemahaman Anda? Sebagai contoh, saya baru saja mengirim pertanyaan lain tentang penyandian di sini: stackoverflow.com/questions/44483067/... : ini tentang Java, Eclipse, Cygwin & Gradle. Jika keahlian Anda sejauh ini, tolong bantu ... di atas semua itu saya ingin tahu di mana harus belajar lebih banyak!
mike rodent
1

Saya mengalami masalah ini dalam aplikasi lawas, dan sulit untuk mengidentifikasi di mana apa yang dicetak. Saya membantu diri saya dengan hack ini:

# 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)

Di atas skrip saya, test.py:

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

Perhatikan bahwa ini mengubah SEMUA panggilan untuk mencetak untuk menggunakan penyandian, sehingga konsol Anda akan mencetak ini:

$ python test.py
b'Axwell \xce\x9b Ingrosso'
penilai
sumber
1

Di Windows, saya sering mengalami masalah ini ketika menjalankan kode Python dari editor (seperti Sublime Text), tetapi tidak jika menjalankannya dari command-line.

Dalam hal ini, periksa parameter editor Anda. Dalam kasus SublimeText, ini Python.sublime-buildmenyelesaikannya:

{
  "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
sumber