Perché Python stampa caratteri unicode quando la codifica predefinita è ASCII?


139

Dalla shell Python 2.6:

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

Mi aspettavo di avere un po 'incomprensibile o un errore dopo l'istruzione print, poiché il carattere "é" non fa parte di ASCII e non ho specificato una codifica. Immagino di non capire cosa significhi ASCII come codifica predefinita.

MODIFICARE

Ho spostato la modifica nella sezione Risposte e l'ho accettata come suggerito.


6
Sarebbe piuttosto bello se invece potessi trasformare quella modifica in una risposta e accettarla.
mercatore

2
La stampa '\xe9'in un terminale configurato per UTF-8 non verrà stampata é. Stamperà un carattere sostitutivo (di solito un punto interrogativo) in quanto \xe9non è una sequenza UTF-8 valida (mancano due byte che avrebbero dovuto seguire quel byte iniziale). Sarà certamente non essere interpretato come Latin-1, invece.
Martijn Pieters

2
@MartijnPieters Sospetto che potresti aver scremato la parte in cui ho specificato che il terminale è impostato per la decodifica in ISO-8859-1 (latino1) quando eseguo l'output \xe9per la stampa é.
Michael Ekoka,

2
Ah sì, mi mancava quella parte; il terminale ha una configurazione diversa dalla shell. Dai un'occhiata.
Martijn Pieters

ho sfogliato la risposta ma in realtà ho la stringa senza il prefisso u per python 2.7. perché quello viene ancora gestito come Unicode? (my sys.getdefaultencoding () is ascii)
dtc,

Risposte:


104

Grazie a frammenti di varie risposte, penso che possiamo ricucire una spiegazione.

Provando a stampare una stringa unicode, u '\ xe9', Python tenta implicitamente di codificare quella stringa usando lo schema di codifica attualmente memorizzato in sys.stdout.encoding. Python effettivamente prende questa impostazione dall'ambiente da cui è stata avviata. Se non riesce a trovare una codifica corretta dall'ambiente, solo allora ripristina l' impostazione predefinita , ASCII.

Ad esempio, utilizzo una shell bash che codifica per impostazione predefinita UTF-8. Se avvio Python da esso, raccoglie e usa quell'impostazione:

$ python

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

Lasciamo per un momento la shell di Python e impostiamo l'ambiente di bash con una codifica fasulla:

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

Quindi riavviare la shell Python e verificare che ripristini effettivamente la codifica ASCII predefinita.

$ python

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

Bingo!

Se ora provi a generare un carattere unicode al di fuori di ASCII, dovresti ricevere un bel messaggio di errore

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

Consente di uscire da Python e scartare la shell bash.

Osserveremo ora cosa succede dopo che Python emette stringhe. Per questo inizieremo prima una shell bash all'interno di un terminale grafico (utilizzo Gnome Terminal) e imposteremo il terminale per decodificare l'output con ISO-8859-1 aka latin-1 (i terminali grafici di solito hanno un'opzione per impostare il carattere Codifica in uno dei loro menu a discesa). Si noti che ciò non modifica la codifica dell'ambiente di shell reale , ma cambia solo il modo in cui il terminale stesso decodificherà l'output fornito, un po 'come fa un browser web. È quindi possibile modificare la codifica del terminale, indipendentemente dall'ambiente della shell. Quindi avviamo Python dalla shell e verificiamo che sys.stdout.encoding sia impostato sulla codifica dell'ambiente di shell (UTF-8 per me):

$ 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 emette una stringa binaria così com'è, il terminale la riceve e cerca di far corrispondere il suo valore con la mappa di caratteri latin-1. In latino-1, 0xe9 o 233 restituisce il carattere "é" e quindi questo è ciò che viene visualizzato dal terminale.

(2) Python tenta di codificare implicitamente la stringa Unicode con qualunque schema sia attualmente impostato in sys.stdout.encoding, in questo caso è "UTF-8". Dopo la codifica UTF-8, la stringa binaria risultante è '\ xc3 \ xa9' (vedere la spiegazione successiva). Il terminale riceve lo stream in quanto tale e tenta di decodificare 0xc3a9 usando latin-1, ma latin-1 va da 0 a 255 e quindi decodifica solo i flussi 1 byte alla volta. 0xc3a9 è lungo 2 byte, il decodificatore latino-1 quindi lo interpreta come 0xc3 (195) e 0xa9 (169) e produce 2 caratteri: Ã e ©.

(3) python codifica il punto di codice unicode u '\ xe9' (233) con lo schema latino-1. Risulta che l'intervallo di punti del codice latino-1 è 0-255 e punta allo stesso identico carattere di Unicode all'interno di quell'intervallo. Pertanto, i punti di codice Unicode in quell'intervallo produrranno lo stesso valore se codificati in latino-1. Quindi u '\ xe9' (233) codificato in latino-1 produce anche la stringa binaria '\ xe9'. Il terminale riceve quel valore e cerca di abbinarlo sulla mappa dei caratteri latino-1. Proprio come case (1), produce "é" ed è quello che viene visualizzato.

Ora cambiamo le impostazioni di codifica del terminale in UTF-8 dal menu a discesa (come se cambiassi le impostazioni di codifica del tuo browser web). Non è necessario arrestare Python o riavviare la shell. La codifica del terminale ora corrisponde a quella di Python. Proviamo di nuovo a stampare:

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

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

>>>

(4) python genera una stringa binaria così com'è. Il terminale tenta di decodificare quel flusso con UTF-8. Ma UTF-8 non comprende il valore 0xe9 (vedere la spiegazione successiva) e non è quindi in grado di convertirlo in un punto di codice unicode. Nessun punto di codice trovato, nessun carattere stampato.

(5) python tenta di codificare implicitamente la stringa Unicode con qualunque cosa sia in sys.stdout.encoding. Ancora "UTF-8". La stringa binaria risultante è '\ xc3 \ xa9'. Il terminale riceve lo stream e tenta di decodificare 0xc3a9 anche usando UTF-8. Restituisce il valore del codice 0xe9 (233), che sulla mappa dei caratteri Unicode punta al simbolo "é". Il terminale visualizza "é".

(6) python codifica la stringa unicode con latin-1, produce una stringa binaria con lo stesso valore '\ xe9'. Ancora una volta, per il terminale questo è praticamente lo stesso del caso (4).

Conclusioni: - Python genera stringhe non unicode come dati non elaborati, senza considerare la codifica predefinita. Il terminale semplicemente li visualizza se la sua codifica corrente corrisponde ai dati. - Python genera stringhe Unicode dopo averle codificate usando lo schema specificato in sys.stdout.encoding. - Python ottiene quell'impostazione dall'ambiente della shell. - il terminale visualizza l'output in base alle proprie impostazioni di codifica. - la codifica del terminale è indipendente da quella della shell.


Maggiori dettagli su unicode, UTF-8 e latin-1:

Unicode è fondamentalmente una tabella di caratteri in cui alcuni tasti (punti di codice) sono stati assegnati in modo convenzionale per indicare alcuni simboli. ad es. per convenzione è stato deciso che la chiave 0xe9 (233) è il valore che punta al simbolo 'é'. ASCII e Unicode usano gli stessi punti di codice da 0 a 127, così come latin-1 e Unicode da 0 a 255. Cioè, 0x41 punti a 'A' in ASCII, latin-1 e Unicode, 0xc8 punti a 'Ü' in latin-1 e Unicode, 0xe9 punta a 'é' in latin-1 e Unicode.

Quando si lavora con dispositivi elettronici, i punti di codice Unicode necessitano di un modo efficiente per essere rappresentati elettronicamente. Ecco a cosa servono gli schemi di codifica. Esistono vari schemi di codifica Unicode (utf7, UTF-8, UTF-16, UTF-32). L'approccio di codifica più intuitivo e diretto sarebbe semplicemente usare il valore di un punto di codice nella mappa Unicode come valore per la sua forma elettronica, ma attualmente Unicode ha oltre un milione di punti di codice, il che significa che alcuni di essi richiedono 3 byte per essere espresso. Per lavorare in modo efficiente con il testo, una mappatura da 1 a 1 sarebbe piuttosto impraticabile, poiché richiederebbe che tutti i punti di codice vengano archiviati esattamente nella stessa quantità di spazio, con un minimo di 3 byte per carattere, indipendentemente dalla loro effettiva necessità.

La maggior parte degli schemi di codifica ha carenze in termini di spazio, i più economici non coprono tutti i punti di codice unicode, ad esempio ascii copre solo i primi 128, mentre latino-1 copre i primi 256. Altri che cercano di essere più completi finiscono anche essendo dispendiosi, poiché richiedono più byte del necessario, anche per caratteri "economici" comuni. UTF-16, ad esempio, utilizza un minimo di 2 byte per carattere, inclusi quelli nell'intervallo ascii ('B' che è 65, richiede ancora 2 byte di memoria in UTF-16). UTF-32 è ancora più dispendioso in quanto memorizza tutti i caratteri in 4 byte.

UTF-8 sembra aver risolto abilmente il dilemma, con uno schema in grado di memorizzare punti di codice con una quantità variabile di spazi byte. Come parte della sua strategia di codifica, UTF-8 allaccia punti di codice con bit di flag che indicano (presumibilmente ai decodificatori) i loro requisiti di spazio e i loro confini.

Codifica UTF-8 dei punti di codice unicode nell'intervallo ascii (0-127):

0xxx xxxx  (in binary)
  • le x mostrano lo spazio effettivo riservato per "memorizzare" il punto di codice durante la codifica
  • Lo 0 iniziale è un flag che indica al decodificatore UTF-8 che questo punto di codice richiederà solo 1 byte.
  • al momento della codifica, UTF-8 non modifica il valore dei punti di codice in quell'intervallo specifico (ovvero 65 codificato in UTF-8 è anche 65). Considerando che anche Unicode e ASCII sono compatibili nello stesso intervallo, incidentalmente rende UTF-8 e ASCII compatibili anche in quell'intervallo.

es. il punto di codice Unicode per 'B' è '0x42' o 0100 0010 in binario (come abbiamo detto, è lo stesso in ASCII). Dopo la codifica in UTF-8 diventa:

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)

Codifica UTF-8 dei punti di codice Unicode sopra 127 (non ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • i bit iniziali "110" indicano al decodificatore UTF-8 l'inizio di un punto di codice codificato in 2 byte, mentre "1110" indica 3 byte, 11110 indica 4 byte e così via.
  • i bit flag '10' interni sono usati per segnalare l'inizio di un byte interno.
  • ancora una volta, le x indicano lo spazio in cui è memorizzato il valore del punto del codice Unicode dopo la codifica.

ad es. 'é' il punto di codice Unicode è 0xe9 (233).

1110 1001    <-- 0xe9

Quando UTF-8 codifica questo valore, determina che il valore è maggiore di 127 e inferiore a 2048, pertanto deve essere codificato in 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

Il codice Unicode 0xe9 punta dopo la codifica UTF-8 diventa 0xc3a9. È esattamente come lo riceve il terminale. Se il tuo terminale è impostato per decodificare le stringhe usando latin-1 (una delle codifiche legacy non unicode), vedrai à ©, perché accade che 0xc3 in latin-1 punti a à e 0xa9 a ©.


6
Spiegazione eccellente. Ora capisco UTF-8!
Doctor Coder,

2
Ok, ho letto tutto il tuo post in circa 10 secondi. Diceva: "Python fa schifo quando si tratta di codifica".
Andrew

Ottima spiegazione Potresti rispondere a questa domanda?
Maggyero,

26

Quando i caratteri Unicode vengono stampati su stdout, sys.stdout.encodingviene utilizzato. Si presume che sia presente un carattere non Unicode sys.stdout.encodinged è appena inviato al terminale. Sul mio sistema (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() viene utilizzato solo quando Python non ha un'altra opzione.

Si noti che Python 3.6 o versione successiva ignora le codifiche su Windows e utilizza le API Unicode per scrivere Unicode sul terminale. Nessun avviso UnicodeEncodeError e il carattere corretto viene visualizzato se il carattere lo supporta. Anche se il carattere non lo supporta, i caratteri possono comunque essere tagliati e incollati dal terminale a un'applicazione con un carattere di supporto e sarà corretto. Aggiornamento!


8

Python REPL tenta di rilevare quale codifica utilizzare dal proprio ambiente. Se trova qualcosa di sano, allora funziona e basta. È quando non riesce a capire cosa sta succedendo che si risolve.

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

3
solo per curiosità, come cambierei sys.stdout.encoding in ascii?
Michael Ekoka,

2
@TankorSmash Sto salendo la TypeError: readonly attribute2.7.2
Kos,

4

È stata specificata una codifica immettendo una stringa Unicode esplicita. Confronta i risultati del non utilizzo del uprefisso.

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

>>> 

Nel caso di \xe9allora Python assume la codifica predefinita (Ascii), quindi stampa ... qualcosa di vuoto.


1
quindi se capisco bene, quando stampo stringhe unicode (i punti di codice), python presume che io voglia un output codificato in utf-8, invece di provare a darmi quello che avrebbe potuto essere in ascii?
Michael Ekoka,

1
@mike: AFAIK quello che hai detto è corretto. Se avesse stampato i caratteri Unicode ma codificato come ASCII, tutto sarebbe uscito confuso e probabilmente tutti i principianti avrebbero chiesto: "Come mai non riesco a stampare il testo Unicode?"
Mark Rushakoff,

2
Grazie. In realtà sono uno di quei principianti, ma vengo dalla parte delle persone che hanno una certa comprensione dell'unicode, motivo per cui questo comportamento mi sta gettando un po 'fuori.
Michael Ekoka,

3
R., non corretto, poiché '\ xe9' non è nel set di caratteri ASCII. Le stringhe non Unicode vengono stampate utilizzando sys.stdout.encoding, le stringhe Unicode vengono codificate in sys.stdout.encoding prima della stampa.
Mark Tolonen,

0

Per me funziona:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')

1
Hack sporco a basso costo che inevitabilmente romperà qualcos'altro. Non è difficile farlo nel modo giusto!
Chris Johnson,

0

Secondo le codifiche e le conversioni di stringa predefinite / implicite di Python :

  • Quando printing unicode, è encoded <file>.encoding.
    • quando encodingnon è impostato, unicodeviene convertito implicitamente in str(poiché il codec per questo è sys.getdefaultencoding(), cioè ascii, qualsiasi carattere nazionale causerebbe a UnicodeEncodeError)
    • per i flussi standard, encodingviene dedotto dall'ambiente. In genere è impostato per i ttyflussi (dalle impostazioni locali del terminale), ma è probabile che non sia impostato per i tubi
      • quindi print u'\xe9'è probabile che a abbia esito positivo quando l'output è su un terminale e fallisce se viene reindirizzato. Una soluzione è encode()la stringa con la codifica desiderata prima di printing.
  • Quando printing str, i byte vengono inviati allo stream così come sono. Quali glifi mostrati dal terminale dipenderanno dalle sue impostazioni locali.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.