Dlaczego Python drukuje znaki Unicode, gdy domyślnym kodowaniem jest ASCII?

139

Z powłoki Pythona 2.6:

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

Spodziewałem się jakiegoś bełkotu lub błędu po instrukcji print, ponieważ znak „é” nie jest częścią ASCII i nie określiłem kodowania. Wydaje mi się, że nie rozumiem, co oznacza ASCII jako domyślne kodowanie.

EDYTOWAĆ

Przeniosłem zmianę do sekcji Odpowiedzi i zaakceptowałem ją zgodnie z sugestią.

Michael Ekoka
źródło
6
Byłoby całkiem miło, gdybyś zamiast tego mógł zamienić tę zmianę w odpowiedź i zaakceptować ją.
mercator
2
Drukowanie '\xe9'w terminalu skonfigurowanym dla UTF-8 nie będzie drukowane é. Wyświetli znak zastępczy (zwykle znak zapytania), ponieważ \xe9nie jest prawidłową sekwencją UTF-8 (brakuje dwóch bajtów, które powinny następować po tym wiodącym bajcie). Z pewnością nie będzie to interpretowane jako Latin-1.
Martijn Pieters
2
@MartijnPieters Podejrzewam, że mogłeś przejrzeć część, w której wskazałem, że terminal jest ustawiony na dekodowanie w ISO-8859-1 (latin1), kiedy wyprowadzam \xe9do drukowania é.
Michael Ekoka,
2
Ach tak, tęskniłem za tą częścią; terminal ma konfigurację inną niż powłoka. Czek.
Martijn Pieters
przejrzałem odpowiedź, ale w rzeczywistości mam ciąg bez prefiksu u dla Pythona 2.7. dlaczego ten nadal jest traktowany jako Unicode? (my sys.getdefaultencoding () is ascii)
dtc

Odpowiedzi:

104

Myślę, że dzięki fragmentom z różnych odpowiedzi możemy zszyć wyjaśnienie.

Próbując wydrukować ciąg znaków Unicode, u '\ xe9', Python niejawnie próbuje zakodować ten ciąg przy użyciu schematu kodowania obecnie przechowywanego w sys.stdout.encoding. Python faktycznie pobiera to ustawienie ze środowiska, z którego został zainicjowany. Jeśli nie może znaleźć odpowiedniego kodowania w środowisku, tylko wtedy powraca do swojego domyślnego ASCII.

Na przykład używam powłoki bash, której kodowanie jest domyślnie ustawione na UTF-8. Jeśli uruchomię z niego Pythona, odbierze i użyje tego ustawienia:

$ python

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

Wyjdźmy na chwilę z powłoki Pythona i ustawmy środowisko basha z jakimś fałszywym kodowaniem:

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

Następnie ponownie uruchom powłokę Pythona i sprawdź, czy rzeczywiście powraca do domyślnego kodowania ascii.

$ python

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

Bingo!

Jeśli teraz spróbujesz wypisać jakiś znak Unicode poza ascii, powinieneś otrzymać ładny komunikat o błędzie

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

Zamknijmy Pythona i odrzućmy powłokę bash.

Teraz będziemy obserwować, co się dzieje po tym, jak Python wyprowadza ciągi. W tym celu najpierw uruchomimy powłokę bash w terminalu graficznym (używam terminala Gnome) i ustawimy terminal na dekodowanie wyjścia za pomocą ISO-8859-1 aka latin-1 (terminale graficzne zwykle mają opcję Ustaw znak Kodowanie w jednym z rozwijanych menu). Zauważ, że nie zmienia to rzeczywistego kodowania środowiska powłoki , zmienia tylko sposób, w jaki terminal sam dekoduje dane wyjściowe, podobnie jak robi to przeglądarka internetowa. Możesz zatem zmienić kodowanie terminala, niezależnie od środowiska powłoki. Zacznijmy więc Pythona od powłoki i sprawdźmy, czy sys.stdout.encoding jest ustawione na kodowanie środowiska powłoki (dla mnie UTF-8):

$ 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 wysyła ciąg binarny bez zmian, terminal odbiera go i próbuje dopasować jego wartość do mapy znaków latin-1. W Latin-1, 0xe9 lub 233 daje znak „é”, więc to właśnie wyświetla terminal.

(2) python próbuje niejawnie zakodować ciąg znaków Unicode za pomocą dowolnego schematu ustawionego w sys.stdout.encoding, w tym przypadku jest to „UTF-8”. Po zakodowaniu UTF-8 otrzymany ciąg binarny to „\ xc3 \ xa9” (zobacz późniejsze wyjaśnienie). Terminal odbiera strumień jako taki i próbuje zdekodować kod 0xc3a9 przy użyciu latin-1, ale latin-1 przechodzi od 0 do 255, a więc dekoduje tylko strumienie 1 bajt na raz. 0xc3a9 ma długość 2 bajtów, dekoder latin-1 interpretuje go zatem jako 0xc3 (195) i 0xa9 (169), co daje 2 znaki: Ă i ©.

(3) Python koduje punkt kodowy Unicode u '\ xe9' (233) ze schematem latin-1. Okazuje się, że zakres punktów kodowych Latin-1 to 0-255 i wskazuje dokładnie ten sam znak, co Unicode w tym zakresie. Dlatego punkty kodowe Unicode w tym zakresie będą dawały tę samą wartość, gdy zostaną zakodowane w latin-1. Więc u '\ xe9' (233) zakodowane w latin-1 również da ciąg binarny '\ xe9'. Terminal otrzymuje tę wartość i próbuje dopasować ją na mapie znaków latin-1. Podobnie jak przypadek (1), zwraca „é” i to jest wyświetlane.

Zmieńmy teraz ustawienia kodowania terminala na UTF-8 z menu rozwijanego (tak jakbyś zmienił ustawienia kodowania przeglądarki internetowej). Nie ma potrzeby zatrzymywania Pythona ani restartowania powłoki. Kodowanie terminala jest teraz zgodne z kodowaniem Pythona. Spróbujmy wydrukować ponownie:

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

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

>>>

(4) Python wyprowadza ciąg binarny bez zmian. Terminal próbuje zdekodować ten strumień za pomocą UTF-8. Ale UTF-8 nie rozumie wartości 0xe9 (patrz późniejsze wyjaśnienie) i dlatego nie jest w stanie przekonwertować jej na punkt kodowy Unicode. Nie znaleziono punktu kodowego, brak wydrukowanego znaku.

(5) Python próbuje niejawnie zakodować ciąg znaków Unicode za pomocą tego, co jest w sys.stdout.encoding. Nadal „UTF-8”. Wynikowy ciąg binarny to „\ xc3 \ xa9”. Terminal odbiera strumień i próbuje zdekodować 0xc3a9 również przy użyciu UTF-8. Zwraca wartość kodu 0xe9 (233), która na mapie znaków Unicode wskazuje na symbol „é”. Terminal wyświetla „é”.

(6) Python koduje ciąg znaków Unicode za pomocą latin-1, daje ciąg binarny o tej samej wartości '\ xe9'. Ponownie, w przypadku terminala jest to prawie to samo, co w przypadku (4).

Wnioski: - Python wyprowadza ciągi nie-Unicode jako surowe dane, bez uwzględnienia domyślnego kodowania. Terminal po prostu wyświetla je, jeśli jego bieżące kodowanie jest zgodne z danymi. - Python wyprowadza ciągi Unicode po zakodowaniu ich przy użyciu schematu określonego w sys.stdout.encoding. - Python pobiera to ustawienie ze środowiska powłoki. - terminal wyświetla dane wyjściowe zgodnie z własnymi ustawieniami kodowania. - kodowanie terminala jest niezależne od powłoki.


Więcej szczegółów na temat Unicode, UTF-8 i latin-1:

Unicode to w zasadzie tablica znaków, w której niektóre klawisze (punkty kodowe) zostały tradycyjnie przypisane do wskazania niektórych symboli. np. konwencją zdecydowano, że klucz 0xe9 (233) jest wartością wskazującą na symbol „é”. ASCII i Unicode używają tych samych punktów kodowych od 0 do 127, podobnie jak latin-1 i Unicode od 0 do 255. Oznacza to, że 0x41 wskazuje „A” w ASCII, latin-1 i Unicode, 0xc8 wskazuje „Ü” w latin-1 i Unicode, 0xe9 wskazuje na „é” w latin-1 i Unicode.

Podczas pracy z urządzeniami elektronicznymi punkty kodowe Unicode wymagają wydajnego sposobu reprezentacji elektronicznej. O to chodzi w schematach kodowania. Istnieją różne schematy kodowania Unicode (utf7, UTF-8, UTF-16, UTF-32). Najbardziej intuicyjnym i prostym podejściem do kodowania byłoby po prostu użycie wartości punktu kodowego w mapie Unicode jako wartości jej formy elektronicznej, ale Unicode ma obecnie ponad milion punktów kodowych, co oznacza, że ​​niektóre z nich wymagają 3 bajtów wyrażone. Aby efektywnie pracować z tekstem, mapowanie 1 do 1 byłoby raczej niepraktyczne, ponieważ wymagałoby, aby wszystkie punkty kodowe były przechowywane w dokładnie tej samej ilości miejsca, z minimum 3 bajtami na znak, niezależnie od ich rzeczywistych potrzeb.

Większość schematów kodowania ma wady związane z wymaganiami dotyczącymi miejsca, najbardziej ekonomiczne nie obejmują wszystkich punktów kodowych Unicode, na przykład ascii obejmuje tylko pierwsze 128, podczas gdy latin-1 obejmuje pierwsze 256. Inne, które starają się być bardziej wszechstronne, również kończą marnotrawstwo, ponieważ wymagają więcej bajtów niż to konieczne, nawet w przypadku typowych „tanich” znaków. Na przykład UTF-16 wykorzystuje co najmniej 2 bajty na znak, w tym te z zakresu ascii („B”, które wynosi 65, nadal wymaga 2 bajtów pamięci w UTF-16). UTF-32 jest jeszcze bardziej marnotrawny, ponieważ przechowuje wszystkie znaki w 4 bajtach.

UTF-8 sprytnie rozwiązał ten dylemat za pomocą schematu zdolnego do przechowywania punktów kodowych ze zmienną ilością przestrzeni bajtowych. W ramach swojej strategii kodowania UTF-8 łączy punkty kodowe z bitami flag, które wskazują (prawdopodobnie dekoderom) ich wymagania dotyczące przestrzeni i ich granice.

Kodowanie UTF-8 punktów kodowych Unicode w zakresie ascii (0-127):

0xxx xxxx  (in binary)
  • znaki x pokazują rzeczywistą przestrzeń zarezerwowaną do „przechowywania” punktu kodowego podczas kodowania
  • Początkowe 0 to flaga wskazująca dekoderowi UTF-8, że ten punkt kodowy będzie wymagał tylko 1 bajtu.
  • po zakodowaniu UTF-8 nie zmienia wartości punktów kodowych w tym konkretnym zakresie (tj. 65 zakodowanych w UTF-8 to również 65). Biorąc pod uwagę, że Unicode i ASCII są również kompatybilne w tym samym zakresie, nawiasem mówiąc, sprawia, że ​​UTF-8 i ASCII są również kompatybilne w tym zakresie.

np. punkt kodowy Unicode dla „B” to „0x42” lub 0100 0010 w postaci binarnej (jak powiedzieliśmy, jest to to samo w ASCII). Po zakodowaniu w UTF-8 staje się:

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)

Kodowanie UTF-8 punktów kodowych Unicode powyżej 127 (nie ASCII):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • początkowe bity „110” wskazują dekoderowi UTF-8 początek punktu kodowego zakodowanego w 2 bajtach, podczas gdy „1110” oznacza 3 bajty, 11110 oznaczałoby 4 bajty i tak dalej.
  • wewnętrzne bity flagi „10” są używane do sygnalizowania początku wewnętrznego bajtu.
  • ponownie x oznaczają miejsce, w którym wartość punktu kodu Unicode jest przechowywana po zakodowaniu.

np. „é” Punkt kodowy Unicode to 0xe9 (233).

1110 1001    <-- 0xe9

Kiedy UTF-8 koduje tę wartość, określa, że ​​wartość jest większa niż 127 i mniejsza niż 2048, dlatego powinna być zakodowana w 2 bajtach:

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

Punkty kodowe 0xe9 Unicode po kodowaniu UTF-8 stają się 0xc3a9. Dokładnie tak, jak odbiera go terminal. Jeśli twój terminal jest ustawiony na dekodowanie łańcuchów przy użyciu latin-1 (jednego ze starszych kodowań innych niż Unicode), zobaczysz à ©, ponieważ tak się składa, że ​​0xc3 w Latin-1 wskazuje na à i 0xa9 na ©.

Michael Ekoka
źródło
6
Doskonałe wyjaśnienie. Teraz rozumiem UTF-8!
Doctor Coder,
2
OK, przeczytałem cały Twój post za około 10 sekund. Mówi się, że „Python jest do niczego, jeśli chodzi o kodowanie”.
Andrew,
Świetne wyjaśnienie. Czy mógłbyś odpowiedzieć na to pytanie?
Maggyero,
26

Gdy znaki Unicode są drukowane na standardowe wyjście, sys.stdout.encodingjest używane. Zakłada się, że znak inny niż Unicode znajduje się w środku sys.stdout.encodingi jest po prostu wysyłany do terminala. W moim systemie (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() jest używany tylko wtedy, gdy Python nie ma innej opcji.

Zauważ, że Python 3.6 lub nowszy ignoruje kodowanie w systemie Windows i używa interfejsów API Unicode do zapisywania Unicode na terminalu. Brak ostrzeżeń UnicodeEncodeError i wyświetlany jest prawidłowy znak, jeśli czcionka go obsługuje. Nawet jeśli czcionka go nie obsługuje, znaki nadal można wycinać i wklejać z terminala do aplikacji z czcionką pomocniczą i będzie to poprawne. Aktualizacja!

Mark Tolonen
źródło
8

Python REPL próbuje pobrać kodowanie z twojego środowiska. Jeśli znajdzie coś rozsądnego, to wszystko po prostu działa. To wtedy, gdy nie może dowiedzieć się, co się dzieje, robi to źle.

>>> print sys.stdout.encoding
UTF-8
Ignacio Vazquez-Abrams
źródło
3
tylko z ciekawości, jak zmieniłbym sys.stdout.encoding na ascii?
Michael Ekoka
2
@TankorSmash Dostaję się TypeError: readonly attributena 2.7.2
Kos,
4

Państwo nie określono kodowania, wprowadzając wyraźne ciąg Unicode. Porównaj wyniki nieużywania uprzedrostka.

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

>>> 

W takim przypadku \xe9Python zakłada domyślne kodowanie (Ascii), wypisując w ten sposób ... coś pustego.

Mark Rushakoff
źródło
1
więc jeśli dobrze rozumiem, kiedy drukuję ciągi znaków Unicode (punkty kodowe), python zakłada, że ​​chcę, aby dane wyjściowe były zakodowane w utf-8, zamiast po prostu próbować dać mi to, co mogłoby być w ascii?
Michael Ekoka
1
@mike: AFAIK to, co powiedziałeś, jest poprawne. Jeśli to nie drukować znaki Unicode, ale zakodowany jako ASCII, wszystko wyjdzie zniekształcone i prawdopodobnie wszyscy początkujący będą pytać: „Dlaczego nie mogę wydrukować tekst Unicode?”
Mark Rushakoff
2
Dziękuję Ci. Właściwie jestem jednym z tych początkujących, ale pochodzę ze strony ludzi, którzy rozumieją Unicode, dlatego to zachowanie trochę mnie zraża.
Michael Ekoka
3
R., niepoprawne, ponieważ '\ xe9' nie znajduje się w zestawie znaków ascii. Ciągi inne niż Unicode są drukowane przy użyciu sys.stdout.encoding, ciągi Unicode są kodowane do sys.stdout.encoding przed drukowaniem.
Mark Tolonen
0

Mi to pasuje:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
user3611630
źródło
1
Tani brudny hack, który nieuchronnie zepsuje coś innego. Nie jest trudno zrobić to we właściwy sposób!
Chris Johnson,
0

Zgodnie z domyślnym / niejawnym kodowaniem ciągów i konwersjami w języku Python :

  • Kiedy printing unicode, to encoded z <file>.encoding.
    • gdy encodingnie jest ustawiona, to unicodejest niejawnie konwertowana na str(ponieważ kodek do tego jest sys.getdefaultencoding(), tj. asciidowolny znak narodowy spowodowałby a UnicodeEncodeError)
    • w przypadku standardowych strumieni encodingjest on wywnioskowany ze środowiska. Zwykle jest to ustawione dla ttystrumieni (z ustawień regionalnych terminala), ale prawdopodobnie nie zostanie ustawione dla potoków
      • więc print u'\xe9'prawdopodobnie powiedzie się, gdy dane wyjściowe trafią do terminala, i zakończy się niepowodzeniem, jeśli zostanie przekierowany. Rozwiązaniem jest encode()łańcuch z żądanym kodowaniem przed rozpoczęciem print.
  • Podczas printing strbajty są wysyłane do strumienia bez zmian. To, jakie glify wyświetla terminal, zależy od jego ustawień regionalnych.
ivan_pozdeev
źródło