ขอบคุณบิตและชิ้นส่วนจากคำตอบที่หลากหลายฉันคิดว่าเราสามารถต่อคำอธิบายได้
ด้วยการพยายามพิมพ์สตริง unicode, u '\ xe9', Python พยายามเข้ารหัสสตริงนั้นโดยใช้รูปแบบการเข้ารหัสที่เก็บอยู่ใน sys.stdout.encoding Python เลือกการตั้งค่านี้จากสภาพแวดล้อมที่เริ่มต้นขึ้นจริง หากไม่พบการเข้ารหัสที่เหมาะสมจากสภาพแวดล้อมระบบจะเปลี่ยนกลับเป็นค่าเริ่มต้น ASCII เท่านั้น
ตัวอย่างเช่นฉันใช้ bash shell ซึ่งเป็นค่าเริ่มต้นการเข้ารหัสเป็น UTF-8 ถ้าฉันเริ่ม Python มันจะหยิบขึ้นมาและใช้การตั้งค่านั้น:
$ python
>>> import sys
>>> print sys.stdout.encoding
UTF-8
เราออกจาก Python shell สักครู่แล้วตั้งค่าสภาพแวดล้อมของ bash ด้วยการเข้ารหัสแบบปลอม:
$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.
จากนั้นเริ่มต้นไพ ธ อนเชลล์อีกครั้งและตรวจสอบว่ามันกลับไปใช้การเข้ารหัส ASCII ที่เป็นค่าเริ่มต้นแน่นอน
$ python
>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968
บิงโก!
หากคุณพยายามส่งออกอักขระยูนิโค้ดนอก ascii คุณควรได้รับข้อความแสดงข้อผิดพลาดที่ดี
>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9'
in position 0: ordinal not in range(128)
ให้ออกจาก Python และทิ้งเปลือก bash
ตอนนี้เราจะสังเกตเห็นสิ่งที่เกิดขึ้นหลังจาก Python แสดงผลสตริง สำหรับเรื่องนี้ครั้งแรกที่เราจะเริ่มต้นเปลือกทุบตีภายในขั้วกราฟิก (ผมใช้คำพังเพยของ Terminal) และเราจะตั้งสถานีการส่งออกถอดรหัสมาตรฐาน ISO-8859-1 หรือที่รู้จัก latin-1 (ขั้วกราฟิกมักจะมีตัวเลือกให้กับชุดอักขระ เข้ารหัสในหนึ่งในเมนูแบบเลื่อนลง) โปรดทราบว่าสิ่งนี้จะไม่เปลี่ยนการเข้ารหัสสภาพแวดล้อมของเชลล์จริงแต่จะเปลี่ยนเฉพาะวิธีที่เทอร์มินัลจะถอดรหัสเอาท์พุทที่ได้รับมาเล็กน้อยเหมือนที่เว็บเบราว์เซอร์ทำ คุณสามารถเปลี่ยนการเข้ารหัสของเทอร์มินัลอย่างอิสระจากสภาพแวดล้อมของเชลล์ จากนั้นให้เราเริ่มต้น Python จากเชลล์และตรวจสอบว่า sys.stdout.encoding ถูกตั้งค่าเป็นการเข้ารหัสของสภาพแวดล้อมของเชลล์ (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) ไพ ธ อนส่งออกสตริงไบนารี่ตามที่เป็นเทอร์มินัลรับและพยายามจับคู่ค่าของมันกับการแม็พอักขระ latin-1 ใน latin-1, 0xe9 หรือ 233 ให้ผลอักขระ "é" และนั่นคือสิ่งที่เทอร์มินัลแสดง
(2) ความพยายามหลามไปโดยปริยายเข้ารหัสสตริง Unicode กับสิ่งที่โครงการมีการตั้งค่าในปัจจุบัน sys.stdout.encoding ในกรณีนี้มัน "UTF-8" หลังการเข้ารหัส UTF-8 สตริงไบนารี่ที่ได้คือ '\ xc3 \ xa9' (ดูคำอธิบายในภายหลัง) เทอร์มินัลได้รับสตรีมดังกล่าวและพยายามถอดรหัส 0xc3a9 โดยใช้ latin-1 แต่ latin-1 ไปจาก 0 ถึง 255 และดังนั้นถอดรหัสเฉพาะสตรีม 1 ไบต์ในแต่ละครั้ง 0xc3a9 มีความยาว 2 ไบต์ตัวถอดรหัสละติน -1 จึงตีความเป็น 0xc3 (195) และ 0xa9 (169) และให้อักขระ 2 ตัว: Ãและ©
(3) python เข้ารหัสจุดโค้ด unicode u '\ xe9' (233) ด้วยรูปแบบ latin-1 เปลี่ยนช่วงจุดรหัส latin-1 เป็น 0-255 และชี้ไปที่อักขระเดียวกับ Unicode ในช่วงนั้น ดังนั้นรหัสยูนิโคดชี้ในช่วงนั้นจะให้ค่าเดียวกันเมื่อเข้ารหัสใน latin-1 ดังนั้นคุณ '\ xe9' (233) ที่เข้ารหัสใน latin-1 จะให้ผลลัพธ์เป็นสตริงไบนารี '\ xe9' เทอร์มินัลได้รับค่านั้นและพยายามจับคู่บนแมปอักขระ latin-1 เช่นเดียวกับกรณี (1) จะให้ "é" และนั่นคือสิ่งที่แสดง
ตอนนี้เปลี่ยนการตั้งค่าการเข้ารหัสของเทอร์มินัลเป็น UTF-8 จากเมนูแบบเลื่อนลง (เช่นคุณเปลี่ยนการตั้งค่าการเข้ารหัสของเว็บเบราว์เซอร์) ไม่จำเป็นต้องหยุด Python หรือรีสตาร์ทเชลล์ การเข้ารหัสของเทอร์มินัลตรงกับ Python ลองพิมพ์อีกครั้ง:
>>> print '\xe9' # (4)
>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)
>>>
(4) python ส่งออกสตริงไบนารี่ตามที่เป็นอยู่ เทอร์มินัลพยายามถอดรหัสสตรีมนั้นด้วย UTF-8 แต่ UTF-8 ไม่เข้าใจค่า 0xe9 (ดูคำอธิบายในภายหลัง) และดังนั้นจึงไม่สามารถแปลงเป็นจุดโค้ด Unicode ได้ ไม่พบจุดโค้ดไม่พิมพ์อักขระ
(5) python พยายามเข้ารหัส Unicode string โดยปริยายด้วยสิ่งใด ๆ ใน sys.stdout.encoding ยัง "UTF-8" สตริงไบนารี่ที่ได้คือ '\ xc3 \ xa9' เทอร์มินัลได้รับสตรีมและพยายามถอดรหัส 0xc3a9 ด้วย UTF-8 มันให้ค่ารหัสกลับ 0xe9 (233) ซึ่งบนแผนที่อักขระ Unicode ชี้ไปที่สัญลักษณ์ "é" เทอร์มินัลแสดง "é"
(6) ไพ ธ อนเข้ารหัสสตริง unicode ด้วย latin-1 มันให้ผลเป็นสตริงไบนารีที่มีค่าเดียวกัน '\ xe9' อีกครั้งสำหรับเทอร์มินัลนี่ก็เหมือนกับกรณี (4)
สรุป: - Python ส่งออกสตริงที่ไม่ใช่ยูนิโค้ดเป็นข้อมูลดิบโดยไม่พิจารณาการเข้ารหัสเริ่มต้น เทอร์มินัลเพิ่งจะแสดงขึ้นมาหากการเข้ารหัสปัจจุบันตรงกับข้อมูล - Python ส่งออกสตริง Unicode หลังจากการเข้ารหัสโดยใช้รูปแบบที่ระบุใน sys.stdout.encoding - Python รับการตั้งค่านั้นจากสภาพแวดล้อมของเชลล์ - เครื่องจะแสดงเอาต์พุตตามการตั้งค่าการเข้ารหัสของมันเอง - การเข้ารหัสของเทอร์มินัลเป็นอิสระจากเชลล์
รายละเอียดเพิ่มเติมเกี่ยวกับ Unicode, UTF-8 และ latin-1:
Unicode นั้นเป็นตารางของอักขระที่คีย์บางอย่าง (จุดรหัส) ได้รับมอบหมายตามอัตภาพเพื่อชี้ไปที่สัญลักษณ์บางอย่าง เช่นโดยการประชุมได้มีการตัดสินใจแล้วว่าคีย์ 0xe9 (233) เป็นค่าที่ชี้ไปที่สัญลักษณ์ 'é' ASCII และ Unicode ใช้คะแนนรหัสเดียวกันจาก 0 ถึง 127 เช่นเดียวกับ latin-1 และ Unicode จาก 0 ถึง 255 นั่นคือ 0x41 คะแนนถึง 'A' ใน ASCII, latin-1 และ Unicode, 0xc8 คะแนนถึง 'Ü' ใน latin-1 และ Unicode, 0xe9 ชี้ไปที่ 'é' ใน latin-1 และ Unicode
เมื่อทำงานกับอุปกรณ์อิเล็กทรอนิกส์จุดรหัส Unicode จำเป็นต้องมีวิธีที่มีประสิทธิภาพในการแสดงทางอิเล็กทรอนิกส์ นั่นคือรูปแบบการเข้ารหัสที่เกี่ยวกับ มีรูปแบบการเข้ารหัส Unicode ต่างๆ (utf7, UTF-8, UTF-16, UTF-32) วิธีการเข้ารหัสที่ใช้งานง่ายและตรงไปตรงมาที่สุดคือการใช้ค่าของรหัสจุดในแผนที่ Unicode เป็นค่าสำหรับรูปแบบอิเล็กทรอนิกส์ แต่ปัจจุบัน Unicode มีจุดรหัสมากกว่าล้านจุดซึ่งหมายความว่าบางส่วนจำเป็นต้องมี 3 ไบต์ แสดง เพื่อให้ทำงานกับข้อความได้อย่างมีประสิทธิภาพการแมป 1 ถึง 1 จะค่อนข้างใช้การไม่ได้เนื่องจากจะต้องให้จุดรหัสทั้งหมดถูกเก็บไว้ในปริมาณที่เท่ากันโดยมีอย่างน้อย 3 ไบต์ต่ออักขระโดยไม่คำนึงถึงความต้องการที่แท้จริง
รูปแบบการเข้ารหัสส่วนใหญ่มีข้อบกพร่องเกี่ยวกับความต้องการพื้นที่ส่วนใหญ่ทางเศรษฐกิจไม่ครอบคลุมจุดรหัสยูนิโค้ดทั้งหมดเช่น ascii ครอบคลุมเฉพาะ 128 ตัวแรกเท่านั้นในขณะที่ Latin-1 ครอบคลุม 256 ตัวแรกคนอื่น ๆ ที่พยายามที่จะครอบคลุมมากขึ้น เป็นสิ้นเปลืองเนื่องจากพวกเขาต้องการไบต์มากกว่าที่จำเป็นแม้สำหรับตัวอักษร "ถูก" ทั่วไป ตัวอย่างเช่น UTF-16 ใช้อย่างน้อย 2 ไบต์ต่อตัวอักษรรวมถึงที่อยู่ในช่วง ascii ('B' ซึ่งคือ 65 ยังคงต้องใช้พื้นที่เก็บข้อมูล 2 ไบต์ใน UTF-16) UTF-32 ยิ่งสิ้นเปลืองเพราะเก็บอักขระทั้งหมดใน 4 ไบต์
UTF-8 เกิดขึ้นเพื่อแก้ไขภาวะที่กลืนไม่เข้าคายไม่ออกอย่างชาญฉลาดด้วยรูปแบบที่สามารถเก็บคะแนนรหัสด้วยจำนวนตัวแปรของช่องว่างไบต์ ในฐานะที่เป็นส่วนหนึ่งของกลยุทธ์การเข้ารหัส UTF-8 laces โค้ดพอยต์ด้วยบิตแฟล็กที่ระบุ (น่าจะเป็นตัวถอดรหัส) ข้อกำหนดด้านพื้นที่และขอบเขต
การเข้ารหัส UTF-8 ของจุดโค้ดยูนิโคดในช่วง ascii (0-127):
0xxx xxxx (in binary)
- x แสดงพื้นที่จริงที่สงวนไว้เพื่อ "เก็บ" จุดรหัสในระหว่างการเข้ารหัส
- 0 นำหน้าคือธงที่บ่งบอกถึงการถอดรหัส UTF-8 ว่าจุดรหัสนี้จะต้องมีเพียง 1 ไบต์
- เมื่อเข้ารหัส UTF-8 จะไม่เปลี่ยนค่าของรหัสจุดในช่วงที่ระบุ (เช่น 65 ที่เข้ารหัสใน UTF-8 ก็คือ 65) เมื่อพิจารณาว่า Unicode และ ASCII ยังเข้ากันได้ในช่วงเดียวกันโดยบังเอิญทำให้ UTF-8 และ ASCII ยังเข้ากันได้ในช่วงนั้น
เช่นจุดโค้ด Unicode สำหรับ 'B' คือ '0x42' หรือ 0100 0010 ในรูปแบบไบนารี่ (อย่างที่เราบอกว่ามันเหมือนกันใน ASCII) หลังจากการเข้ารหัสใน UTF-8 มันจะกลายเป็น:
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)
การเข้ารหัส UTF-8 ของรหัส Unicode ชี้ไปที่ 127 (ไม่ใช่ ascii):
110x xxxx 10xx xxxx <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx <-- (from 2048 to 65535)
- บิตนำหน้า '110' ระบุถึงตัวถอดรหัส UTF-8 จุดเริ่มต้นของจุดโค้ดที่เข้ารหัสใน 2 ไบต์ในขณะที่ '1110' หมายถึง 3 ไบต์ 11110 จะระบุ 4 ไบต์เป็นต้น
- บิตแฟล็ก '10' ด้านในใช้เพื่อส่งสัญญาณการเริ่มต้นของไบต์ภายใน
- อีกครั้งเครื่องหมายของ x คือช่องว่างที่ค่าจุดโค้ด Unicode ถูกเก็บไว้หลังจากการเข้ารหัส
เช่นจุดโค้ด 'Unicode' คือ 0xe9 (233)
1110 1001 <-- 0xe9
เมื่อ UTF-8 เข้ารหัสค่านี้จะเป็นตัวกำหนดว่าค่านั้นมีค่ามากกว่า 127 และน้อยกว่า 2048 ดังนั้นควรเข้ารหัสใน 2 ไบต์:
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
รหัส 0xe9 Unicode ชี้หลังจากการเข้ารหัส UTF-8 กลายเป็น 0xc3a9 ซึ่งเป็นวิธีที่เครื่องรับ หากเทอร์มินัลของคุณถูกตั้งค่าให้ถอดรหัสสตริงโดยใช้ latin-1 (หนึ่งในการเข้ารหัสที่ไม่ใช่ Unicode) คุณจะเห็นéเพราะมันเกิดขึ้นอย่างนั้น 0xc3 ใน latin-1 ชี้ไปที่Ãและ 0xa9 ถึง©