Python3里的unicode和byte string


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前添加前綴brrb來避免反斜杠轉義識別。

注意,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
  • \ue0000chr(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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM