Python3里的unicode和byte string
原文: Python 3 Unicode and Byte Strings
Python2和Python3的一個顯著區別:字符數據分別是用unicode和bytes存儲的。在遷移老代碼和新寫代碼時,你可能都不不知道這個區別,因為大多數字符串算法同時支持這兩種表示;但你應該知道它們的區別。
如果你在使用web service的一些庫,比如urllib(以前叫urllib2)和requests、網絡socket、二進制文件、基於pySerial的串口I/O等,會發現它們現在都是用byte string存數據的。
在比較字符串常量時,你很可能會發現問題。拿unicode string和byte string比較會失敗:有序比較(<, <=, >, >=)時會觸發TypeError,判斷是否相等時則始終會返回False。
字符集簡史
如果你對unicode很熟悉,知道過字符集映射,可以跳過本小節直接看 Python和unicode 一節。
計算機(底層)只能處理數字,因此,為了處理文本,每個字符被賦予了一個獨立的數字,叫做字符碼(character code)。你肯定對ASCII很熟悉,它給128個字符定義了標准碼,包括95個可打印字符和33個不可打印字符(例如空格、LF,tab,Esc,CR)。它的定義追溯至1963年,最初是用來給電報和串口通信使用的:它們用一個字節(8bit)中的7個bit來存儲字符,而最高位則當做糾錯碼使用。
ASCII只定義了英文字母以及美國的常用符號。盡管包含美元符號$(代碼是24(16進制)或36(10進制)),但並不包含其他貨幣符號,例如英鎊£,日元¥,歐元€。
當數據被存儲到磁盤或通過網絡糾錯協議傳輸時,單個字節的最高位(第8位)被空閑出來,可以用於映射額外的128個字符。然而這一位要怎樣使用,映射到哪些字符,有多種方式而且並不統一。盡管Extended ASCII(8bit)包含了英鎊符號£、日元符號¥,但是那會兒歐元還沒確立,也就不可能包含歐元符號了。
Extended ASCII之后,產生了多個版本的8-bit編碼集,其中英國最多使用的是Latin-1(ISO-8859-1)和Microsoft CP1252。這兩個編碼集都映射了英鎊、日元以及其他西歐重音字符,但它們不是100%兼容對方,並且都不包含英鎊符號。
英鎊符號最后實在ISO-8859-15中定義的,它在8859-1的基礎上做了少量修改,而許多網站則用8859-1指代8859-15。另一方面,CP1252的第三版增加了歐元符號的定義。
這些提到的內容,無疑增加了字符集映射的混亂;而wikipedia上則列出了至少50種不同的字符編碼標准。
Unicode
到20世紀80年代末,人們試圖用兩個字節來定義一個通用字符集(unicode),他可以唯一地定義65000多個不同的字符,包括西歐字符、希臘字母、西里爾文、阿拉伯文、科普特和其他東歐字符,以及亞洲字符,像在日文和中文中的漢子都被包含在內。
unicode的前256個字符和ISO-8859-1(Latin-1)字符是完全重合的。歐元符號被正式定義為代碼#20AC。
unicode的目的是用2個字節對現代語言中廣泛使用的字符進行編碼。1996年,unicode 2定義了擴展平面,允許4字節字符代碼支持古代文字和特殊字符集的映射。
unicode 2將字符代碼D800-DBFF定義為高代理代碼點,后面必須跟第二個雙字節代碼(低代理點)。盡管需要4字節,但這兩個字節組合在一起形成了一個3字節的代碼,用於10000到10FFF的字符(在當前規范中)。Emojis是需要4字節擴展點的代碼的一個很好的例子:笑臉 🙂 是字符代碼1F642(編碼為D83DDE42)。
截至本博客發文,有超過137000個unicode字符。還有一些非官方的unicode映射,包括為pIqad Klingon等語言構建的腳本。
unicode的缺點
unicode的明顯缺點是需要為每個字符使用兩個字節。這增加了內存、磁盤、I/O時間的開銷,同時降低了數據傳輸速率。
為了支持更有效地處理西方字符集的方法,定義了UTF-8編碼方案,該方案最多使用4個字節來存儲任何Unicode字符。ASCII碼只需要一個字節,7FF以下的代碼需要兩個字節(這包括大多數歐洲、中東和西里爾字符)。FFFF以下的字符代碼需要3個字節,其余的需要4個字節。這意味着歐元符號(20AC)需要三個字節,而emoji符號比如笑臉(1F642)需要四個字節。
(疑問:UTF-8和unicode兼容嗎??)
Python和unicode
第一次遭遇Python unicode問題,可能是讀取文本文件時發生編碼報錯,也可能是字符無法正確顯示在屏幕上。
python3在讀取文本文件時創建一個TextIO對象,它使用默認編碼將文件中的字節映射為Unicode字符。在Linux和OSX下,默認編碼是UTF-8,而Windows采用CP1252。
如果文本文件不使用Python假定的默認編碼,則需要在打開該文件時指定編碼。要讀取使用Latin-1(ISO-8859-1)編碼的文件,請使用'latin-1':
with open('example.txt', mode='r', encoding='latin_1'):
pass
支持的編碼的完整列表在 python api編解碼器頁面 上。
Python3的所有字符串常量都是unicode。使用小寫轉義字符\u
開頭、緊跟4個數字的轉義序列(\uxxxx
)來定義FFFF之前(包括FFFF)的unicode字符。歐元符號定義為:
euro = '\u20AC'
高於FFFF的unicode字符,使用大寫轉義字符\U
開頭、緊跟8個數字(\Uxxxxxxxx
)。笑臉的emoji符號定義為:
smile = '\U0001F642'
如果打印此值,則僅當輸出設備支持emojis時才會顯示高興的表情(實測,Win10 powershell支持emoji,cmd/wsl則不支持)。
如果你在python2中使用過Unicode文本,那么你肯定知道在字符串文本定義前增加'u'來表示Unicode字符串文本(u'Hello world!
)。 Python3仍然支持這種表示法,只是不再需要了,因為Python3的字符串就是unicode。
Python和byte string
Unicode string,類型為str
Byte string, 類型為bytes
如果你使用底層數據連接,例如串口或網絡套接字(包括web連接和藍牙),你會發現python3以字節字符串的形式傳輸數據:數據類型為bytes。類似地,如果你以二進制模式打開一個文件,你將使用字節字符串(byte string)。
Python3中的字節字符串(也就是打印出來為bytes的類型),支持Unicode字符串(數據類型str)提供的大多數方法。如果代碼使用字符串方法、下標和切片(slice),通常來說代碼不用改,可以繼續使用。
但也有例外的情況,例如startswith
方法,雖然它支持
some_unicode_string.startswith(another_unicode_string)
,和some_byte_string.startswith(another_byte_string)
但它不支持:some_unicode_string.startswith(some_byte_string)
,以及some_byte_string.startswith(some_unicode_string)
在用字符串常量來定義字符串時,在字面量前面增加b
表示定義一個byte string,例如:
byte_hello = b'Hello world!'
byte string支持常見的反斜杠轉義字符,並且可以包含十六進制代碼,例如英鎊符號用\xA3
表示(假設是latin-1字符集)。可以在原始byte string前添加前綴br
或rb
來避免反斜杠轉義識別。
注意,byte string和unicode string這兩種string表示方式,永遠是不兼容的,因此如下的表達式比較結果永遠是False:
'Hello world' == b'Hello world!'
unicode string支持字符串format方法,而byte string並不支持format。如果你用format string來打印一個byte string,你看到的肯定是byte string的表示方式。例如:
message = b'world'
print('Hello {}!'.format(message))
對應的輸出:
Hello b'world'!
字符串的編碼和解碼 (Encoding and Decoding Strings)
decode()
byte ------------> unicode
string <------------ string
encode()
要把byte string轉為unicode,用str.decode()
方法,它接受一個編碼參數,所有平台的默認編碼都是UTF-8。因此前一個例子的改正寫法是:
print('Hello {}!'.format(message.decode()))
如果你在用Windows CP1252字符集,並且是從二進制文件獲取了文本(data是byte string),則可以用如下方式處理:
print('Hello {}!'.format(data.decode('cp1252'))
反過來,要把unicode string轉為byte string,使用bytes.encode()
方法,它也接受一個可選的編碼參數,默認值也是UTF-8。要把我們的“hello world”內容寫到Windows CP1252編碼的文本文件,我們用:
message = 'world'
with open('hello.txt', 'wb') as fp:
fp.write('Hello {}!'.format(message).encode('cp1252'))
實際上,對於本例,我們可能只需要單獨編寫byte string,或者使用字符串拼接:
b'Hello '+message.encode('cp1252')+b'!'
格式串(Format Strings)
作為Python2程序員,尤其是有C背景的程序員,你可能已經使用%
運算符格式化輸出。python3仍然支持unicode string和byte string。但是format string和任何字符串參數必須是同一類型(和前面提到過的startswith()
類似)。要使用byte string的printf樣式格式,請使用:
print(b'Hello %s!' % (b'world'))
但是如果你還沒有治安想格式化字符串(從Python2.7開始有的),你真的應該用用它。因為format string比printf格式強大得多。推薦的格式化輸出方法是使用str.format()
方法。在最簡單的行駛中,format string使用大括號來表示具有與printf相似的數據類型和字段寬度約定的可替換參數:
print('Hello {:s}!'.format('world'))
Python3.6引入了一個新的格式化字符串文本特性,它在unicode字符串上使用f
作為前綴,一允許大括號內的任何Python表達式。通常縮寫為f-strings,這樣就避免了對簡單格式化情況調用format()
的需要,例如hello world實例中的以下變量:
message = 'world'
print(f'Hello {message}!')
事實上,任何有效的Python表達式都可以與大括號一起使用,因此可以調用函數並執行運算,例如:
import random
print(f'Random value {random.random()**2:.2f}')
但這在實踐中可能不是一個好主意,因為很難識別表達式(random.random()**2
)和字符串文本中的格式(:.2f
)。如第一個例子所示,我們應該堅持使用簡單的變量名。
如果使用格式化字符串文本,那么解析f字符串的IDE是必不可少的,因為它會突出語法錯誤和不正確的變量名。
在撰寫本文時,PyCharm強調了語法錯誤和不正確的變量名。在其他流行的pythonide(Atom、Eclipse/pydev、Spyder和VSCode)中,對語法檢查的支持有限(並不總是正確的),但對變量名沒有語義檢查。當你讀到這篇文章的時候,這一點可能已經改變了,所以要確保你的IDE是最新的,這樣你就可以得到f-string的支持了。
總結
Python3的string class(str)存儲Unicode字符串,新的byte string(bytes)類支持單字節字符串。這兩種類型不同,因此字符串表達式必須使用一種形式或另一種形式。以小寫字母b
開頭的是byte string,不以b
開頭的則是unicode string。
尤其是在web頁面上,通常需要將基本的UTF編碼設置為8字符編碼。將byte string轉換為使用Unicode,使用bytes.decode()方法,而要轉換Unicode為byte string,則使用str.encode()。如果需要UTF-8以外的字符集,這兩種方法都允許將字符集編碼指定為可選參數。
新的格式化字符串文本(也叫做f-string)允許在格式字符串的大括號內計算表達式,可以替代使用str.format()。
延伸
- 應當用UTF-8格式存儲
.py
文件,如果是Python2則應該用# coding: utf-8
開頭,避免中文亂碼 - 如果
.py
文件內容只包含ASCII的可打印字符,則存儲為ANSI或UTF-8時,每個字符都是占1個字節,文件大小的輕微差距在於UTF-8格式文件本身的一些flags。 - "UTF-8是Unicode的一種實現":Unicode僅僅是規定了每個字符對應的唯一數字(標量值)。至於這個數字在存儲時用幾個字節(字節序列),這是unicode編碼形式(encoding form)。UTF-8則是建立unicode標量值和字節序列的一個真子集之間的雙射。
- "一個漢字算兩個英文字符"的說法,不適用於Unicode。那是GB2312/GBK/GB18030編碼系統下的便於記憶的規則,但如今應換用UTF-8。
- Unicode 和 UTF-8 有什么區別? - 盛世唐朝的回答 - 知乎
- 字符編碼筆記:ASCII,Unicode 和 UTF-8 - 阮一峰的網絡日志
- Windows10的cmd.exe仍然不是UTF-8的,而Linux則默認使用UTF-8編碼,如果你不想處理編碼之間的相互轉換,可以直接用Linux/MacOSX
打印chr(n)
返回的結果,當n
的取值范圍不同時,開頭的轉義字符不一樣:
\x
開頭:
ASCII范圍內的數字(准確說是[0,160]區間內的數字),
chr(0)
'\x00'
chr(160)
'\xa0'
\u
開頭:unicode在FFFF之前,\u
緊跟4個(16進制)數字:
In [57]: chr(57344)
Out[57]: '\ue000'
\U
開頭:unicode在FFFF之后,\U
緊跟8個(16進制)數字:
In [58]: chr(1114111)
Out[58]: '\U0010ffff'
實際上,上述說法,在Python3.7.3下實測,並不嚴謹:
\x80
也可以用\u0080
和\U00000080
表示- 用
ord('\x80')
和ord('\u0080')
以及ord('\U00000080')
打印,結果都是128 \ue0000
是chr(57344)
的最短表示,但不能用\xhh
表示。因為\xFF
是\x
開頭的能表示的最大值(255)
移植Python2的代碼到Python3,發現原來定義為hoho='\x01\xF4'
的變量,現在需要改成hoho=b'\x01\xF4'
才能使用。這是能手動改代碼的情況。假如沒法修改hoho
的值,並且也不知道hoho的具體取值,該怎么修改代碼呢?嘗試了hoho = hoho.encode()
,雖然能運行,但是效果不對啊?
原因:此時應當用.encode('latin1')
而不是.encode()
,因為.encode()
等同於.encode('utf-8')
:
In [36]: hoho = '\x01\xF4'
In [37]: hoho.encode('latin1')
Out[37]: b'\x01\xf4'
In [38]: hoho.encode()
Out[38]: b'\x01\xc3\xb4'
In [39]: hoho.encode('utf-8')
Out[39]: b'\x01\xc3\xb4'
舉例
貼幾個可以復現問題、帶解決方案的 Python3 編碼問題。
例子1
"""
Python編碼問題,例子1
來源:https://www.cnblogs.com/WangAoBo/p/7108278.html
"""
import binascii
import struct
#\x49\x48\x44\x52\x00\x00\x01\xF4\x00\x00\x01\xA4\x08\x06\x00\x00\x00
crc32key = 0xCBD6DF8A
for i in range(0, 65535):
height = struct.pack('>i', i)
#CRC: CBD6DF8A
# 直接用這句,報錯
#data = '\x49\x48\x44\x52\x00\x00\x01\xF4' + height + '\x08\x06\x00\x00\x00'
# 方法1:換成這句即可
data = b'\x49\x48\x44\x52\x00\x00\x01\xF4' + height + b'\x08\x06\x00\x00\x00'
# 方法2:換成如下6句即可
part1 = '\x49\x48\x44\x52\x00\x00\x01\xF4'
part2 = height
part3 = '\x08\x06\x00\x00\x00'
part1 = part1.encode('latin1')
part3 = part3.encode('latin1')
data = part1 + part2 + part3
crc32result = binascii.crc32(data) & 0xffffffff
if crc32result == crc32key:
print('height is', i)
print(''.join(map(lambda c: "%02X" % c, height)))
例子2
"""
Python編碼問題,例子2
報錯信息:TypeError: a bytes-like object is required, not 'str'
來源:https://blog.csdn.net/weixin_40283816/article/details/83591582
"""
import codecs
import urllib.request
target_url = ('https://archive.ics.uci.edu/ml/machine-learning-'
'databases/undocumented/connectionist-bench/sonar/sonar.all-data')
data = urllib.request.urlopen(target_url)
xList = []
labels = []
for line in data:
# row = line.strip().split(',') # 報錯
# 如下的每一種修改方式,都可以
# row = line.strip().split(',')
# row = line.strip().split(b',')
# row = line.strip().split(','.encode())
# row = line.strip().split(','.encode('utf-8'))
# row = line.strip().split(','.encode('latin1'))
# row = line.decode().strip().split(',')
# row = bytes.decode(line).strip().split(',')
row = codecs.decode(line).strip().split(',')
xList.append(row)
print('Number of Rows of Data = %d' % len(xList))
print('Number of Columns of Data = %d' % len(xList[1]))
例子3
"""
Python編碼問題,例子3
報錯:TypeError: a bytes-like object is required, not 'str'
來源:https://justcode.ikeepstudying.com/2019/01/python-3-5-a-bytes-like-object-is-requirednot-str-%E6%8A%A5%E9%94%99/
"""
import base64
a = 'hello'
# 這句會報錯
#out = base64.b64encode(a)
# 改成如下即可
out = base64.b64encode(a.encode())
print(out)
例子4
"""
Python編碼問題,例子4
報錯:
來源:https://www.zhihu.com/question/60231684
"""
a = '中文'.encode('utf-8')
print(a)
test_str = '\xe4\xb8\xad\xe6\x96\x87'
print(test_str)
print(test_str.encode())
test_str2 = b'\xe4\xb8\xad\xe6\x96\x87'
print(test_str2.decode())
a = '中文'.encode('utf-8')
print(a)
test_str = '\xe4\xb8\xad\xe6\x96\x87'
print(test_str) # 輸出亂碼
print(test_str)
print(test_str.encode('latin1').decode())
例子5
"""
Python編碼問題,例子5
報錯: argument for 's' must be a bytes object
來源:https://segmentfault.com/a/1190000022812087
"""
a = 'hello {:s}'.format('Chris')
print(type(a))
F = open('data.bin', 'wb')
import struct
# 這句,py3下會報錯
#data = struct.pack('>i4sh', 7, b'spam', 8)
# 改成這句即可(增加了'b')
data = struct.pack('>i4sh', 7, b'spam', 8)
data
例子6
"""
來自zheng tianqi的提問
"""
a = '\x00'
# 如何把a變成int類型?
# 實際上,此時的a相當於C語言中的 char a = 0,即ASCII的第一個字符
# 用ord打印即可
out = ord(a)
print(out)