Python reqeusts在作为代理爬虫节点抓取不同字符集网站时遇到的一些问题总结. 简单说就是中文乱码的问题. 如果单纯的抓取微博,微信,电商,那么字符集charset很容易就确认,你甚至可以单方面把encoding给固定住。 但作为舆情数据来说,他每天要抓取几十万个不同网站的敏感数据,所以这就需要我们更好确认字符集编码,避免中文的乱码情况.
该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。
我们首先看这个例子. 你会发现一些有意思的事情.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #blog: xiaorui.cc
In [9]: r = requests.get('http://cn.python-requests.org/en/latest/')
In [10]: r.encoding Out[10]: 'ISO-8859-1'
In [11]: type(r.text) Out[11]: unicode
In [12]: type(r.content) Out[12]: str
In [13]: r.apparent_encoding Out[13]: 'utf-8'
In [14]: chardet.detect(r.content) Out[14]: {'confidence': 0.99, 'encoding': 'utf-8'} |
第一个问题是,为什么会有ISO-8859-1这样的字符集编码?
iso-8859是什么? 他又被叫做Latin-1或“西欧语言” . 对于我来说,这属于requests的一个bug,在requests库的github里可以看到不只是中国人提交了这个issue. 但官方的回复说是按照http rfc设计的。
下面通过查看requests源代码,看这问题是如何造成的 !
requests会从服务器返回的响应头的 Content-Type 去获取字符集编码,如果content-type有charset字段那么requests才能正确识别编码,否则就使用默认的 ISO-8859-1. 一般那些不规范的页面往往有这样的问题.
1 2 | In [52]: r.headers Out[52]: {'content-length': '16907', 'via': 'BJ-H-NX-116(EXPIRED), http/1.1 BJ-UNI-1-JCS-116 ( [cHs f ])', 'ser': '3.81', 'content-encoding': 'gzip', 'age': '23', 'expires': 'Fri, 19 Feb 2016 07:36:25 GMT', 'vary': 'Accept-Encoding', 'server': 'JDWS', 'last-modified': 'Fri, 19 Feb 2016 07:35:25 GMT', 'connection': 'keep-alive', 'cache-control': 'max-age=60', 'date': 'Fri, 19 Feb 2016 07:35:31 GMT', 'content-type': 'text/html;'} |
文件: requests.utils.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #blog: xiaorui.cc def get_encoding_from_headers(headers): """通过headers头部的dict中获取编码格式"""
content_type = headers.get('content-type')
if not content_type: return None
content_type, params = cgi.parse_header(content_type)
if 'charset' in params: return params['charset'].strip("'\"")
if 'text' in content_type: return 'ISO-8859-1' |
第二个问题, 那么如何获取正确的编码?
requests的返回结果对象里有个apparent_encoding函数, apparent_encoding通过调用chardet.detect()来识别文本编码. 但是需要注意的是,这有些消耗计算资源.
至于为毛,可以看看chardet的源码实现.
1 2 3 4 5 | #blog: xiaorui.cc @property def apparent_encoding(self): """使用chardet来计算编码""" return chardet.detect(self.content)['encoding'] |
第三个问题,requests的text() 跟 content() 有什么区别?
requests在获取网络资源后,我们可以通过两种模式查看内容。 一个是r.text,另一个是r.content,那他们之间有什么区别呢?
分析requests的源代码发现,r.text返回的是处理过的Unicode型的数据,而使用r.content返回的是bytes型的原始数据。也就是说,r.content相对于r.text来说节省了计算资源,r.content是把内容bytes返回. 而r.text是decode成Unicode. 如果headers没有charset字符集的化,text()会调用chardet来计算字符集,这又是消耗cpu的事情.
通过看requests代码来分析text() content()的区别.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 文件: requests.models.py @property def apparent_encoding(self): """The apparent encoding, provided by the chardet library""" return chardet.detect(self.content)['encoding']
@property def content(self): """Content of the response, in bytes."""
if self._content is False: # Read the contents. try: if self._content_consumed: raise RuntimeError( 'The content for this response was already consumed')
if self.status_code == 0: self._content = None else: self._content = bytes().join(self.iter_content(CONTENT_CHUNK_SIZE)) or bytes()
except AttributeError: self._content = None
self._content_consumed = True # don't need to release the connection; that's been handled by urllib3 # since we exhausted the data. return self._content
@property def text(self): """Content of the response, in unicode. If Response.encoding is None, encoding will be guessed using ``chardet``. The encoding of the response content is determined based solely on HTTP headers, following RFC 2616 to the letter. If you can take advantage of non-HTTP knowledge to make a better guess at the encoding, you should set ``r.encoding`` appropriately before accessing this property. """
# Try charset from content-type content = None encoding = self.encoding
if not self.content: return str('')
# 当为空的时候会使用chardet来猜测编码. if self.encoding is None: encoding = self.apparent_encoding
# Decode unicode from given encoding. try: content = str(self.content, encoding, errors='replace') except (LookupError, TypeError): # A LookupError is raised if the encoding was not found which could # indicate a misspelling or similar mistake. # # A TypeError can be raised if encoding is None # # So we try blindly encoding. content = str(self.content, errors='replace') |
对于requests中文乱码解决方法有这么几种.
方法一:
由于content是HTTP相应的原始字节串,可以根据headers头部的charset把content decode为unicode,前提别是ISO-8859-1编码.
1 2 3 4 5 | In [96]: r.encoding Out[96]: 'gbk'
In [98]: print r.content.decode(r.encoding)[200:300] ="keywords" content="Python数据分析与挖掘实战,,机械工业出版社,9787111521235,,在线购买,折扣,打折"/> |
另外有一种特别粗暴方式,就是直接根据chardet的结果来encode成utf-8格式.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #http://xiaorui.cc
In [22]: r = requests.get('http://item.jd.com/1012551875.html')
In [23]: print r.content KeyboardInterrupt
In [23]: r.apparent_encoding Out[23]: 'GB2312'
In [24]: r.encoding Out[24]: 'gbk'
In [25]: r.content.decode(r.encoding).encode('utf-8') --------------------------------------------------------------------------- UnicodeDecodeError Traceback (most recent call last) <ipython-input-25-918324cdc053> in <module>() ----> 1 r.content.decode(r.apparent_encoding).encode('utf-8')
UnicodeDecodeError: 'gb2312' codec can't decode bytes in position 49882-49883: illegal multibyte sequence
In [27]: r.content.decode(r.apparent_encoding,'replace').encode('utf-8') |
如果在确定使用text,并已经得知该站的字符集编码时,可以使用 r.encoding = ‘xxx’ 模式, 当你指定编码后,requests在text时会根据你设定的字符集编码进行转换.
1 2 3 4 5 6 | >>> import requests >>> r = requests.get('https://up.xiaorui.cc') >>> r.text >>> r.encoding 'gbk' >>> r.encoding = 'utf-8' |
方法二:
根据我抓几十万的网站的经验,大多数网站还是很规范的,如果headers头部没有charset,那么就从html的meta中抽取.
1 2 3 4 5 6 7 | In [78]: s Out[78]: ' <meta http-equiv="Content-Type" content="text/html; charset=gbk"'
In [79]: b = re.compile("<meta.*content=.*charset=(?P<charset>[^;\s]+)", flags=re.I)
In [80]: b.search(s).group(1) Out[80]: 'gbk"' |
python requests的utils.py里已经有个完善的从html中获取meta charset的函数. 说白了还是一对的正则表达式.
1 2 | In [32]: requests.utils.get_encodings_from_content(r.content) Out[32]: ['gbk'] |
文件: utils.py
1 2 3 4 5 6 7 8 | def get_encodings_from_content(content): charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I) pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I) xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]')
return (charset_re.findall(content) + pragma_re.findall(content) + xml_re.findall(content)) |
最后,针对requests中文乱码的问题总结:
统一编码,要不都成utf-8, 要不就用unicode做中间码 !
国内的站点一般是utf-8、gbk、gb2312 , 当requests的encoding是这些字符集编码后,是可以直接decode成unicode.
但当你判断出encoding是 ISO-8859-1 时,可以结合re正则和chardet判断出他的真实编码. 可以把这逻辑封装补丁引入进来.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import requests def monkey_patch(): prop = requests.models.Response.content def content(self): _content = prop.fget(self) if self.encoding == 'ISO-8859-1': encodings = requests.utils.get_encodings_from_content(_content) if encodings: self.encoding = encodings[0] else: self.encoding = self.apparent_encoding _content = _content.decode(self.encoding, 'replace').encode('utf8', 'replace') self._content = _content return _content requests.models.Response.content = property(content) monkey_patch() |
Python3.x解决了这编码问题,如果你还是python2.6 2.7,那么还需要用上面的方法解决中文乱码的问题.
END.