Mengapa Python mencetak karakter unicode ketika pengkodean default adalah ASCII?

139

Dari shell Python 2.6:

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 

Saya berharap memiliki beberapa omong kosong atau Kesalahan setelah pernyataan cetak, karena karakter "é" bukan bagian dari ASCII dan saya belum menentukan penyandian. Saya kira saya tidak mengerti apa artinya pengkodean default ASCII.

EDIT

Saya memindahkan hasil edit ke bagian Jawaban dan menerimanya seperti yang disarankan.

Michael Ekoka
sumber
6
Alangkah baiknya jika Anda dapat mengubah hasil edit itu menjadi jawaban dan menerimanya.
mercator
2
Mencetak '\xe9'di terminal yang dikonfigurasi untuk UTF-8 tidak akan mencetak é. Ini akan mencetak karakter pengganti (biasanya tanda tanya) karena \xe9bukan urutan UTF-8 yang valid (tidak ada dua byte yang seharusnya mengikuti byte terkemuka itu). Ini pasti tidak akan ditafsirkan sebagai Latin-1 sebagai gantinya.
Martijn Pieters
2
@ MartijnPieters Saya menduga Anda mungkin telah membaca skim di bagian di mana saya menentukan bahwa terminal diatur untuk memecahkan kode di ISO-8859-1 (latin1) ketika saya output \xe9untuk mencetak é.
Michael Ekoka
2
Ah ya, aku memang merindukan bagian itu; terminal ini memiliki konfigurasi yang berbeda dari shell. Memeriksa.
Martijn Pieters
i skim melalui jawaban tetapi sebenarnya, saya memiliki string tanpa awalan u untuk python 2.7. mengapa itu masih ditangani sebagai unicode? (my sys.getdefaultencoding () is ascii)
dtc

Jawaban:

104

Berkat potongan-potongan dari berbagai balasan, saya pikir kita bisa menjahit penjelasan.

Dengan mencoba mencetak string unicode, u '\ xe9', Python secara implisit mencoba untuk menyandikan string itu menggunakan skema penyandian yang saat ini disimpan di sys.stdout.encoding. Python sebenarnya mengambil pengaturan ini dari lingkungannya. Jika itu tidak dapat menemukan pengkodean yang tepat dari lingkungan, hanya kemudian kembali ke standarnya , ASCII.

Sebagai contoh, saya menggunakan bash shell yang secara default encoding ke UTF-8. Jika saya memulai Python dari itu, ia mengambil dan menggunakan pengaturan itu:

$ python

>>> import sys
>>> print sys.stdout.encoding
UTF-8

Mari sesaat keluar dari shell Python dan atur lingkungan bash dengan beberapa penyandian palsu:

$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.

Kemudian mulai shell python lagi dan verifikasi bahwa memang mengembalikan ke ascii default.

$ python

>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968

Bingo!

Jika sekarang Anda mencoba untuk mengeluarkan beberapa karakter unicode di luar ascii Anda akan mendapatkan pesan kesalahan yang bagus

>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' 
in position 0: ordinal not in range(128)

Ayo keluar dari Python dan buang bash shell.

Kami sekarang akan mengamati apa yang terjadi setelah Python mengeluarkan string. Untuk ini pertama-tama kita akan memulai bash shell dalam terminal grafik (saya menggunakan Terminal Gnome) dan kami akan mengatur terminal untuk mendekode output dengan ISO-8859-1 alias latin-1 (terminal grafik biasanya memiliki opsi untuk Mengatur Karakter Pengkodean di salah satu menu dropdown mereka). Perhatikan bahwa ini tidak mengubah pengkodean lingkungan shell yang sebenarnya , itu hanya mengubah cara terminal itu sendiri akan mendekode output yang diberikan, sedikit seperti browser web. Karena itu Anda dapat mengubah pengkodean terminal, secara independen dari lingkungan shell. Mari kita mulai Python dari shell dan verifikasi bahwa sys.stdout.encoding diatur ke pengkodean lingkungan shell (UTF-8 untuk saya):

$ python

>>> import sys

>>> print sys.stdout.encoding
UTF-8

>>> print '\xe9' # (1)
é
>>> print u'\xe9' # (2)
é
>>> print u'\xe9'.encode('latin-1') # (3)
é
>>>

(1) python mengeluarkan string biner, terminal menerimanya dan mencoba mencocokkan nilainya dengan peta karakter latin-1. Dalam bahasa latin-1, 0xe9 atau 233 menghasilkan karakter "é" dan itulah yang ditampilkan terminal.

(2) python mencoba menyandikan string Unicode secara implisit dengan skema apa pun yang saat ini ditetapkan dalam sys.stdout.encoding, dalam hal ini "UTF-8". Setelah pengkodean UTF-8, string biner yang dihasilkan adalah '\ xc3 \ xa9' (lihat penjelasan selanjutnya). Terminal menerima aliran seperti itu dan mencoba untuk men-decode 0xc3a9 menggunakan latin-1, tetapi latin-1 beralih dari 0 ke 255 dan karenanya, hanya mendekode stream 1 byte pada satu waktu. 0xc3a9 panjangnya 2 byte, oleh karena itu decoder latin-1 menerjemahkannya sebagai 0xc3 (195) dan 0xa9 (169) dan menghasilkan 2 karakter: Ã dan ©.

(3) python mengkodekan titik kode unicode u '\ xe9' (233) dengan skema latin-1. Ternyata rentang kode titik latin-1 adalah 0-255 dan menunjuk ke karakter yang sama persis dengan Unicode dalam rentang itu. Oleh karena itu, titik kode Unicode dalam rentang itu akan menghasilkan nilai yang sama ketika dikodekan dalam latin-1. Jadi u '\ xe9' (233) yang dikodekan dalam latin-1 juga akan menghasilkan string biner '\ xe9'. Terminal menerima nilai itu dan mencoba mencocokkannya pada peta karakter latin-1. Sama seperti kasus (1), ia menghasilkan "é" dan itulah yang ditampilkan.

Sekarang mari kita ubah pengaturan penyandian terminal ke UTF-8 dari menu dropdown (seperti Anda akan mengubah pengaturan penyandian browser web Anda). Tidak perlu menghentikan Python atau memulai ulang shell. Pengkodean terminal sekarang cocok dengan Python. Mari kita coba mencetak lagi:

>>> print '\xe9' # (4)

>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)

>>>

(4) python menampilkan string biner apa adanya. Terminal mencoba untuk men-decode aliran itu dengan UTF-8. Tetapi UTF-8 tidak memahami nilai 0xe9 (lihat penjelasan selanjutnya) dan karena itu tidak dapat mengubahnya menjadi titik kode unicode. Tidak ditemukan titik kode, tidak ada karakter yang dicetak.

(5) python mencoba menyandikan string Unicode secara implisit dengan apa pun yang ada di sys.stdout.encoding. Masih "UTF-8". String biner yang dihasilkan adalah '\ xc3 \ xa9'. Terminal menerima aliran dan mencoba untuk memecahkan kode 0xc3a9 juga menggunakan UTF-8. Ini menghasilkan kembali nilai kode 0xe9 (233), yang pada peta karakter Unicode menunjuk ke simbol "é". Terminal menampilkan "é".

(6) python mengkodekan string unicode dengan latin-1, menghasilkan string biner dengan nilai yang sama '\ xe9'. Sekali lagi, untuk terminal ini hampir sama dengan case (4).

Kesimpulan: - Python menampilkan string non-unicode sebagai data mentah, tanpa mempertimbangkan penyandian defaultnya. Terminal kebetulan menampilkannya jika pengkodeannya saat ini cocok dengan data. - Python mengeluarkan string Unicode setelah menyandikannya menggunakan skema yang ditentukan dalam sys.stdout.encoding. - Python mendapatkan pengaturan itu dari lingkungan shell. - terminal menampilkan output sesuai dengan pengaturan pengkodeannya sendiri. - Pengkodean terminal independen dari shell.


Lebih detail tentang unicode, UTF-8 dan latin-1:

Unicode pada dasarnya adalah tabel karakter di mana beberapa tombol (titik kode) telah secara konvensional ditugaskan untuk menunjuk ke beberapa simbol. misalnya dengan konvensi, telah diputuskan bahwa kunci 0xe9 (233) adalah nilai yang menunjuk ke simbol 'é'. ASCII dan Unicode menggunakan titik kode yang sama dari 0 hingga 127, seperti halnya latin-1 dan Unicode dari 0 hingga 255. Artinya, 0x41 menunjuk ke 'A' di ASCII, latin-1 dan Unicode, 0xc8 menunjuk ke 'Ü' di latin-1 dan Unicode, 0xe9 menunjuk ke 'é' dalam latin-1 dan Unicode.

Saat bekerja dengan perangkat elektronik, titik kode Unicode memerlukan cara yang efisien untuk direpresentasikan secara elektronik. Tentang itulah skema penyandian. Berbagai skema pengkodean Unicode ada (utf7, UTF-8, UTF-16, UTF-32). Pendekatan pengkodean yang paling intuitif dan lurus ke depan adalah dengan hanya menggunakan nilai titik kode dalam peta Unicode sebagai nilai untuk bentuk elektroniknya, tetapi Unicode saat ini memiliki lebih dari satu juta titik kode, yang berarti bahwa beberapa di antaranya memerlukan 3 byte untuk menjadi menyatakan. Untuk bekerja secara efisien dengan teks, pemetaan 1 ke 1 akan agak tidak praktis, karena akan mengharuskan semua titik kode disimpan dalam jumlah ruang yang persis sama, dengan minimal 3 byte per karakter, terlepas dari kebutuhan mereka yang sebenarnya.

Sebagian besar skema pengkodean memiliki kekurangan mengenai persyaratan ruang, yang paling ekonomis tidak mencakup semua poin kode unicode, misalnya ascii hanya mencakup 128 pertama, sedangkan latin-1 mencakup 256 pertama. Lainnya yang mencoba menjadi lebih komprehensif akhirnya juga menjadi boros, karena mereka membutuhkan lebih banyak byte daripada yang diperlukan, bahkan untuk karakter "murah" yang umum. UTF-16 misalnya, menggunakan minimal 2 byte per karakter, termasuk yang berada dalam rentang ascii ('B' yang 65, masih membutuhkan 2 byte penyimpanan di UTF-16). UTF-32 bahkan lebih boros karena menyimpan semua karakter dalam 4 byte.

UTF-8 secara cerdik menyelesaikan dilema, dengan skema yang dapat menyimpan titik kode dengan jumlah variabel ruang byte. Sebagai bagian dari strategi pengkodeannya, UTF-8 bertali poin kode dengan bit bendera yang menunjukkan (mungkin untuk decoder) persyaratan ruang mereka dan batas-batasnya.

Pengkodean poin kode unicode UTF-8 dalam rentang ascii (0-127):

0xxx xxxx  (in binary)
  • tanda x menunjukkan ruang aktual yang disediakan untuk "menyimpan" titik kode selama penyandian
  • 0 yang terdepan adalah tanda yang menunjukkan ke decoder UTF-8 bahwa titik kode ini hanya akan membutuhkan 1 byte.
  • pada saat encoding, UTF-8 tidak mengubah nilai poin kode dalam rentang spesifik tersebut (yaitu 65 yang dikodekan dalam UTF-8 juga 65). Mempertimbangkan bahwa Unicode dan ASCII juga kompatibel dalam rentang yang sama, itu secara kebetulan membuat UTF-8 dan ASCII juga kompatibel dalam kisaran itu.

mis. Titik kode Unicode untuk 'B' adalah '0x42' atau 0100 0010 dalam biner (seperti yang kami katakan, itu sama di ASCII). Setelah encoding di UTF-8 menjadi:

0xxx xxxx  <-- UTF-8 encoding for Unicode code points 0 to 127
*100 0010  <-- Unicode code point 0x42
0100 0010  <-- UTF-8 encoded (exactly the same)

Pengkodean poin kode Unicode UTF-8 di atas 127 (non-ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • bit terkemuka '110' menunjukkan ke decoder UTF-8 awal dari titik kode yang dikodekan dalam 2 byte, sedangkan '1110' menunjukkan 3 byte, 11110 akan menunjukkan 4 byte dan sebagainya.
  • bit bendera '10' bagian dalam digunakan untuk memberi sinyal awal byte batin.
  • lagi, tanda x adalah spasi tempat nilai titik kode Unicode disimpan setelah penyandian.

misalnya 'é' Titik kode Unicode adalah 0xe9 (233).

1110 1001    <-- 0xe9

Ketika UTF-8 mengkodekan nilai ini, ini menentukan bahwa nilainya lebih besar dari 127 dan kurang dari 2048, oleh karena itu harus dikodekan dalam 2 byte:

110x xxxx 10xx xxxx   <-- UTF-8 encoding for Unicode 128-2047
***0 0011 **10 1001   <-- 0xe9
1100 0011 1010 1001   <-- 'é' after UTF-8 encoding
C    3    A    9

Kode Unicode 0xe9 menunjuk setelah pengkodean UTF-8 menjadi 0xc3a9. Persis bagaimana terminal menerimanya. Jika terminal Anda diatur untuk mendekodekan string menggunakan latin-1 (salah satu pengkodean warisan non-unicode), Anda akan melihat à ©, karena kebetulan 0xc3 dalam bahasa latin-1 menunjuk ke à dan 0xa9 ke ©.

Michael Ekoka
sumber
6
Penjelasan yang bagus. Sekarang saya mengerti UTF-8!
Dokter Coder
2
Oke, saya membaca seluruh posting Anda dalam waktu sekitar 10 detik. Dikatakan, "Python menyebalkan ketika datang ke pengkodean."
Andrew
Penjelasan yang bagus. Bisakah Anda menjawab pertanyaan ini ?
Maggyero
26

Ketika karakter Unicode dicetak ke stdout, sys.stdout.encodingdigunakan. Karakter non-Unicode diasumsikan berada sys.stdout.encodingdan baru saja dikirim ke terminal. Di sistem saya (Python 2):

>>> import unicodedata as ud
>>> import sys
>>> sys.stdout.encoding
'cp437'
>>> ud.name(u'\xe9') # U+00E9 Unicode codepoint
'LATIN SMALL LETTER E WITH ACUTE'
>>> ud.name('\xe9'.decode('cp437')) 
'GREEK CAPITAL LETTER THETA'
>>> '\xe9'.decode('cp437') # byte E9 decoded using code page 437 is U+0398.
u'\u0398'
>>> ud.name(u'\u0398')
'GREEK CAPITAL LETTER THETA'
>>> print u'\xe9' # Unicode is encoded to CP437 correctly
é
>>> print '\xe9'  # Byte is just sent to terminal and assumed to be CP437.
Θ

sys.getdefaultencoding() hanya digunakan ketika Python tidak memiliki opsi lain.

Perhatikan bahwa Python 3.6 atau yang lebih baru mengabaikan penyandian pada Windows dan menggunakan Unicode API untuk menulis Unicode ke terminal. Tidak ada peringatan UnicodeEncodeError dan karakter yang benar ditampilkan jika font mendukungnya. Bahkan jika font tidak mendukungnya, karakter masih dapat dipotong-n-paste dari terminal ke aplikasi dengan font yang mendukung dan itu akan benar. Meningkatkan!

Mark Tolonen
sumber
8

Python REPL mencoba mengambil pengodean apa yang digunakan dari lingkungan Anda. Jika ia menemukan sesuatu yang waras maka itu semua hanya berfungsi. Itu ketika ia tidak tahu apa yang terjadi sehingga ia keluar.

>>> print sys.stdout.encoding
UTF-8
Ignacio Vazquez-Abrams
sumber
3
hanya karena penasaran, bagaimana saya mengubah sys.stdout.encoding menjadi ascii?
Michael Ekoka
2
@TankorSmash Saya mulai TypeError: readonly attribute2.7.2
Kos
4

Anda telah menentukan penyandian dengan memasukkan string Unicode eksplisit. Bandingkan hasil tidak menggunakan uawalan.

>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> '\xe9'
'\xe9'
>>> u'\xe9'
u'\xe9'
>>> print u'\xe9'
é
>>> print '\xe9'

>>> 

Dalam kasus \xe9Python mengasumsikan pengkodean default Anda (Ascii), sehingga mencetak ... sesuatu yang kosong.

Mark Rushakoff
sumber
1
jadi jika saya mengerti dengan baik, ketika saya mencetak string unicode (poin kode), python mengasumsikan bahwa saya ingin output dikodekan dalam utf-8, bukannya hanya mencoba memberi saya apa yang bisa di ascii?
Michael Ekoka
1
@ Mike: AFAIK apa yang Anda katakan benar. Jika memang mencetak karakter Unicode tetapi dikodekan sebagai ASCII, semuanya akan berantakan dan mungkin semua pemula akan bertanya, "Kenapa saya tidak bisa mencetak teks Unicode?"
Mark Rushakoff
2
Terima kasih. Saya sebenarnya adalah salah satu dari mereka yang pemula, tetapi datang dari sisi orang-orang yang memiliki pemahaman tentang unicode, itulah sebabnya perilaku ini membuat saya sedikit marah.
Michael Ekoka
3
R., tidak benar, karena '\ xe9' tidak ada dalam rangkaian karakter ascii. String non-Unicode dicetak menggunakan sys.stdout.encoding, string Unicode dikodekan ke sys.stdout.encoding sebelum dicetak.
Mark Tolonen
0

Ini bekerja untuk saya:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
pengguna3611630
sumber
1
Retas kotor murah yang pasti akan merusak sesuatu yang lain. Tidak sulit melakukannya dengan cara yang benar!
Chris Johnson
0

Sesuai dengan pengkodean dan konversi string Python default / implisit :

  • Ketika printing unicode, itu encodedengan <file>.encoding.
    • ketika encodingtidak diatur, unicodesecara implisit dikonversi ke str(karena codec untuk itu sys.getdefaultencoding(), yaitu ascii, setiap karakter nasional akan menyebabkan a UnicodeEncodeError)
    • untuk aliran standar, encodingdisimpulkan dari lingkungan. Ini biasanya mengatur ttyaliran fot (dari pengaturan lokal terminal), tetapi kemungkinan tidak diatur untuk pipa
      • jadi a print u'\xe9'kemungkinan berhasil ketika output ke terminal, dan gagal jika diarahkan. Solusi untuk encode()string dengan pengkodean yang diinginkan sebelum print.
  • Ketika printing str, byte dikirim ke aliran apa adanya. Mesin terbang apa yang ditunjukkan terminal akan tergantung pada pengaturan lokalnya.
ivan_pozdeev
sumber