UnicodeDecodeError khi chuyển hướng đến tệp


100

Tôi chạy đoạn mã này hai lần, trong thiết bị đầu cuối Ubuntu (mã hóa được đặt thành utf-8), một lần với ./test.pyvà sau đó với ./test.py >out.txt:

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

Nếu không chuyển hướng nó sẽ in rác. Với chuyển hướng, tôi nhận được một lỗi UnicodeDecodeError. Ai đó có thể giải thích lý do tại sao tôi chỉ gặp lỗi trong trường hợp thứ hai, hoặc thậm chí tốt hơn là đưa ra lời giải thích chi tiết về những gì đang xảy ra đằng sau bức màn trong cả hai trường hợp?


Câu trả lời này cũng có thể hữu ích.
tzot

Khi tôi cố gắng sao chép phát hiện của bạn, tôi nhận được một UnicodeEncodeError, không phải UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Câu trả lời:


252

Chìa khóa của toàn bộ các vấn đề mã hóa như vậy là phải hiểu rằng về nguyên tắc có hai khái niệm khác biệt về "chuỗi" : (1) chuỗi ký tự và (2) chuỗi / mảng byte. Sự phân biệt này hầu như đã bị bỏ qua trong một thời gian dài vì tính phổ biến trong lịch sử của các bảng mã có không quá 256 ký tự (ASCII, Latin-1, Windows-1252, Mac OS Roman,…): các bảng mã này ánh xạ một tập hợp các ký tự phổ biến thành số từ 0 đến 255 (tức là byte); việc trao đổi tệp tương đối hạn chế trước khi web ra đời đã làm cho tình huống mã hóa không tương thích này có thể chấp nhận được, vì hầu hết các chương trình có thể bỏ qua thực tế là có nhiều mã hóa miễn là chúng tạo ra văn bản vẫn trên cùng một hệ điều hành: các chương trình như vậy sẽ đơn giản xử lý văn bản dưới dạng byte (thông qua mã hóa được sử dụng bởi hệ điều hành). Chế độ xem hiện đại, đúng đắn phân tách đúng đắn hai khái niệm chuỗi này, dựa trên hai điểm sau:

  1. Nhân vật chủ yếu là không liên quan đến máy tính : người ta có thể vẽ chúng trên bảng phấn, v.v., chẳng hạn như بايثون, 中 蟒 và 🐍. "Ký tự" cho máy cũng bao gồm "hướng dẫn vẽ" ví dụ như dấu cách, dấu xuống dòng, hướng dẫn đặt hướng viết (đối với tiếng Ả Rập, v.v.), dấu, v.v. Một danh sách ký tự rất lớn được đưa vào tiêu chuẩn Unicode ; nó bao gồm hầu hết các ký tự đã biết.

  2. Mặt khác, máy tính cần phải biểu diễn các ký tự trừu tượng theo một cách nào đó: đối với điều này, chúng sử dụng các mảng byte (bao gồm các số từ 0 đến 255), vì bộ nhớ của chúng có dạng các khối byte. Quá trình cần thiết để chuyển đổi các ký tự thành byte được gọi là (UTF-8, UTF-16,…) được Unicode xác định cho danh sách các ký tự của nó (Unicode do đó xác định cả danh sách các ký tự và mã hóa cho các ký tự này — vẫn còn những chỗ mà người ta coi cụm từ "mã hóa Unicode" như một cách để chỉ UTF-8 phổ biến, nhưng đây là thuật ngữ không chính xác, vì Unicode cung cấp mã hóa . Do đó, một máy tính yêu cầu một bảng mã để biểu diễn các ký tự. Bất kỳ văn bản nào hiện diện trên máy tính của bạn đều được mã hóa (cho đến khi nó được hiển thị), cho dù nó được gửi đến một thiết bị đầu cuối (mong đợi các ký tự được mã hóa theo một cách cụ thể) hay được lưu trong một tệp. Để được hiển thị hoặc được "hiểu" đúng cách (nói cách khác, trình thông dịch Python), các luồng byte được giải mã thành các ký tự. Một vài mã hóa nhiều bảng mã).

Tóm tắt, máy tính cần biểu diễn bên trong các ký tự bằng byte và chúng thực hiện điều đó thông qua hai hoạt động:

Mã hóa : ký tự → byte

Giải mã : byte → ký tự

Một số bảng mã không thể mã hóa tất cả các ký tự (ví dụ: ASCII), trong khi (một số) bảng mã Unicode cho phép bạn mã hóa tất cả các ký tự Unicode. Bảng mã cũng không nhất thiết phải là duy nhất , vì một số ký tự có thể được biểu diễn trực tiếp hoặc dưới dạng kết hợp (ví dụ: ký tự cơ sở và dấu).

Lưu ý rằng khái niệm dòng mới thêm một lớp phức tạp , vì nó có thể được biểu diễn bằng các ký tự (điều khiển) khác nhau phụ thuộc vào hệ điều hành (đây là lý do cho chế độ đọc tệp dòng mới phổ biến của Python ).

Bây giờ, cái mà tôi đã gọi là "ký tự" ở trên là cái mà Unicode gọi là " ký tự do người dùng cảm nhận ". Một ký tự do người dùng cảm nhận đôi khi có thể được biểu diễn bằng Unicode bằng cách kết hợp các phần ký tự (ký tự cơ sở, dấu trọng âm,…) được tìm thấy tại các chỉ mục khác nhau trong danh sách Unicode, được gọi là " điểm mã " —các điểm mã này có thể được kết hợp với nhau để tạo thành một "cụm grapheme". Do đó, Unicode dẫn đến khái niệm chuỗi thứ ba, được tạo thành từ một chuỗi các điểm mã Unicode, nằm giữa chuỗi byte và chuỗi ký tự, và gần với chuỗi ký tự sau hơn. Tôi sẽ gọi chúng là " chuỗi Unicode " (giống như trong Python 2).

Trong khi Python có thể in các chuỗi ký tự (do người dùng cảm nhận), các chuỗi không phải byte của Python về cơ bản là chuỗi các điểm mã Unicode , không phải các ký tự do người dùng cảm nhận. Các giá trị điểm mã là những giá trị được sử dụng trong cú pháp chuỗi của Python \u\UUnicode. Không nên nhầm lẫn chúng với mã hóa của một ký tự (và không phải chịu bất kỳ mối quan hệ nào với nó: các điểm mã Unicode có thể được mã hóa theo nhiều cách khác nhau).

Điều này có một hệ quả quan trọng: độ dài của một chuỗi Python (Unicode) là số điểm mã của nó, không phải lúc nào cũng là số ký tự được người dùng cảm nhận : do đó s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) cho 각 len 3s có một người dùng cảm nhận (tiếng Hàn) ký tự (bởi vì nó được biểu diễn bằng 3 điểm mã — ngay cả khi nó không bắt buộc, như print("\uac01")hiển thị). Tuy nhiên, trong nhiều trường hợp thực tế, độ dài của một chuỗi là số ký tự mà người dùng cảm nhận được, vì nhiều ký tự thường được Python lưu trữ dưới dạng một điểm mã Unicode duy nhất.

Trong Python 2 , chuỗi Unicode được gọi là… "chuỗi Unicode" ( unicodekiểu, dạng chữ u"…"), trong khi mảng byte là "chuỗi" ( strkiểu, ví dụ như mảng byte có thể được xây dựng bằng chuỗi ký tự "…"). Trong Python 3 , chuỗi Unicode được gọi đơn giản là "chuỗi" ( strkiểu, dạng chữ "…"), trong khi mảng byte là "byte" ( byteskiểu, dạng chữ b"…"). Do đó, một cái gì đó giống như "🐍"[0]cho một kết quả khác trong Python 2 ( '\xf0', một byte) và Python 3 ( "🐍", ký tự đầu tiên và duy nhất).

Với một vài điểm chính này, bạn sẽ có thể hiểu hầu hết các câu hỏi liên quan đến mã hóa!


Thông thường, khi bạn in u"…" tới một thiết bị đầu cuối , bạn sẽ không nhận được rác: Python biết mã hóa của thiết bị đầu cuối của bạn. Trên thực tế, bạn có thể kiểm tra mã hóa mà thiết bị đầu cuối mong đợi:

% 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

Nếu các ký tự đầu vào của bạn có thể được mã hóa bằng mã hóa của thiết bị đầu cuối, Python sẽ làm như vậy và sẽ gửi các byte tương ứng đến thiết bị đầu cuối của bạn mà không phàn nàn. Sau đó, thiết bị đầu cuối sẽ cố gắng hết sức để hiển thị các ký tự sau khi giải mã các byte đầu vào (tệ nhất là phông chữ đầu cuối không có một số ký tự và thay vào đó sẽ in ra một số loại trống).

Nếu các ký tự đầu vào của bạn không thể được mã hóa bằng mã hóa của thiết bị đầu cuối, thì điều đó có nghĩa là thiết bị đầu cuối không được định cấu hình để hiển thị các ký tự này. Python sẽ phàn nàn (trong Python cóUnicodeEncodeError vì chuỗi ký tự không thể được mã hóa theo cách phù hợp với thiết bị đầu cuối của bạn). Giải pháp khả thi duy nhất là sử dụng một thiết bị đầu cuối có thể hiển thị các ký tự (bằng cách định cấu hình thiết bị đầu cuối để nó chấp nhận một mã hóa có thể đại diện cho các ký tự của bạn hoặc bằng cách sử dụng một chương trình đầu cuối khác). Điều này rất quan trọng khi bạn phân phối các chương trình có thể được sử dụng trong các môi trường khác nhau: thông báo mà bạn in ra phải có thể biểu diễn được trong thiết bị đầu cuối của người dùng. Vì vậy, đôi khi tốt nhất là bám vào các chuỗi chỉ chứa các ký tự ASCII.

Tuy nhiên, khi bạn chuyển hướng hoặc chuyển đầu ra của chương trình, thì thường không thể biết mã hóa đầu vào của chương trình nhận là gì và đoạn mã trên trả về một số mã hóa mặc định: Không (Python 2.7) hoặc 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

Tuy nhiên, mã hóa của stdin, stdout và stderr có thể được đặt thông qua PYTHONIOENCODINGbiến môi trường, nếu cần:

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

Nếu việc in tới một thiết bị đầu cuối không tạo ra những gì bạn mong đợi, bạn có thể kiểm tra mã hóa UTF-8 mà bạn nhập theo cách thủ công có đúng không; chẳng hạn, ký tự đầu tiên của bạn ( \u001A) không thể in được, nếu tôi không nhầm .

Tại http://wiki.python.org/moin/PrintFails , bạn có thể tìm thấy một giải pháp như sau, cho 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

Đối với Python 3, bạn có thể kiểm tra một trong các câu hỏi được hỏi trước đây trên StackOverflow.


2
@singularity: Cảm ơn! Tôi đã thêm một số thông tin cho Python 3.
Eric O Lebigot

2
Cảm ơn bạn! Tôi cần lời giải thích này trong một thời gian dài ... Thật tiếc khi tôi chỉ có thể cho bạn một ủng hộ.
mik01aj

3
Tôi rất vui vì đã được giúp đỡ, @ m01! Một trong những động lực để viết câu trả lời này là có nhiều trang trên web về Unicode và Python, nhưng tôi thấy rằng mặc dù rất thú vị, nhưng chúng không bao giờ hoàn toàn cho phép tôi giải quyết các vấn đề mã hóa cụ thể… Tôi thực sự tin rằng bằng cách ghi nhớ các nguyên tắc được tìm thấy trong câu trả lời này và dành thời gian sử dụng chúng khi giải quyết các vấn đề mã hóa cụ thể sẽ giúp ích rất nhiều.
Eric O Lebigot

3
Đây là lời giải thích tốt nhất về unicode và python từ trước đến nay. HowTO Python Unicode nên được thay thế bằng mã này.
stantonk

1
Đây, hãy để tôi vẽ ký tự “ghi đè từ phải sang trái” trên bảng đen này…
icktoofay

20

Python luôn mã hóa chuỗi Unicode khi ghi vào thiết bị đầu cuối, tệp, đường dẫn, v.v. Khi viết vào thiết bị đầu cuối, Python thường có thể xác định mã hóa của thiết bị đầu cuối và sử dụng nó một cách chính xác. Khi ghi vào một tệp hoặc đường ống, Python mặc định là mã hóa 'ascii' trừ khi được chỉ dẫn rõ ràng theo cách khác. Python có thể được cho biết phải làm gì khi đầu ra đường ống thông qua PYTHONIOENCODINGbiến môi trường. Một trình bao có thể đặt biến này trước khi chuyển hướng đầu ra Python thành tệp hoặc đường dẫn để mã hóa chính xác được biết.

Trong trường hợp của bạn, bạn đã in 4 ký tự không phổ biến mà thiết bị đầu cuối của bạn không hỗ trợ phông chữ của nó. Dưới đây là một số ví dụ để giúp giải thích hành vi, với các ký tự thực sự được hỗ trợ bởi thiết bị đầu cuối của tôi (sử dụng cp437, không phải UTF-8).

ví dụ 1

Lưu ý rằng #codingchú thích chỉ ra kiểu mã hóa mà tệp nguồn được lưu. Tôi đã chọn utf8 để có thể hỗ trợ các ký tự trong nguồn mà thiết bị đầu cuối của tôi không thể. Mã hóa được chuyển hướng đến stderr để nó có thể được nhìn thấy khi được chuyển hướng đến một tệp.

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

Đầu ra (chạy trực tiếp từ thiết bị đầu cuối)

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

Python đã xác định chính xác mã hóa của thiết bị đầu cuối.

Đầu ra (được chuyển hướng đến tệp)

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 không thể xác định mã hóa (Không có) vì vậy được sử dụng mặc định 'ascii'. ASCII chỉ hỗ trợ chuyển đổi 128 ký tự đầu tiên của Unicode.

Đầu ra (được chuyển hướng đến tệp, PYTHONIOENCODING = cp437)

cp437

và tệp đầu ra của tôi là chính xác:

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

Ví dụ 2

Bây giờ tôi sẽ đưa vào một ký tự trong nguồn không được thiết bị đầu cuối của tôi hỗ trợ:

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

Đầu ra (chạy trực tiếp từ thiết bị đầu cuối)

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>

Thiết bị đầu cuối của tôi không hiểu ký tự Trung Quốc cuối cùng đó.

Đầu ra (chạy trực tiếp, PYTHONIOENCODING = 437: thay thế)

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

Trình xử lý lỗi có thể được chỉ định bằng mã hóa. Trong trường hợp này, các ký tự không xác định được thay thế bằng ?. ignorexmlcharrefreplacelà một số tùy chọn khác. Khi sử dụng UTF8 (hỗ trợ mã hóa tất cả các ký tự Unicode) sẽ không bao giờ thay thế được, nhưng phông chữ được sử dụng để hiển thị các ký tự vẫn phải hỗ trợ chúng.


Không hoàn toàn đúng rằng "Khi ghi vào tệp hoặc đường ống, Python mặc định là mã hóa 'ascii' trừ khi được chỉ dẫn rõ ràng bằng cách khác.". Trên thực tế, Python 3 sử dụng UTF-8, trên Mac OS X / Fink.
Eric O Lebigot

2
Có, Python 3 mặc định là 'utf8', nhưng dựa trên mẫu của OP, anh ấy đang sử dụng Python 2.X, được mặc định là 'ascii'.
Mark Tolonen

Tôi không thể nhận được đầu ra chính xác bằng cách thao tác PYTHONIOENCODING. Làm print string.encode("UTF-8")theo đề xuất của @Ismail đã hiệu quả với tôi.
tripleee

bạn có thể thấy các ký tự Trung Quốc nếu phông chữ của bạn hỗ trợ chúng ngay cả khi chcpcodepage không hỗ trợ chúng. Để tránh UnicodeEncodeError: 'charmap', bạn có thể cài đặt win-unicode-consolegói.
jfs

Vấn đề của tôi là python-gitlab CLI in tốt các ký tự Trung Quốc trong cmd nhưng các ký tự bị rác sau khi được chuyển hướng thành tệp. PYTHONIOENCODING=utf-8giải quyết vấn đề.
ElpieKay

12

Mã hóa nó trong khi in

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

Điều này là do khi bạn chạy tập lệnh theo cách thủ công, python sẽ mã hóa nó trước khi xuất nó ra thiết bị đầu cuối, khi bạn truyền nó, python không tự mã hóa nó nên bạn phải mã hóa thủ công khi thực hiện I / O.


4
Nó vẫn không trả lời câu hỏi WTH đang diễn ra ở đây. Tại sao, ngoài màu xanh, nó quyết định chỉ mã hóa khi được chuyển hướng, khi điều này được cho là hoàn toàn minh bạch với quy trình.
Maxim Sloyko

Tại sao python không mã hóa nó khi thực hiện chuyển hướng? Python có kiểm tra rõ ràng và quyết định rằng nó sẽ làm mọi thứ khác đi chỉ để gây khó khăn không?
Arafangion

1
python thậm chí có một cách để phân biệt hai tình huống? Tôi nghĩ (cho đến bây giờ ...) rằng không có cách nào nó có thể biết được.
zedoo

4
Python có thể kiểm tra xem đầu ra có phải là thiết bị đầu cuối hay không, nếu đầu ra của nó là một đường ống, thì kiểu đầu cuối sẽ là "câm". Tôi đoán "ngu ngốc" sẽ cho bạn biết tại sao Python không cố gắng làm bất cứ điều gì tự động trong trường hợp này, nó có thể thất bại.
ismail

1
nó tạo ra mojibake nếu môi trường sử dụng mã hóa ký tự không tương thích với utf-8 (ví dụ: nó phổ biến trên Windows). Đừng làm cứng mã hóa ký tự của môi trường bên trong tập lệnh của bạn. Định cấu hình ngôn ngữ của bạn hoặc PYTHONIOENCODING, hoặc cài đặt win-unicode-console(Windows) hoặc chấp nhận một tham số dòng lệnh (nếu bạn phải).
jfs
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.