给 Python requests 模块打 patch自动解决编码问题

这篇文章讲的不是 Python 的编码问题,讲的是如何给Python 的一个requests 模块打 patch 使之能更自动化地处理网页编码。

首先

如果无英文阅读障碍请直接移步https://github.com/requests/requests/issues/1604https://github.com/requests/requests/issues/2086查看讨论。
想直接要代码的请直接点击上面的 issue或者https://github.com/fiht/reqeusts_patch_example 进行获取。

背景

程序员就是为了解决问题而存在的,这篇文章要解决的问题是在使用Python requests 进行广泛网页抓取(类似Google一样的对大规模的网站进行抓取)时候遇到的网页编码的问题。requests 是一个很方便的 http client,被广泛的应用在爬虫抓取中。requests 模块十分方便,但是存在一个问题是requests 在某些情况下无法正确判断编码。

如我们学校的网站 http://www.sdu.edu.cn,使用requests 模块抓取会无法正确判断编码格式而导致乱码。

In [8]: req = requests.get("http://www.sdu.edu.cn")

In [9]: print req.text[:1000]

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="description" content="ɽ¶«´óѧ,ɽ´ó,ɽ¶«´óѧ¹Ù·½ÍøÕ¾,ɽ´ó¹ÙÍø,SDU" />
<meta name="keywords" content="ɽ¶«´óѧ,ɽ´ó,ɽ¶«´óѧ¹Ù·½ÍøÕ¾,ɽ´ó¹ÙÍø,SDU" />
<meta name="Copyright" content="Copyright (c) 2010 www.sdu.edu.cn All Rights Reserved." />
<title>»¶Ó­·ÃÎÊɽ¶«´óѧÖ÷Ò³</title>
<link href="../2010/images/style2.css" rel="stylesheet" type="text/css" />
<link href="../2010/images/ad2.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../2010/images/ad.js" ></script>
<link href="imagechangenews.css" type="text/css" rel="stylesheet">
<script language="javascript" src="imagechangenews.js"></script>


<style type="text/css">
* {margin: 0;padding: 0;}
h1,h2,h3,h4,h5,h6,ul,li,dl,dt,dd,form,img,p{margi

在抓取单个站点时,我们可以手动的调整编码,比如我根据 HTML 知道网站的编码格式是 gb2312的,我们可以通过如下设置来纠正内容的编码:

In [10]: req.encoding = "gb2312"

In [11]: print req.text[:1000]

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="description" content="山东大学,山大,山东大学官方网站,山大官网,SDU" />
<meta name="keywords" content="山东大学,山大,山东大学官方网站,山大官网,SDU" />
<meta name="Copyright" content="Copyright (c) 2010 www.sdu.edu.cn All Rights Reserved." />
<title>欢迎访问山东大学主页</title>
<link href="../2010/images/style2.css" rel="stylesheet" type="text/css" />
<link href="../2010/images/ad2.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../2010/images/ad.js" ></script>
<link href="imagechangenews.css" type="text/css" rel="stylesheet">
<script language="javascript" src="imagechangenews.js"></script>


<style type="text/css">
* {margin: 0;padding: 0;}
h1,h2,h3,h4,h5,h6,ul,li,dl,dt,dd,form,img,p{margin:0;padding:0;border:none;list-style-type:none

我们可以看到在手动设置编码之后乱码的问题已经解决了,但是对于大规模抓取,比如讲我有十万个网站需要使用 requests 抓取回来,很明显使用上面这种手动设置编码已经不合适了,那么我们应该怎么办呢?

下策

可以使用如下语法来对文本编码进行自动判断。

In [12]: req = requests.get("http://www.sdu.edu.cn")

In [13]: req.encoding = req.apparent_encoding

In [14]: print req.text[:1000]

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="description" content="山东大学,山大,山东大学官方网站,山大官网,SDU" />
<meta name="keywords" content="山东大学,山大,山东大学官方网站,山大官网,SDU" />
<meta name="Copyright" content="Copyright (c) 2010 www.sdu.edu.cn All Rights Reserved." />
<title>欢迎访问山东大学主页</title>
<link href="../2010/images/style2.css" rel="stylesheet" type="text/css" />
<link href="../2010/images/ad2.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../2010/images/ad.js" ></script>
<link href="imagechangenews.css" type="text/css" rel="stylesheet">
<script language="javascript" src="imagechangenews.js"></script>


<style type="text/css">
* {margin: 0;padding: 0;}
h1,h2,h3,h4,h5,h6,ul,li,dl,dt,dd,form,img,p{margin:0;padding:0;border:none;list-style-type:none

这种方法能够处理大部分的网站编码问题,因为在执行 req.encoding = req.apparent_encoding 的时候内部是使用 chardet 这一第三方库来实现的,但是效率较低,执行成本较高。在我的机器上测试是否加上 req.encoding = req.apparent_encoding 程序执行速度会有数量级级别的差异。感兴趣的朋友可以自己测试一下。

上策

那么我们浏览器是怎么识别编码的呢?服务器有哪些方法能够将他的编码方式传送过来呢?
就我所知,浏览器识别编码主要靠以下两个方法

Response Header 中的Content-Type 字段

➜  ~ curl https://www.google.com --head
HTTP/2 302
location: http://www.google.com.hk/url?sa=p&hl=zh-CN&pref=hkredirect&pval=yes&q=http://www.google.com.hk/
cache-control: private
content-type: text/html; charset=UTF-8

我们可以看到 Response Header 中的 Content-Type 字段中含有 charset 字段,我们可以直接使用这个字段来解码内容。
另:在浏览器中,会优先使用 Header 中的编码信息。

网页 HTML 中的字段

有些服务器不在 Header 中发送信息,如 http://www.sdu.edu.cn 就不包含编码信息了

➜  ~ curl http://www.sdu.edu.cn --head
HTTP/1.1 200 OK
Date: Sun, 07 Jan 2018 03:24:34 GMT
Server: ********************
Last-Modified: Thu, 28 Dec 2017 01:42:44 GMT
ETag: "f1dad-6914-5615ca1dc2d00"
Accept-Ranges: bytes
Content-Length: 26900
Content-Type: text/html

但是我们的浏览器为什么能不乱码地显示网页呢?这是因为服务器在 HTML 内容中加入了一个如下的字段:

<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />

浏览器看到这个字段之后就可以知道对方发送过来的内容采取的是 gb2312编码,于是使用 gb2312的规则来解码,就不会有错了。


上面的这两个方法在 requests 中都已经实现了,在 util.py 文件中,我们可以看到这样两个方法。

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


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

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

    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 判断编码的方式是先使用 get_encoding_from_headers 获取编码,如果获取不到,则使用 get_encodings_from_content 方法获取编码。
那么我们在访问 www.sdu.edu.cn 的时候为什么会编码失效呢?我们看到 get_encoding_from_headers 中有一判断条件

    if 'text' in content_type:
        return 'ISO-8859-1'

若返回头里面存在 Content-Type 而且 charset 没有在 Content-Type 中,则会默认选择 ‘ISO-8859-1’ 作为默认编码格式。为什么这样子呢,因为是 RFC 规范规定的。requests 选择遵循此规范。
那么我们如何手动移除这一段代码呢?
手动修改requests的 util.py 文件未免也太不优雅了,我们可以用pythonic 的方法来实现。
新建一个 Patch.py 文件

import chardet
import requests
def m_patch():
    """
    解决 requests 对于编码处理错误的问题。
    详情见: https://github.com/requests/requests/issues/1604
    """
    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 = chardet.detect(_content)['encoding']

            if self.encoding:
                try:
                    _content = _content.decode(self.encoding, 'replace').encode('utf8', 'replace')
                except:  # 服务器端的编码不正确的情况
                    self.encoding = self.apparent_encoding
                    _content = _content.decode(self.encoding, 'replace').encode('utf8', 'replace')
                self._content = _content
                self.encoding = 'utf8'

        return _content

    requests.models.Response.content = property(content)

然后在使用 requests 之前先引入这个 m_patch 方法,然后执行这个方法。相关代码见https://github.com/fiht/reqeusts_patch_example

效果


BEFORE PATCH:
****************************************************************************************************














欢迎访问山东大学主页

*****************************************************************************************************
点赞
  1. wife说道:

    很好的一篇~老公加油~ :rolleyes:

  2. alwaysR9说道:

    hi, 看起来这是为库函数打补丁的好方法。
    原理是,自己创建的condent方法覆盖了requests.models.Response.content方法吗?

    1. admin说道:

      嗯,是的。在访问 response.content 时会调用到我们的 patch过的 content 方法。

      1. alwaysR9说道:

        :biggrin: 谢谢,刚好我最近遇到redis-cluster库存在不成熟的地方,我试试这种方法

发表评论

电子邮件地址不会被公开。 必填项已用*标注