'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为源网页标签中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

你可能感兴趣的:(Python)