Tại sao Python in các ký tự unicode khi mã hóa mặc định là ASCII?


139

Từ trình bao Python 2.6:

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

Tôi dự kiến ​​sẽ có một số lỗi vô nghĩa hoặc Lỗi sau câu lệnh in, vì ký tự "é" không phải là một phần của ASCII và tôi chưa chỉ định mã hóa. Tôi đoán tôi không hiểu ASCII là mã hóa mặc định nghĩa là gì.

BIÊN TẬP

Tôi chuyển phần chỉnh sửa sang phần Câu trả lời và chấp nhận nó theo đề xuất.


6
Sẽ thật tuyệt nếu bạn có thể biến bản chỉnh sửa đó thành câu trả lời và chấp nhận nó.
Mercator

2
In '\xe9'trong một thiết bị đầu cuối được cấu hình cho UTF-8 sẽ không in é. Nó sẽ in một ký tự thay thế (thường là một dấu hỏi) vì \xe9đây không phải là một chuỗi UTF-8 hợp lệ (nó thiếu hai byte nên theo byte dẫn đầu đó). Nó chắc chắn sẽ không được hiểu là Latin-1 thay vào đó.
Martijn Pieters

2
@MartijnPieters Tôi nghi ngờ bạn có thể đã lướt qua phần mà tôi đã chỉ định rằng thiết bị đầu cuối được đặt thành giải mã theo ISO-8859-1 (latin1) khi tôi xuất ra \xe9để in é.
Michael Ekoka

2
À đúng rồi, tôi đã bỏ lỡ phần đó; thiết bị đầu cuối có cấu hình khác với vỏ. Kiểm tra.
Martijn Pieters

Tôi lướt qua câu trả lời nhưng thực ra, tôi có chuỗi không có tiền tố u cho python 2.7. Tại sao cái đó vẫn được xử lý như unicode? (sys.getdefaultencoding của tôi () là ascii)
dtc

Câu trả lời:


104

Nhờ các bit và phần từ các câu trả lời khác nhau, tôi nghĩ rằng chúng ta có thể đưa ra một lời giải thích.

Bằng cách cố gắng in một chuỗi unicode, u '\ xe9', Python cố gắng mã hóa chuỗi đó bằng cách sử dụng lược đồ mã hóa hiện được lưu trữ trong sys.stdout.encoding. Python thực sự chọn cài đặt này từ môi trường mà nó được bắt đầu từ đó. Nếu nó không thể tìm thấy một mã hóa phù hợp từ môi trường, chỉ sau đó nó mới trở lại mặc định , ASCII.

Ví dụ: tôi sử dụng shell bash để mã hóa mặc định thành UTF-8. Nếu tôi khởi động Python từ nó, nó sẽ chọn và sử dụng cài đặt đó:

$ python

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

Hãy chờ một lát thoát khỏi vỏ Python và thiết lập môi trường của bash với một số mã hóa không có thật:

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

Sau đó khởi động lại lớp vỏ trăn và xác minh rằng nó thực sự trở lại mã hóa ascii mặc định của nó.

$ python

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

Chơi lô tô!

Nếu bây giờ bạn cố gắng xuất một số ký tự unicode bên ngoài ascii, bạn sẽ nhận được một thông báo lỗi đẹp

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

Cho phép thoát Python và loại bỏ bash shell.

Bây giờ chúng ta sẽ quan sát những gì xảy ra sau khi Python xuất chuỗi. Để làm điều này, trước tiên chúng ta sẽ bắt đầu một bash shell trong một thiết bị đầu cuối đồ họa (tôi sử dụng Gnome Terminal) và chúng ta sẽ đặt thiết bị đầu cuối để giải mã đầu ra với ISO-8859-1 aka latin-1 (thiết bị đầu cuối đồ họa thường có tùy chọn Đặt ký tự Mã hóa trong một trong các menu thả xuống của họ). Lưu ý rằng điều này không thay đổi mã hóa của môi trường shell thực tế , nó chỉ thay đổi cách chính thiết bị đầu cuối sẽ giải mã đầu ra mà nó đưa ra, giống như trình duyệt web. Do đó, bạn có thể thay đổi mã hóa của thiết bị đầu cuối, độc lập từ môi trường của shell. Sau đó, hãy khởi động Python từ shell và xác minh rằng sys.stdout.encoding được đặt thành mã hóa của môi trường shell (UTF-8 cho tôi):

$ 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 xuất chuỗi nhị phân như hiện tại, terminal nhận nó và cố gắng khớp giá trị của nó với bản đồ ký tự latin-1. Trong latin-1, 0xe9 hoặc 233 mang lại ký tự "é" và đó là những gì thiết bị đầu cuối hiển thị.

(2) python cố gắng mã hóa hoàn toàn chuỗi Unicode với bất kỳ lược đồ nào hiện đang được đặt trong sys.stdout.encoding, trong trường hợp này là "UTF-8". Sau khi mã hóa UTF-8, chuỗi nhị phân kết quả là '\ xc3 \ xa9' (xem phần giải thích sau). Terminal nhận luồng như vậy và cố gắng giải mã 0xc3a9 bằng latin-1, nhưng latin-1 đi từ 0 đến 255 và do đó, chỉ giải mã luồng 1 byte mỗi lần. 0xc3a9 dài 2 byte, bộ giải mã latin-1 do đó hiểu nó là 0xc3 (195) và 0xa9 (169) và mang lại 2 ký tự: Ã và ©.

(3) python mã hóa điểm mã unicode u '\ xe9' (233) với sơ đồ latin-1. Hóa ra phạm vi điểm mã Latin-1 là 0-255 và trỏ đến chính xác ký tự như Unicode trong phạm vi đó. Do đó, các điểm mã Unicode trong phạm vi đó sẽ mang lại cùng một giá trị khi được mã hóa theo tiếng Latin-1. Vì vậy, u '\ xe9' (233) được mã hóa theo tiếng Latin-1 cũng sẽ mang lại chuỗi nhị phân '\ xe9'. Terminal nhận giá trị đó và cố gắng khớp nó trên bản đồ ký tự latin-1. Giống như trường hợp (1), nó mang lại "é" và đó là những gì được hiển thị.

Bây giờ chúng ta hãy thay đổi cài đặt mã hóa của thiết bị đầu cuối thành UTF-8 từ menu thả xuống (giống như bạn sẽ thay đổi cài đặt mã hóa của trình duyệt web). Không cần phải dừng Python hoặc khởi động lại shell. Mã hóa của thiết bị đầu cuối hiện khớp với Python. Hãy thử in lại:

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

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

>>>

(4) python xuất ra một chuỗi nhị phân . Thiết bị đầu cuối cố gắng giải mã luồng đó bằng UTF-8. Nhưng UTF-8 không hiểu giá trị 0xe9 (xem phần giải thích sau) và do đó không thể chuyển đổi nó thành điểm mã unicode. Không tìm thấy điểm mã, không in ký tự.

(5) python cố gắng mã hóa hoàn toàn chuỗi Unicode bằng bất cứ thứ gì có trong sys.stdout.encoding. Vẫn là "UTF-8". Chuỗi nhị phân kết quả là '\ xc3 \ xa9'. Terminal nhận được luồng và cố gắng giải mã 0xc3a9 cũng bằng UTF-8. Nó mang lại giá trị mã 0xe9 (233), trên bản đồ ký tự Unicode trỏ đến ký hiệu "é". Thiết bị đầu cuối hiển thị "é".

(6) python mã hóa chuỗi unicode bằng latin-1, nó mang lại một chuỗi nhị phân có cùng giá trị '\ xe9'. Một lần nữa, đối với thiết bị đầu cuối, điều này khá giống với trường hợp (4).

Kết luận: - Python đưa ra các chuỗi không unicode dưới dạng dữ liệu thô, mà không xem xét mã hóa mặc định của nó. Thiết bị đầu cuối chỉ tình cờ hiển thị chúng nếu mã hóa hiện tại của nó khớp với dữ liệu. - Python xuất các chuỗi Unicode sau khi mã hóa chúng bằng cách sử dụng lược đồ được chỉ định trong sys.stdout.encoding. - Python nhận cài đặt đó từ môi trường của shell. - thiết bị đầu cuối hiển thị đầu ra theo các cài đặt mã hóa riêng của nó. - mã hóa của thiết bị đầu cuối độc lập với vỏ.


Thêm chi tiết về unicode, UTF-8 và latin-1:

Unicode về cơ bản là một bảng các ký tự trong đó một số khóa (điểm mã) đã được gán theo quy ước để trỏ đến một số ký hiệu. ví dụ: theo quy ước, người ta đã quyết định rằng khóa 0xe9 (233) là giá trị trỏ đến biểu tượng 'é'. ASCII và Unicode sử dụng cùng một điểm mã từ 0 đến 127, cũng như Latin-1 và Unicode từ 0 đến 255. Nghĩa là, 0x41 điểm đến 'A' trong ASCII, latin-1 và Unicode, 0xc8 điểm thành 'Ü' trong latin-1 và Unicode, 0xe9 trỏ đến 'é' trong tiếng Latin-1 và Unicode.

Khi làm việc với các thiết bị điện tử, các điểm mã Unicode cần một cách hiệu quả để được trình bày bằng điện tử. Đó là những gì chương trình mã hóa là về. Các sơ đồ mã hóa Unicode khác nhau tồn tại (utf7, UTF-8, UTF-16, UTF-32). Cách tiếp cận mã hóa trực tiếp và trực tiếp nhất sẽ chỉ đơn giản là sử dụng giá trị của điểm mã trong bản đồ Unicode làm giá trị cho dạng điện tử của nó, nhưng Unicode hiện có hơn một triệu điểm mã, có nghĩa là một số trong số chúng yêu cầu 3 byte bày tỏ. Để hoạt động hiệu quả với văn bản, ánh xạ 1 đến 1 sẽ không thực tế, vì nó sẽ yêu cầu tất cả các điểm mã được lưu trữ trong cùng một không gian, với tối thiểu 3 byte cho mỗi ký tự, bất kể nhu cầu thực tế của chúng.

Hầu hết các lược đồ mã hóa đều có những thiếu sót liên quan đến yêu cầu không gian, các lược đồ kinh tế nhất không bao gồm tất cả các điểm mã unicode, ví dụ ascii chỉ bao gồm 128 đầu tiên, trong khi latin-1 bao gồm 256 đầu tiên. lãng phí, vì chúng đòi hỏi nhiều byte hơn mức cần thiết, ngay cả đối với các ký tự "giá rẻ" phổ biến. Chẳng hạn, UTF-16 sử dụng tối thiểu 2 byte cho mỗi ký tự, bao gồm cả các byte trong phạm vi ascii ('B' là 65, vẫn cần 2 byte lưu trữ trong UTF-16). UTF-32 thậm chí còn lãng phí hơn vì nó lưu trữ tất cả các ký tự trong 4 byte.

UTF-8 tình cờ đã giải quyết một cách khéo léo tình huống khó xử, với một sơ đồ có thể lưu trữ các điểm mã với một lượng không gian byte khác nhau. Là một phần của chiến lược mã hóa, UTF-8 viền các điểm mã với các bit cờ biểu thị (có lẽ là để giải mã) các yêu cầu không gian và ranh giới của chúng.

Mã hóa UTF-8 của các điểm mã unicode trong phạm vi ascii (0-127):

0xxx xxxx  (in binary)
  • x hiển thị không gian thực tế dành riêng để "lưu trữ" điểm mã trong quá trình mã hóa
  • Số 0 đứng đầu là cờ biểu thị cho bộ giải mã UTF-8 rằng điểm mã này sẽ chỉ yêu cầu 1 byte.
  • khi mã hóa, UTF-8 không thay đổi giá trị của các điểm mã trong phạm vi cụ thể đó (tức là 65 được mã hóa trong UTF-8 cũng là 65). Xét rằng Unicode và ASCII cũng tương thích trong cùng phạm vi, điều này khiến UTF-8 và ASCII cũng tương thích trong phạm vi đó.

ví dụ: điểm mã Unicode cho 'B' là '0x42' hoặc 0100 0010 ở dạng nhị phân (như chúng tôi đã nói, nó giống nhau trong ASCII). Sau khi mã hóa trong UTF-8, nó trở thành:

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)

Mã hóa UTF-8 của các điểm mã Unicode trên 127 (không phải mã ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • các bit hàng đầu '110' chỉ ra cho bộ giải mã UTF-8, điểm bắt đầu của một điểm mã được mã hóa bằng 2 byte, trong khi '1110' chỉ ra 3 byte, 11110 sẽ chỉ ra 4 byte và cứ thế.
  • các bit cờ '10' bên trong được sử dụng để báo hiệu sự bắt đầu của một byte bên trong.
  • một lần nữa, dấu x x đánh dấu khoảng trống nơi giá trị điểm mã Unicode được lưu trữ sau khi mã hóa.

ví dụ: điểm mã Unicode là 0xe9 (233).

1110 1001    <-- 0xe9

Khi UTF-8 mã hóa giá trị này, nó xác định rằng giá trị này lớn hơn 127 và nhỏ hơn 2048, do đó nên được mã hóa thành 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

Mã Unicode 0xe9 sau khi mã hóa UTF-8 trở thành 0xc3a9. Đó chính xác là cách thiết bị đầu cuối nhận được nó. Nếu thiết bị đầu cuối của bạn được đặt để giải mã các chuỗi bằng cách sử dụng latin-1 (một trong những mã hóa di sản không phải là mã đơn), bạn sẽ thấy, vì điều đó xảy ra khi 0xc3 trong latin-1 điểm thành à và 0xa9 thành ©.


6
Giải thích tuyệt vời. Bây giờ tôi đã hiểu UTF-8!
Bác sĩ mã hóa

2
Được rồi, tôi đọc qua toàn bộ bài viết của bạn trong khoảng 10 giây. Nó nói, "Python hút khi nói đến mã hóa."
Andrew

Giải thích tuyệt vời. Bạn có thể giải quyết câu hỏi này ?
Maggyero

26

Khi các ký tự Unicode được in ra thiết bị xuất chuẩn, sys.stdout.encodingđược sử dụng. Một ký tự không phải là Unicode được giả sử là có trong sys.stdout.encodingvà chỉ được gửi đến thiết bị đầu cuối. Trên hệ thống của tôi (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() chỉ được sử dụng khi Python không có tùy chọn khác.

Lưu ý rằng Python 3.6 trở lên bỏ qua các mã hóa trên Windows và sử dụng API Unicode để ghi Unicode vào thiết bị đầu cuối. Không có cảnh báo UnicodeEncodeError và ký tự chính xác được hiển thị nếu phông chữ hỗ trợ nó. Ngay cả khi phông chữ không hỗ trợ, các ký tự vẫn có thể được cắt từ thiết bị đầu cuối đến một ứng dụng có phông chữ hỗ trợ và nó sẽ chính xác. Nâng cấp!


8

Python REPL cố gắng chọn mã hóa nào sẽ sử dụng từ môi trường của bạn. Nếu nó tìm thấy một cái gì đó lành mạnh thì tất cả chỉ hoạt động. Đó là khi nó không thể tìm ra những gì đang xảy ra mà nó phát hiện ra.

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

3
Vì tò mò, tôi sẽ thay đổi sys.stdout.encoding thành ascii như thế nào?
Michael Ekoka

2
@TankorSmash Tôi sẽ nhận được TypeError: readonly attributevào ngày 2.7.2
Kos

4

Bạn đã chỉ định mã hóa bằng cách nhập chuỗi Unicode rõ ràng. So sánh kết quả của việc không sử dụng utiền tố.

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

>>> 

Trong trường hợp \xe9sau đó Python giả định mã hóa mặc định của bạn (Ascii), do đó in ... một cái gì đó trống.


1
Vì vậy, nếu tôi hiểu rõ, khi tôi in ra các chuỗi unicode (điểm mã), python giả định rằng tôi muốn một đầu ra được mã hóa trong utf-8, thay vì chỉ cố gắng đưa cho tôi những gì nó có thể có trong ascii?
Michael Ekoka

1
@mike: AFAIK những gì bạn nói là chính xác. Nếu nó đã in ra các ký tự Unicode nhưng được mã hóa thành ASCII, mọi thứ sẽ bị cắt xén và có lẽ tất cả những người mới bắt đầu sẽ hỏi, "Tại sao tôi không thể in văn bản Unicode?"
Đánh dấu Rushakoff

2
Cảm ơn bạn. Tôi thực sự là một trong những người mới bắt đầu, nhưng đến từ phía những người có hiểu biết về unicode, đó là lý do tại sao hành vi này làm tôi thất vọng một chút.
Michael Ekoka

3
R., không đúng, vì '\ xe9' không có trong bộ ký tự ascii. Các chuỗi không Unicode được in bằng sys.stdout.encoding, các chuỗi Unicode được mã hóa thành sys.stdout.encoding trước khi in.
Đánh dấu Tolonen

0

Nó hoạt động với tôi:

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

1
Hack bẩn giá rẻ chắc chắn sẽ phá vỡ một cái gì đó khác. Không khó để làm điều đó đúng cách!
Chris Johnson

0

Theo mã hóa và chuyển đổi chuỗi mặc định / ẩn của Python :

  • Khi printing unicode, nó encoded với <file>.encoding.
    • khi encodingkhông được đặt, unicodenó được chuyển đổi hoàn toàn thành str(vì codec cho sys.getdefaultencoding()nghĩa là ascii, bất kỳ ký tự quốc gia nào cũng sẽ gây ra a UnicodeEncodeError)
    • đối với các luồng tiêu chuẩn, encodingđược suy ra từ môi trường. Nó thường đặt ttycác luồng fot (từ cài đặt ngôn ngữ của thiết bị đầu cuối), nhưng có khả năng không được đặt cho các đường ống
      • vì vậy a print u'\xe9'có khả năng thành công khi đầu ra tới một thiết bị đầu cuối và thất bại nếu nó được chuyển hướng. Một giải pháp là encode()chuỗi với mã hóa mong muốn trước khi printing.
  • Khi printing str, các byte được gửi đến luồng như hiện tại. Những glyphs mà thiết bị đầu cuối hiển thị sẽ phụ thuộc vào cài đặt ngôn ngữ của nó.
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.