基本概念
字符:表示數據和信息的字母、數字或其他符號。在電子計算機中,每一個字符與一個二進制編碼相對應。
字符的標識(碼位):是0-1114111的數字,在Unicode標准中以4-6個十六進制數字表示,而且加前綴“U+”。例如,字母A的碼位是U+0041,歐元符號的碼位是U+20AC.
字符的具體表述取決於所用的編碼。編碼是在碼位和字節序列之間轉換時使用的算法。在UTF-8編碼中,A(U+0041)的碼位編碼成單個字節\x41,而在UTF-16LE編碼中編碼成兩個字節\x41\x00。歐元符號(U+20AC)在UFT-8編碼中是三個字節\xe2\x82\xac,而在UTF-16LE編碼中編碼成兩個字節\xac\x20。
編碼:把碼位轉換成字節序列(通俗來說:把字符串轉換成用於存儲或傳輸的字節序列,python中是.encode())
解碼:把字節序列轉換成碼位(通俗來說:把字節序列轉換成人類可讀的文本字符串,python中是.decode())
>>> s = 'café' >>> len(s) # Unicode字符數量 4 >>> b = s.encode('utf8') # 編碼為bytes >>> b b'caf\xc3\xa9' >>> len(b) # 字節數 5 >>> b.decode('utf8') # 解碼 'café
字節概要
新的二進制序列類型在很多方面與 Python 2 的 str 類型不同。首先要知道,Python 內置了兩種基本的二進制序列類型:Python 3 引入的不可變bytes 類型和 Python 2.6 添加的可變 bytearray 類型。(Python 2.6 也引入了 bytes 類型,但那只不過是 str 類型的別名,與 Python 3 的bytes 類型不同。)
bytes 或 bytearray 對象的各個元素是介於 0~255(含)之間的整數,而不像 Python 2 的 str 對象那樣是單個的字符。然而,二進制序列的切片始終是同一類型的二進制序列,包括長度為 1 的切片,如示例:
>>> cafe = bytes('café', encoding='utf_8') >>> cafe b'caf\xc3\xa9' >>> cafe[0] >>> cafe[:1] b'c' >>> cafe_arr = bytearray(cafe) >>> cafe_arr bytearray(b'caf\xc3\xa9') >>> cafe_arr[-1:] bytearray(b'\xa9')
二進制序列有個類方法是 str 沒有的,名為 fromhex,它的作用是解析十六進制數字對(數字對之間的空格是可選的),構建二進制序列:
>>> bytes.fromhex('31 4B CE A9') b'1K\xce\xa9'
python基本編碼解碼器
python自帶了超過100種編碼解碼器,用於在文本和字節中轉換。每個編碼解碼器有一個名稱,如'utf-8',通常有幾個別名如'utf-8'別名有'utf8'、''utf_8、'U8'。
ISO-8859-1(latin1):該編碼是單字節編碼,向下兼容ASCII,其編碼范圍是0x00-0xFF,0x00-0x7F之間完全和ASCII一致,0x80-0x9F之間是控制字符,0xA0-0xFF之間是文字符號。
cp1252:ISO-8859-1的超集,添加了一些有用的符號。
cp437:IBM PC最初的字符集,包含框圖符號,於ISO-8859-1不兼容。
gb2312:簡體中文陳舊標准,亞洲語言使用較廣泛的多字節編碼。
utf-8:目前Web中最常見的8位編碼;與ASCII兼容。
utf-16le:UTF-16的16位編碼方案的一種形式;所有UTF-16支持轉義序列表示超過U+FFFF的碼位。
編碼存在的問題
處理UnicodeError
多數非 UTF 編解碼器只能處理 Unicode 字符的一小部分子集。把文本轉換成字節序列時,如果目標編碼中沒有定義某個字符,那就會拋出UnicodeEncodeError 異常,除非把 errors 參數傳給編碼方法或函數,對錯誤進行特殊處理。
city = 'São Paulo' u8 = city.encode('utf_8') print('utf-8:', u8) #結果: utf-8: b'S\xc3\xa3o Paulo' 'utf_?' 編碼能處理任何字符串
u16 = city.encode('utf_16') print('utf-16:', u16) #結果: utf-16: b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
iso = city.encode('iso8859_1') print('iso:', iso) #結果: iso: b'S\xe3o Paulo' 'iso8859_1' 編碼也能處理字符串 'São Paulo
cpr37 = city.encode('cp437') #結果: 報錯 'cp437' 無法編碼 'ã'(帶波形符的“a”)
cp_ig = city.encode('cp437', errors='ignore') print('cp ignore:', cp_ig) #結果: cp ignore: b'So Paulo'
cp_rp = city.encode('cp437', errors='replace') print('cp replace:', cp_rp) #結果: cp replace: b'S?o Paulo'
如上處理策略:
1.error='ignore' 處理方式悄無聲息地跳過無法編碼的字符;這樣做通常很是不妥
2.編碼時指定error='replace',把無法編碼的字符替換成'?';數據損壞了,但是用戶知道出現了問題
3.把errors參數注冊額外字符串,方法是把一個名稱和一個錯誤處理函數傳遞給codes.register_error函數。(參考python標准庫)
處理UnicodeDecodeError
不是每個字節都包含有效的ASCALL字符,也不是每個字符序列都是有效的UTF-8或UTF16。因此,把二進制序列轉換為文本時,如果假設是這兩個編碼中的一個,遇到無法轉換的字節序列時會拋出UnicodeDecodeError。
octets = b'Montr\xe9al' #iso8859_1編碼 cp1252 = octets.decode('cp1252') print(cp1252) #結果:Montréal 正確解碼 cp1252是iso8859_1的超集 iso = octets.decode('iso8859_7') print(iso) #結果:Montrιal 錯誤解碼 koi8_r = octets.decode('koi8_r') print(koi8_r) #結果:MontrИal 錯誤解碼 #utf_8 = octets.decode('utf-8') print(utf_8) #結果:異常 不是有效的utf-8字符串 new_utf_8 = octets.decode('utf-8', errors='replace') print(new_utf_8) #結果:Montr�al 官方指定的錯誤處理方式
處理SyntaxError
python3默認使用utf-8編碼源碼,如果加載的模塊中包含utf-8之外的數據,而且沒有聲明編碼,則會拋出SyntaxError異常。linux和OS X系統大都使用utf-8,因此在打開windows系統中使用cp1252編碼的.py文件可能發生這種情況。
為了修正這個問題,可以在文件頂部添加一個神奇的coding注釋, coding:cp1252
判斷文件字節序列編碼
使用命令行工具chardetect(python庫Chardet提供的)
處理文本文件
處理文本的最佳實踐是“Unicode 三明治”(如圖下圖所示)。 意思是,要盡早把輸入(例如讀取文件時)的字節序列解碼成字符串。這種三明治中的“肉片”是程序的業務邏輯,在這里只能處理字符串對象。在其他處理過程中,一定不能編碼或解碼。對輸出來說,則要盡量晚地把字符串編碼成字節序列。多數 Web 框架都是這樣做的,使用框架時很少接觸字節序列。例如,在 Django 中,視圖應該輸出 Unicode 字符串;Django 會負責把響應編碼成字節序列,而且默認使用 UTF-8 編碼。

#打開一個文件cafe.txt並寫入內容,w是對文件的模式操作(寫操作), encoding是對文件操作的編碼 fp = open('cafe.txt', 'w', encoding='utf_8') fp_len = fp.write('café') print('fp的io信息:', fp) print('寫入到文件中內容的長度:', fp_len) fp.close() #獲取文件的內容 fp2 = open('cafe.txt') print('fp2的io信息:', fp2) ''' 因為和上面的寫入的編碼不同,所以直接以默認的編碼打開,無法處理é而引發異常 ''' #print(fp2.read()) fp2.close() #解決fp2無法或許文件內容的方法指定打開的時候編碼 fp3 = open('cafe.txt', encoding='utf-8') print('fp3的io信息:', fp3) print('fp3中的文件內容:', fp3.read()) fp3.close() fp4 = open('cafe.txt', 'rb') print('fp4的io信息:', fp4) print('fp4的文件內容:', fp4.read().decode('utf-8')) fp4.close() #另外一種不太可取的解決方案, errors可以設置成replace或者ignore fp5 = open('cafe.txt', 'r', errors='ignore') print('fp5的io信息:', fp5) print('fp5的文件內容:', fp5.read())
以上代碼結果:
fp的io信息: <_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'> 寫入到文件中內容的長度: 4 fp2的io信息: <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='US-ASCII'> fp3的io信息: <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf-8'> fp3中的文件內容: café fp4的io信息: <_io.BufferedReader name='cafe.txt'> fp4的文件內容: café fp5的io信息: <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='US-ASCII'> fp5的文件內容: caf
Unicode規范化
使用unicodedata.normalize提供的nuicode規范化
NFC使用最少碼位構成等價的字符串,而NFD把組合字符分解成基字符和單獨的組合字符。
>>> s1 = 'café' >>> s2 = 'cafe\u0301' >>> s1, s2 ('café', 'café') >>> len(s1), len(s2) (4, 5) >>> s1 == s2 False
實際 é === 'e\u0301' 是成立的,而Python 看到的是不同的碼位序列,因此判定二者不相等,則需要使用NFC或NFD。
from unicodedata import normalize s1 = 'café' # 把"e"和重音符組合在一起 s2 = 'cafe\u0301' # 分解成"e"和重音符 print('s1和s2的長度:', len(s1), len(s2)) print('NFC標准化處理以后的s1,s2的長度:', len(normalize('NFC', s1)), len(normalize('NFC', s2))) print('NFD標准化處理以后的s1,s2的長度:', len(normalize('NFD', s1)), len(normalize('NFD', s2))) print(normalize('NFC', s1), normalize('NFC', s2))
#結果:
s1和s2的長度: 4 5 NFC標准化處理以后的s1,s2的長度: 4 4 NFD標准化處理以后的s1,s2的長度: 5 5 café café
