解决Requests抓取中文网页乱码问题

Requests是用Python语言编写,使用的是urllib3,拥有了它的所有特性,Requests 支持 HTTP 连接保持和连接池,支持使用 cookie 保持会话,支持文件上传,支持自动确定响应内容的编码,支持国际化的 URL 和 POST 数据自动编码。现代、国际化、人性化。正是由于它的强大,所以现在被广泛的用于爬虫等方面,但是在对中文网页进行抓取的时候,或多或少会遇到一些编码问题,本文就这些问题讨论一下。

简单demo

1
2
3
4
5
import requests
r = requests.get("http://www.scusec.org")
print r.encoding # Requests推测使用的编码格式
print r.text # 字符串方式的响应体,会自动根据响应头部的字符编码进行解码
print r.content # 字节方式的响应体,会自动为你解码 gzip 和 deflate 压缩

请求发出后,Requests默认会基于 HTTP 头部对响应的编码作出有根据的推测。当你使用 r.text 获取返回的内容时,Requests会使用其推测的编码格式进行解码。而当使用 r.content,则会获取原始的字节方式的响应体。

Requests默认识别编码

C:\Python27\Lib\site-packages\requests\utils.py中,发现默认encoding的获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Try charset from content-type
encoding = get_encoding_from_headers(r.headers)

def get_encoding_from_headers(headers):
"""Returns encodings from given HTTP Header Dict.

:param headers: dictionary to extract encoding from.
"""


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'

在函数get_encoding_from_headers,如果返回头content-type发现”charset”,则返回相应的编码格式,否则返回”ISO-8859-1”。利用这种方式判断编码方式,未免有点过于简单和草率。

Requests其他识别编码方式

在知道Requests的默认识别方式不靠谱后,通常我们采用的方式有

  1. 提取返回体中 META http-equiv=Content-Type content="text/html; charset=gb2312中的编码格式
  2. 利用chardet库识别返回体的编码格式

其实Requests中,已经支持这两种识别方式。见utils.py中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_encodings_from_content(content):
"""Returns encodings from given content string.

:param content: bytestring to extract encodings from.
"""

warnings.warn((
'In requests 3.0, get_encodings_from_content will be removed. For '
'more information, please see the discussion on issue #2266. (This'
' warning should only appear once.)'),
DeprecationWarning)

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))

再见models.py,定义了apparent_encoding属性

1
2
3
4
@property
def apparent_encoding(self):
"""The apparent encoding, provided by the chardet library"""
return chardet.detect(self.content)['encoding']

三种识别方式比较

针对这三种方式,有的需要获取返回体,所以这三种识别编码的速率及需要的资源不同。先看看下面的程序:

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
#! usr/bin/env python
#! coding: utf-8

import requests
import time
import re
r = requests.get("http://www.scusec.org")

def charset_type():
char_type = requests.utils.get_encoding_from_headers(r.headers)
return char_type

def charset_content():
charset_content = requests.utils.get_encodings_from_content(r.content)
return charset_content[0]

def charset_det():
charset_det = r.apparent_encoding
return charset_det

def chose_fun(n):
if n == 1:
return charset_type
elif n == 2:
return charset_content
else:
return charset_det

if __name__ == '__main__':
for i in range(1,4):
funs = chose_fun(i)
start_time = time.time()
chars = funs()
end_time = time.time()
times = end_time - start_time
print chars+" "+str(times)

返回结果

可见在速率上charset_type > charset_content > charset_det,并且charset_type在空间使用上也是最优的。

综合的解决方法

综合上面的几种解决方式,可以定义一个解决函数:

1
2
3
4
5
6
7
8
9
def charsets(res):
_charset = requests.utils.get_encoding_from_headers(res.headers)
if _charset == 'ISO-8859-1':
__charset = requests.utils.get_encodings_from_content(res.content)[0]
if __charset:
_charset = __charset
else:
_charset = res.apparent_encoding
return _charset

那么处理中文网页:

  1. 利用charsets函数获取页面编码方式,再配合r.content进行相关的解码和编码
  2. 利用charsets函数获取页面编码方式,设定r.encoding的值,再获取r.text