UnicodeDecodeError saat mengalihkan ke file

100

Saya menjalankan potongan ini dua kali, di terminal Ubuntu (pengkodean diatur ke utf-8), sekali dengan ./test.pydan kemudian dengan ./test.py >out.txt:

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

Tanpa pengalihan mencetak sampah. Dengan pengalihan saya mendapatkan UnicodeDecodeError. Adakah yang bisa menjelaskan mengapa saya mendapatkan kesalahan hanya dalam kasus kedua, atau bahkan lebih baik memberikan penjelasan rinci tentang apa yang terjadi di balik tirai dalam kedua kasus?

zedoo
sumber
Jawaban ini mungkin bisa membantu juga.
tzot
Saat saya mencoba mereplikasi temuan Anda, saya mendapatkan UnicodeEncodeError, bukan UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Jawaban:

252

Kunci utama untuk masalah encoding tersebut adalah untuk memahami bahwa pada prinsipnya ada dua konsep berbeda dari "string" : (1) string karakter , dan (2) string / array byte. Perbedaan ini sebagian besar telah diabaikan untuk waktu yang lama karena penyandian di mana-mana yang bersejarah dengan tidak lebih dari 256 karakter (ASCII, Latin-1, Windows-1252, Mac OS Roman,…): pengkodean ini memetakan sekumpulan karakter umum ke angka antara 0 dan 255 (yaitu byte); pertukaran file yang relatif terbatas sebelum munculnya web membuat situasi penyandian yang tidak kompatibel ini dapat ditoleransi, karena sebagian besar program dapat mengabaikan fakta bahwa ada beberapa penyandiaksaraan selama mereka menghasilkan teks yang tetap pada sistem operasi yang sama: program semacam itu hanya akan perlakukan teks sebagai byte (melalui pengkodean yang digunakan oleh sistem operasi). Tampilan modern yang benar memisahkan kedua konsep string ini dengan tepat, berdasarkan dua poin berikut:

  1. Karakter sebagian besar tidak terkait dengan komputer : seseorang dapat menggambarnya di papan kapur, dll., Seperti misalnya بايثون, 中 蟒 dan 🐍. "Karakter" untuk mesin juga mencakup "instruksi menggambar" seperti misalnya spasi, carriage return, instruksi untuk mengatur arah penulisan (untuk bahasa Arab, dll.), Aksen, dll. Daftar karakter yang sangat besar disertakan dalam standar Unicode ; itu mencakup sebagian besar karakter yang dikenal.

  2. Di sisi lain, komputer memang perlu merepresentasikan karakter abstrak dengan beberapa cara: untuk ini, mereka menggunakan array byte (termasuk angka antara 0 dan 255), karena memori mereka datang dalam potongan byte. Proses yang diperlukan untuk mengubah karakter menjadi byte disebut pengkodean . Jadi, komputer membutuhkan pengkodean untuk mewakili karakter. Teks apa pun yang ada di komputer Anda dikodekan (hingga ditampilkan), apakah itu dikirim ke terminal (yang mengharapkan karakter dikodekan dengan cara tertentu), atau disimpan dalam file. Agar dapat ditampilkan atau "dipahami" dengan benar (oleh, katakanlah, penerjemah Python), aliran byte didekodekan menjadi karakter. Beberapa pengkodean(UTF-8, UTF-16,…) didefinisikan oleh Unicode untuk daftar karakternya (Unicode mendefinisikan kedua daftar karakter dan pengkodean untuk karakter ini — masih ada tempat di mana seseorang melihat ekspresi "Unicode encoding" sebagai cara untuk merujuk ke UTF-8 di mana-mana, tetapi ini adalah terminologi yang salah, karena Unicode menyediakan banyak pengkodean).

Singkatnya, komputer perlu merepresentasikan karakter secara internal dengan byte , dan mereka melakukannya melalui dua operasi:

Pengkodean : karakter → byte

Decoding : byte → karakter

Beberapa pengkodean tidak dapat mengkodekan semua karakter (misalnya, ASCII), sementara (beberapa) pengkodean Unicode memungkinkan Anda untuk mengenkode semua karakter Unicode. Pengkodean juga belum tentu unik , karena beberapa karakter dapat direpresentasikan baik secara langsung atau sebagai kombinasi (misalnya, karakter dasar dan aksen).

Perhatikan bahwa konsep baris baru menambahkan lapisan kerumitan , karena dapat diwakili oleh karakter (kontrol) berbeda yang bergantung pada sistem operasi (ini adalah alasan mode membaca file baris baru universal Python ).

Sekarang, apa yang saya sebut "karakter" di atas adalah apa yang disebut Unicode sebagai " karakter yang dipersepsi pengguna ". Karakter tunggal yang dirasakan pengguna terkadang dapat direpresentasikan dalam Unicode dengan menggabungkan bagian karakter (karakter dasar, aksen,…) yang ditemukan di indeks berbeda dalam daftar Unicode, yang disebut " poin kode " —poin kode ini dapat digabungkan bersama untuk membentuk sebuah "cluster grafem". Unicode dengan demikian mengarah ke konsep string ketiga, yang dibuat dari urutan poin kode Unicode, yang berada di antara byte dan string karakter, dan yang lebih dekat dengan yang terakhir. Saya akan menyebutnya " string Unicode " (seperti di Python 2).

Sementara Python dapat mencetak string karakter (yang dirasakan pengguna), string non-byte Python pada dasarnya adalah urutan poin kode Unicode , bukan karakter yang dipersepsi pengguna. Nilai titik kode adalah yang digunakan dalam sintaks string Python \udan \UUnicode. Mereka tidak boleh bingung dengan pengkodean karakter (dan tidak harus memiliki hubungan apa pun dengannya: Titik kode Unicode dapat dikodekan dengan berbagai cara).

Ini memiliki konsekuensi penting: panjang string Python (Unicode) adalah jumlah poin kodenya, yang tidak selalu jumlah karakter yang dirasakan pengguna : jadi s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) memberi 각 len 3meskipun smemiliki satu yang dirasakan pengguna (Korea) karakter (karena diwakili dengan 3 titik kode — meskipun tidak harus, seperti yang print("\uac01")ditunjukkan). Namun, dalam banyak keadaan praktis, panjang string adalah jumlah karakter yang dianggap pengguna, karena banyak karakter biasanya disimpan oleh Python sebagai titik kode Unicode tunggal.

Dalam Python 2 , string Unicode disebut… "Unicode strings" ( unicodetipe, bentuk literal u"…"), sedangkan array byte adalah "string" ( strjenis, di mana array byte dapat misalnya dibangun dengan string literal "…"). Dalam Python 3 , string Unicode disebut "string" ( strtipe, bentuk literal "…"), sedangkan array byte disebut "byte" ( bytestipe, bentuk literal b"…"). Akibatnya, sesuatu seperti "🐍"[0]memberikan hasil yang berbeda dalam Python 2 ( '\xf0', byte) dan Python 3 ( "🐍", karakter pertama dan satu-satunya).

Dengan beberapa poin kunci ini, Anda seharusnya dapat memahami sebagian besar pertanyaan terkait pengkodean!


Biasanya, saat Anda mencetak u"…" ke terminal , Anda tidak akan mendapatkan sampah: Python mengetahui pengkodean terminal Anda. Nyatanya, Anda dapat memeriksa pengkodean apa yang diharapkan 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

Jika karakter input Anda dapat dikodekan dengan pengkodean terminal, Python akan melakukannya dan akan mengirimkan byte yang sesuai ke terminal Anda tanpa mengeluh. Terminal kemudian akan melakukan yang terbaik untuk menampilkan karakter setelah mendekode byte input (paling buruk font terminal tidak memiliki beberapa karakter dan sebaliknya akan mencetak beberapa jenis kosong).

Jika karakter input Anda tidak dapat dikodekan dengan pengkodean terminal, itu berarti terminal tidak dikonfigurasi untuk menampilkan karakter ini. Python akan mengeluh (dalam Python dengan a UnicodeEncodeErrorkarena string karakter tidak dapat dikodekan dengan cara yang sesuai dengan terminal Anda). Satu-satunya solusi yang mungkin adalah menggunakan terminal yang dapat menampilkan karakter (baik dengan mengkonfigurasi terminal sehingga menerima pengkodean yang dapat mewakili karakter Anda, atau dengan menggunakan program terminal yang berbeda). Ini penting ketika Anda mendistribusikan program yang dapat digunakan di lingkungan yang berbeda: pesan yang Anda cetak harus dapat diwakili di terminal pengguna. Terkadang yang terbaik adalah tetap menggunakan string yang hanya berisi karakter ASCII.

Namun, ketika Anda mengalihkan atau menyalurkan output dari program Anda, maka umumnya tidak mungkin untuk mengetahui apa pengkodean input dari program penerima, dan kode di atas mengembalikan beberapa pengkodean default: Tidak Ada (Python 2.7) atau 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

Pengkodean stdin, stdout dan stderr dapat diatur melalui PYTHONIOENCODINGvariabel lingkungan, jika diperlukan:

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

Jika pencetakan ke terminal tidak menghasilkan apa yang Anda harapkan, Anda dapat memeriksa apakah pengkodean UTF-8 yang Anda masukkan secara manual sudah benar; misalnya, karakter pertama Anda ( \u001A) tidak dapat dicetak, jika saya tidak salah .

Di http://wiki.python.org/moin/PrintFails , Anda dapat menemukan solusi seperti berikut, untuk 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

Untuk Python 3, Anda dapat memeriksa salah satu pertanyaan yang ditanyakan sebelumnya di StackOverflow.

Eric O Lebigot
sumber
2
@ singularity: Terima kasih! Saya menambahkan beberapa info untuk Python 3.
Eric O Lebigot
2
Terima kasih bung! Saya membutuhkan penjelasan ini untuk waktu yang lama ... Sayang sekali saya hanya dapat memberikan satu suara positif.
mik01aj
3
Saya senang bisa membantu, @ m01! Salah satu motivasi untuk menulis jawaban ini adalah bahwa ada banyak halaman di web tentang Unicode dan Python, tetapi saya menemukan bahwa meskipun menarik, mereka tidak pernah sepenuhnya memungkinkan saya untuk memecahkan masalah pengkodean konkret ... Saya benar-benar percaya bahwa dengan mengingat Prinsip-prinsip yang ditemukan dalam jawaban ini dan meluangkan waktu untuk menggunakannya ketika memecahkan masalah pengkodean konkret sangat membantu.
Eric O Lebigot
3
Ini adalah penjelasan terbaik tentang unicode dan python yang pernah ada. Python Unicode HOWTO harus diganti dengan ini.
stantonk
1
Di sini, izinkan saya menggambar karakter "penimpaan kanan-ke-kiri" di papan tulis ini…
icktoofay
20

Python selalu mengkodekan string Unicode saat menulis ke terminal, file, pipa, dll. Saat menulis ke terminal Python biasanya dapat menentukan pengkodean terminal dan menggunakannya dengan benar. Saat menulis ke file atau pipa, Python default ke pengkodean 'ascii' kecuali secara eksplisit diberitahu sebaliknya. Python dapat diberi tahu apa yang harus dilakukan saat menyalurkan output melalui PYTHONIOENCODINGvariabel lingkungan. Sebuah shell dapat mengatur variabel ini sebelum mengarahkan keluaran Python ke file atau pipa sehingga pengkodean yang benar diketahui.

Dalam kasus Anda, Anda telah mencetak 4 karakter tidak umum yang tidak didukung terminal Anda dalam fontnya. Berikut beberapa contoh untuk membantu menjelaskan perilakunya, dengan karakter yang sebenarnya didukung oleh terminal saya (yang menggunakan cp437, bukan UTF-8).

Contoh 1

Perhatikan bahwa #codingkomentar tersebut menunjukkan pengkodean di mana file sumber disimpan. Saya memilih utf8 sehingga saya dapat mendukung karakter dalam sumber yang tidak dapat dilakukan oleh terminal saya. Pengkodean dialihkan ke stderr sehingga bisa dilihat saat diarahkan ke file.

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

Output (dijalankan langsung dari terminal)

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

Python menentukan pengkodean terminal dengan benar.

Output (dialihkan ke file)

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 tidak dapat menentukan pengkodean (Tidak Ada) jadi gunakan 'ascii' default. ASCII hanya mendukung pengubahan 128 karakter pertama Unicode.

Output (diarahkan ke file, PYTHONIOENCODING = cp437)

cp437

dan file keluaran saya benar:

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

Contoh 2

Sekarang saya akan memasukkan karakter di sumber yang tidak didukung oleh terminal saya:

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

Output (dijalankan langsung dari 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>

Terminal saya tidak memahami karakter China terakhir itu.

Output (jalankan langsung, PYTHONIOENCODING = 437: ganti)

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

Penangan kesalahan dapat ditentukan dengan pengkodean. Dalam hal ini karakter yang tidak diketahui diganti dengan ?. ignoredan xmlcharrefreplacebeberapa opsi lainnya. Saat menggunakan UTF8 (yang mendukung pengkodean semua karakter Unicode) penggantian tidak akan pernah dilakukan, tetapi font yang digunakan untuk menampilkan karakter harus tetap mendukungnya.

Mark Tolonen
sumber
Tidak sepenuhnya benar bahwa "Saat menulis ke file atau pipa Python default ke pengkodean 'ascii' kecuali secara eksplisit diberitahu sebaliknya.". Faktanya, Python 3 menggunakan UTF-8, di Mac OS X / Fink.
Eric O Lebigot
2
Ya, Python 3 defaultnya adalah 'utf8', tetapi berdasarkan sampel OP, dia menggunakan Python 2.X, yang defaultnya adalah 'ascii'.
Mark Tolonen
Saya tidak bisa mendapatkan hasil yang benar dengan memanipulasi PYTHONIOENCODING. Melakukan print string.encode("UTF-8")seperti yang disarankan oleh @Ismail berhasil untuk saya.
tripleee
Anda dapat melihat karakter Cina jika font Anda mendukungnya meskipun chcpcodepage tidak mendukungnya. Untuk menghindarinya UnicodeEncodeError: 'charmap', Anda bisa memasang win-unicode-consolepaket.
jfs
Masalah saya adalah CLI python-gitlab mencetak karakter Cina dengan baik di cmd tetapi karakternya adalah sampah setelah dialihkan ke file. PYTHONIOENCODING=utf-8memecahkan masalah.
ElpieKay
12

Encode itu saat mencetak

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

Ini karena ketika Anda menjalankan skrip secara manual python mengkodekannya sebelum mengeluarkannya ke terminal, ketika Anda menyalurkannya python tidak menyandikannya sendiri sehingga Anda harus menyandikannya secara manual saat melakukan I / O.

ismail
sumber
4
Itu masih belum menjawab pertanyaan WTH yang terjadi di sini. Mengapa, tiba-tiba ia memutuskan untuk menyandikan hanya ketika diarahkan, padahal ini seharusnya benar-benar transparan untuk proses tersebut.
Maxim Sloyko
Mengapa python tidak mengkodekannya saat melakukan pengalihan? Apakah python secara eksplisit memeriksa dan memutuskan bahwa ia akan melakukan sesuatu secara berbeda hanya untuk menjadi sulit?
Arafangion
1
apakah python punya cara untuk membedakan kedua situasi tersebut? Saya pikir (sampai sekarang ...) tidak mungkin dia bisa tahu.
zedoo
4
Python dapat memeriksa apakah keluarannya adalah terminal, jika keluarannya ke pipa, maka jenis terminal akan menjadi "bodoh". Saya kira "bodoh" seharusnya memberi tahu Anda mengapa Python tidak mencoba melakukan apa pun secara otomatis dalam kasus ini, itu bisa gagal.
ismail
1
itu menghasilkan mojibake jika lingkungan menggunakan pengkodean karakter yang tidak kompatibel dengan utf-8 (misalnya, ini umum di Windows). Jangan melakukan hardcode pengkodean karakter lingkungan Anda di dalam skrip Anda. Konfigurasikan lokal Anda, atau PYTHONIOENCODING, atau instal win-unicode-console(Windows), atau terima parameter baris perintah (jika harus).
jfs