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 ©.