1. 程式人生 > >[Python除錯] 'gbk' codec can't encode character xxx in position的錯誤解決&編碼與解碼的思考探究

[Python除錯] 'gbk' codec can't encode character xxx in position的錯誤解決&編碼與解碼的思考探究

錯誤出現

使用request模組爬取網頁,將頁面原始檔res.text儲存到檔案get.html時,

import request
res = requests.get('http://weibo.com')
with open(r'd:\get.html', 'w') as f:
    f.write(res.text)

發生瞭如下錯誤:

Traceback (most recent call last):
  File "E:/Project/Study/study.py", line 16, in <module>
    f.write(res.text)
UnicodeEncodeError: 'gbk' codec can't encode character '\xca' in position 349: illegal multibyte sequence

這是一個編碼錯誤,翻譯為:
gbk編解碼器不能對位置349的字元’\xca’編碼:不合法的多位元組流
encode指編碼,是指將Unicode字元按照編碼規則編碼為位元組流
例如將中文的"好"字編碼為UTF-8和GBK

char = '好'
print(char.encode('utf-8'))
print(char.encode('gbk'))
#輸出
b'\xe5\xa5\xbd'
#得到位元組流(Utf-8將漢字編碼為3個位元組) b'\xba\xc3' #gbk將漢字編碼為2個位元組

意味著GBK編碼器不能對字元’\xca’編碼(Python3中能顯示的字元都是Unicode字元),
錯誤等價於以下:

char = '\xca'
char.encode('gbk')
#輸出
UnicodeEncodeError: 'gbk' codec can't encode character '\xca' in position 0: illegal multibyte sequence

錯誤分析

UnicodeEncodeError: ‘gbk’ codec can’t encode character ‘\xca’ in position 349: illegal multibyte sequence

對於,首先要找出出錯的位置character '\xca' in position 349

text_arr=res.text.split('\n') #將原始碼分行
length=i=0 #length是每行長度的累加,i是找到的行數
while True:
    i=i+1
    length+=len(arr[i])
    if length>349 : #找出比349大的位置行在第幾行 
        print(i)
        break
#輸出
11 

所以出錯位置在text_arr[10],即原始碼第11行

>>>text_arr[10]
    window.use_fp = "1" == "1"; // ÊÇ·ñ²É¼¯É豸ָÎÆ¡£

找到出錯的字元位置

import re
>>>re.search('\xca',text_arr[10])
<re.Match object; span=(35, 36), match='Ê'>
>>>'Ê'=='\xca'
True

於是,確定不能編碼的字元為該行第35個字元Ê
先不管這個字元,首先思考下為什麼原始碼res.text為什麼會出現亂碼?
猜測res.text不是用Unicode編碼的,但是嘗試用Unicode編碼,所以顯示亂碼

'    window.use_fp = "1" == "1"; // ÊÇ·ñ²É¼¯É豸ָÎÆ¡£'

根據request模組的說明,修改原始碼的編碼方式res.encoding為源網頁<meta>標籤中charset的屬性

>>>	re.search('charset=(.*?)[;"]',res.text).group(1) #查詢charset
'gb2312'
>>>	res.encoding
'ISO-8859-1'
>>>	res.encoding='gb2312'

再次剛才顯示出錯的行,發現亂碼已經修復,能夠成功編碼並儲存

>>>	res.text.split('\n')[10] #改變編碼後的原來那行
'    window.use_fp = "1" == "1"; // 是否採集裝置指紋。' #顯示為Unicode字元,亂碼修復了
#儲存
>>> with open(r'd:\get.html', 'w') as f:
	try:
		f.write(res.text)
	except : 
		print("儲存失敗")
		raise
	else : 
		print("儲存成功")
儲存成功

進一步思考

再一次理清思路:
以下是儲存為Unicode的源網頁1.html,假如爬取該網頁res=request.get("1.html")

<html>
<meta content="text/html; charset=gb2312">
'    window.use_fp = "1" == "1"; // 是否採集裝置指紋。' 
</html>

res.content儲存著從源網頁獲取的Unicode字串使用了gb2312編碼後的位元組流

>>>	res.content 
b'    window.use_fp = "1" == "1"; // \xca\xc7\xb7\xf1\xb2\xc9\xbc\xaf\xc9\xe8\xb1\xb8\xd6\xb8\xce\xc6\xa1\xa3'

res.encoding表示從content解碼(decode)到text時使用的編碼

>>> res.encoding
'ISO-8859-1'  #一開始失敗的編碼,後面修改為'gbk'才成功

res.text儲存著解碼後的字串

>>> res.text
    window.use_fp = "1" == "1"; // ÊÇ·ñ²É¼¯É豸ָÎÆ¡£

所以,如果開啟檔案get.html,使用預設編碼模式gbk,將res.text寫入時,gbk編譯器gbk codec會將res.text使用f.encoding(預設 cp936,即gbk)編碼為位元組流,然後輸出到檔案(檔案儲存的都是位元組流).
編碼encode與解碼decode是一個可逆過程,所以大多數情況下(除了擴充套件情況,例如gbkgb2312的擴充套件,所以兩者共同擁有的字元不影響), 以一種模式編碼字串所得到的位元組流只能由相同的編碼來解碼得到該字串.
因為gbk codec只能編碼 使用res.encoding=gbk解碼得到的res.text,所以在編碼 使用了res.encoding='ISO-8859-1'解碼得到的res.text會提示題目所示的錯誤.


所以剛剛的解決方案實際上是設定 res.encoding=gbk,使res.content解碼為gbk編碼的res.text,這樣gbk codec在對res.text編碼時,兩個模式都是gbk,就不會出錯了.
另外一個可行解決方案是:以二進位制開啟檔案,gbk codec直接儲存由gbk編碼的位元組流.

res = requests.get('http://weibo.com')
with open(r'd:\get.html', 'wb') as f:
    f.write(res.content)

檔案以gbk編碼模式開啟,可以寫入使用gb2312編碼的位元組流res.content