UnicodeDecodeError podczas przekierowywania do pliku

100

Uruchamiam ten fragment dwukrotnie, w terminalu Ubuntu (kodowanie ustawione na utf-8), raz z, ./test.pya potem z ./test.py >out.txt:

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

Bez przekierowania drukuje śmieci. Z przekierowaniem otrzymuję UnicodeDecodeError. Czy ktoś może wyjaśnić, dlaczego otrzymuję błąd dopiero w drugim przypadku, czy jeszcze lepiej udzielić szczegółowego wyjaśnienia, co dzieje się za kurtyną w obu przypadkach?

zedoo
źródło
Ta odpowiedź też może być pomocna.
tzot
Kiedy próbuję odtworzyć twoje odkrycie, otrzymuję UnicodeEncodeError, a nie UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Odpowiedzi:

252

Cały klucz do takich problemów z kodowaniem polega na zrozumieniu, że w zasadzie istnieją dwa różne pojęcia „ciągu znaków” : (1) ciąg znaków i (2) ciąg / tablica bajtów. To rozróżnienie było przez długi czas ignorowane z powodu historycznej wszechobecności kodowań zawierających nie więcej niż 256 znaków (ASCII, Latin-1, Windows-1252, Mac OS Roman,…): te kodowania odwzorowują zestaw typowych znaków na liczby od 0 do 255 (tj. bajty); stosunkowo ograniczona wymiana plików przed pojawieniem się Internetu sprawiła, że ​​sytuacja niezgodnego kodowania była tolerowana, ponieważ większość programów mogła ignorować fakt, że było wiele kodowań, o ile tworzyły tekst, który pozostawał w tym samym systemie operacyjnym: takie programy po prostu traktuj tekst jako bajty (zgodnie z kodowaniem używanym przez system operacyjny). Prawidłowy, nowoczesny pogląd właściwie rozdziela te dwie koncepcje strun, w oparciu o następujące dwa punkty:

  1. Postacie są w większości niezwiązane z komputerami : można je narysować na tablicy kredowej itp., Jak na przykład بايثون, 中 蟒 i 🐍. „Znaki” dla maszyn obejmują również „instrukcje rysowania”, takie jak na przykład spacje, powrót karetki, instrukcje ustawiania kierunku pisania (w przypadku języka arabskiego itp.), Akcenty itp. Standard Unicode zawiera bardzo dużą listę znaków ; obejmuje większość znanych postaci.

  2. Z drugiej strony komputery muszą w jakiś sposób reprezentować abstrakcyjne znaki: w tym celu używają tablic bajtów (włącznie z liczbami od 0 do 255), ponieważ ich pamięć jest podzielona na fragmenty bajtów. Niezbędny proces, który konwertuje znaki na bajty, nazywa się kodowaniem . Dlatego komputer wymaga kodowania, aby przedstawić znaki. Każdy tekst znajdujący się na Twoim komputerze jest kodowany (do momentu wyświetlenia), czy jest przesyłany do terminala (który oczekuje znaków zakodowanych w określony sposób), czy też zapisywany w pliku. Aby wyświetlić lub właściwie „zrozumieć” (np. Przez interpreter Pythona), strumienie bajtów są dekodowane na znaki. Kilka kodowań(UTF-8, UTF-16,…) są zdefiniowane przez Unicode dla jego listy znaków (Unicode definiuje więc zarówno listę znaków, jak i kodowanie dla tych znaków - wciąż są miejsca, w których wyrażenie „kodowanie Unicode” jest sposób odwoływania się do wszechobecnego UTF-8, ale jest to niepoprawna terminologia, ponieważ Unicode zapewnia wiele kodowań).

Podsumowując, komputery muszą wewnętrznie reprezentować znaki za pomocą bajtów i robią to za pomocą dwóch operacji:

Kodowanie : znaki → bajty

Dekodowanie : bajty → znaki

Niektóre kodowania nie mogą zakodować wszystkich znaków (np. ASCII), podczas gdy (niektóre) kodowania Unicode pozwalają na zakodowanie wszystkich znaków Unicode. Kodowanie również niekoniecznie jest unikalne , ponieważ niektóre znaki mogą być przedstawiane bezpośrednio lub jako kombinacja (np. Znaku podstawowego i akcentów).

Zauważ, że koncepcja nowej linii dodaje warstwę komplikacji , ponieważ może być reprezentowana przez różne (sterujące) znaki, które zależą od systemu operacyjnego (jest to powód uniwersalnego trybu odczytu plików nowej linii w Pythonie ).

To, co powyżej nazwałem „znakiem”, jest tym, co Unicode nazywa „ znakiem postrzeganym przez użytkownika ”. Pojedynczy znak postrzegany przez użytkownika może być czasami reprezentowany w Unicode przez połączenie części znaku (znak bazowy, akcenty,…) znalezionych w różnych indeksach na liście Unicode, które są nazywane „ punktami kodowymi” - te punkty kodowe można łączyć ze sobą w „grapheme cluster”. W ten sposób Unicode prowadzi do trzeciej koncepcji łańcucha, złożonej z sekwencji punktów kodowych Unicode, która znajduje się między ciągami bajtów i znaków i jest bliższa temu drugiemu. Nazwę je „ ciągami Unicode ” (jak w Pythonie 2).

Podczas gdy Python może drukować ciągi znaków (postrzeganych przez użytkownika), łańcuchy niebajtowe w Pythonie są zasadniczo sekwencjami punktów kodowych Unicode , a nie znaków postrzeganych przez użytkownika. Wartości punktów kodowych są używane w składni napisów Python \ui \UUnicode. Nie należy ich mylić z kodowaniem znaku (i nie muszą mieć z tym żadnego związku: punkty kodowe Unicode można kodować na różne sposoby).

Ma to ważną konsekwencję: długość łańcucha w Pythonie (Unicode) to liczba punktów kodowych, która nie zawsze jest liczbą znaków postrzeganych przez użytkownika : w ten sposób s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) daje 각 len 3pomimo sposiadania jednego postrzeganego przez użytkownika (koreański) znak (ponieważ jest reprezentowany przez 3 punkty kodowe - nawet jeśli nie musi, jak print("\uac01")widać). Jednak w wielu praktycznych okolicznościach długość łańcucha to liczba znaków postrzeganych przez użytkownika, ponieważ wiele znaków jest zwykle przechowywanych przez Python jako pojedynczy punkt kodowy Unicode.

W Pythonie 2 ciągi znaków Unicode nazywane są… „ciągami znaków Unicode” ( unicodetyp, forma literału u"…"), podczas gdy tablice bajtów są „łańcuchami” ( strtyp, gdzie tablicę bajtów można na przykład skonstruować za pomocą literałów łańcuchowych "…"). W Pythonie 3 ciągi znaków Unicode nazywane są po prostu „ciągami” ( strtyp, forma literału "…"), podczas gdy tablice bajtów to „bajty” ( bytestyp, forma literału b"…"). W konsekwencji coś takiego "🐍"[0]daje inny wynik w Pythonie 2 ( '\xf0'bajt) i Pythonie 3 ( "🐍"pierwszy i jedyny znak).

Mając kilka kluczowych punktów, powinieneś być w stanie zrozumieć większość pytań związanych z kodowaniem!


Normalnie, kiedy drukujesz u"…" na terminalu , nie powinieneś dostać śmieci: Python zna kodowanie twojego terminala. W rzeczywistości możesz sprawdzić, jakiego kodowania oczekuje 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

Jeśli twoje znaki wejściowe mogą być zakodowane za pomocą kodowania terminala, Python zrobi to i wyśle ​​odpowiednie bajty do twojego terminala bez narzekania. Terminal zrobi wszystko, co w jego mocy, aby wyświetlić znaki po zdekodowaniu bajtów wejściowych (w najgorszym przypadku czcionka terminala nie zawiera niektórych znaków i zamiast tego wypisze jakieś puste miejsce).

Jeśli znaki wejściowe nie mogą być zakodowane za pomocą kodowania terminala, oznacza to, że terminal nie jest skonfigurowany do wyświetlania tych znaków. Python będzie narzekał (w Pythonie z a, UnicodeEncodeErrorponieważ ciąg znaków nie może być zakodowany w sposób, który pasuje do twojego terminala). Jedynym możliwym rozwiązaniem jest użycie terminala, który może wyświetlać znaki (albo przez skonfigurowanie terminala tak, aby akceptował kodowanie, które może reprezentować twoje znaki, lub używając innego programu terminala). Jest to ważne, gdy rozpowszechniasz programy, które mogą być używane w różnych środowiskach: komunikaty, które drukujesz, powinny być reprezentowalne na terminalu użytkownika. Dlatego czasami najlepiej jest trzymać się łańcuchów zawierających tylko znaki ASCII.

Jednakże, gdy przekierowujesz lub potokujesz wyjście swojego programu, generalnie nie jest możliwe ustalenie, jakie jest kodowanie wejściowe programu odbierającego, a powyższy kod zwraca pewne domyślne kodowanie: Brak (Python 2.7) lub 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

W razie potrzeby kodowanie stdin, stdout i stderr można jednak ustawić za pomocą PYTHONIOENCODINGzmiennej środowiskowej:

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

Jeśli drukowanie na terminalu nie przyniesie oczekiwanych rezultatów, możesz sprawdzić, czy kodowanie UTF-8, które wprowadziłeś ręcznie, jest poprawne; na przykład, jeśli się nie mylę , pierwszego znaku ( \u001A) nie można wydrukować .

Pod adresem http://wiki.python.org/moin/PrintFails można znaleźć rozwiązanie podobne do poniższego dla Pythona 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

W przypadku Pythona 3 możesz sprawdzić jedno z pytań zadanych wcześniej w StackOverflow.

Eric O Lebigot
źródło
2
@singularity: Dzięki! Dodałem trochę informacji dla Pythona 3.
Eric O Lebigot
2
Dziękuje! Tak długo potrzebowałem tego wyjaśnienia ... Szkoda, że ​​mogę ci dać tylko jeden głos za.
mik01aj
3
Cieszę się, że mogłem pomóc, @ m01! Jedną z motywacji do napisania tej odpowiedzi było to, że w sieci było wiele stron o Unicode i Pythonie, ale odkryłem, że pomimo tego, że są interesujące, nigdy nie pozwoliły mi całkowicie rozwiązać konkretnych problemów z kodowaniem… Naprawdę wierzę, że pamiętając o zasady zawarte w tej odpowiedzi i poświęcenie czasu na ich wykorzystanie podczas rozwiązywania konkretnych problemów z kodowaniem bardzo pomaga.
Eric O Lebigot
3
To jest najlepsze wyjaśnienie Unicode i Pythona w historii. Python Unicode HOWTO powinno zostać zastąpione tym.
stantonk
1
Pozwólcie, że
narysuję
20

Python zawsze koduje łańcuchy Unicode podczas zapisywania do terminala, pliku, potoku itp. Podczas zapisywania do terminala Python może zwykle określić kodowanie terminala i używać go poprawnie. Podczas zapisywania do pliku lub potoku Python domyślnie stosuje kodowanie „ascii”, chyba że wyraźnie zaznaczono inaczej. Pythonowi można powiedzieć, co ma robić, przesyłając dane wyjściowe przez PYTHONIOENCODINGzmienną środowiskową. Powłoka może ustawić tę zmienną przed przekierowaniem wyjścia Pythona do pliku lub potoku, aby znane było prawidłowe kodowanie.

W twoim przypadku wydrukowałeś 4 nietypowe znaki, których twój terminal nie obsługiwał w swojej czcionce. Oto kilka przykładów, które pomogą wyjaśnić zachowanie, ze znakami, które są faktycznie obsługiwane przez mój terminal (który używa cp437, a nie UTF-8).

Przykład 1

Zwróć uwagę, że #codingkomentarz wskazuje kodowanie, w jakim zapisywany jest plik źródłowy . Wybrałem utf8, więc mogłem obsługiwać znaki w źródle, których mój terminal nie mógł. Kodowanie zostało przekierowane na standardowe wyjście błędów, więc można to zobaczyć po przekierowaniu do pliku.

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

Wyjście (uruchamiane bezpośrednio z terminala)

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

Python poprawnie określił kodowanie terminala.

Dane wyjściowe (przekierowane do pliku)

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 nie mógł określić kodowania (brak), więc użył domyślnego „ascii”. ASCII obsługuje tylko konwersję pierwszych 128 znaków Unicode.

Wyjście (przekierowane do pliku, PYTHONIOENCODING = cp437)

cp437

a mój plik wyjściowy był poprawny:

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

Przykład 2

Teraz wrzucę znak w źródle, który nie jest obsługiwany przez mój terminal:

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

Wyjście (uruchamiane bezpośrednio z terminala)

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>

Mój terminal nie zrozumiał tego ostatniego chińskiego znaku.

Wyjście (uruchom bezpośrednio, PYTHONIOENCODING = 437: wymień)

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

Procedury obsługi błędów można określić za pomocą kodowania. W tym przypadku nieznane znaki zostały zastąpione ?. ignorei xmlcharrefreplacekilka innych opcji. W przypadku korzystania z UTF8 (który obsługuje kodowanie wszystkich znaków Unicode), nigdy nie zostaną dokonane zamiany, ale czcionka używana do wyświetlania znaków musi nadal je obsługiwać.

Mark Tolonen
źródło
Nie jest do końca prawdą, że „Podczas zapisu do pliku lub potoku Python domyślnie stosuje kodowanie 'ascii', chyba że wyraźnie zaznaczono inaczej.”. W rzeczywistości Python 3 używa UTF-8 w systemie Mac OS X / Fink.
Eric O Lebigot,
2
Tak, Python 3 domyślnie jest ustawiony na „utf8”, ale na podstawie próbki OP używa Pythona 2.X, który domyślnie ma wartość „ascii”.
Mark Tolonen,
Nie mogłem uzyskać poprawnych danych wyjściowych, manipulując PYTHONIOENCODING. Robi print string.encode("UTF-8")jak sugeruje @Ismail pracował dla mnie.
tripleee
możesz zobaczyć chińskie znaki, jeśli Twoja czcionka je obsługuje, nawet jeśli chcpstrona kodowa ich nie obsługuje. Aby tego uniknąć UnicodeEncodeError: 'charmap', możesz zainstalować win-unicode-consolepakiet.
jfs
Moim problemem jest to, że Python-gitlab CLI dobrze drukuje chińskie znaki w cmd, ale znaki te są śmieciami po przekierowaniu do plików. PYTHONIOENCODING=utf-8rozwiązuje problem.
ElpieKay,
12

Zakoduj go podczas drukowania

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

Dzieje się tak, ponieważ gdy uruchamiasz skrypt ręcznie, python koduje go przed wyprowadzeniem go do terminala, a kiedy potokujesz go, python nie koduje go sam, więc musisz kodować ręcznie podczas wykonywania operacji we / wy.

ismail
źródło
4
Nadal nie odpowiada na pytanie, które dzieje się tutaj WTH. Przecież nieoczekiwanie decyduje się na kodowanie tylko po przekierowaniu, gdy ma to być całkowicie przezroczyste dla procesu.
Maxim Sloyko
Dlaczego python nie koduje go podczas przekierowywania? Czy Python wyraźnie sprawdza i decyduje, że będzie działać inaczej, tylko po to, by było to trudne?
Arafangion
1
czy python ma nawet sposób na rozróżnienie tych dwóch sytuacji? Myślałem (aż do teraz ...), że nie ma sposobu, by to wiedzieć.
zedoo
4
Python może sprawdzić, czy wyjście jest terminalem, jeśli wyprowadza go do potoku, wtedy typ terminala będzie „głupi”. Myślę, że „głupi” powinien ci powiedzieć, dlaczego w tym przypadku Python nie próbuje robić niczego automatycznie, może się to nie udać.
ismail
1
tworzy mojibake, jeśli środowisko używa kodowania znaków, które jest niekompatybilne z utf-8 (np. jest powszechne w systemie Windows). Nie koduj na stałe kodowania znaków swojego środowiska w skrypcie. Skonfiguruj ustawienia regionalne lub PYTHONIOENCODING, zainstaluj win-unicode-console(Windows) lub zaakceptuj parametr wiersza polecenia (jeśli musisz).
jfs