Python HTTP 库:requests 快速入门

2013 年我接触 Python 的时候,就听闻 Python 的网络编程能力十分强大。因此,在熟悉 Python 的基本语法之后,我就和几个小伙伴一起合作,试着用 Python 的 urlliburllib2 库构建了一个百度贴吧 Python 客户端。

然而,使用的过程中,我发现两个标准库的语法并不自然,甚至可以说十分反人类——用着很难受。又有,我平时使用 Python 甚少涉及到网络编程的内容。因此,Python 的网络编程就被我放下了,直到我认识了 requests 库。

初识 requests

requests 库的宣言是

HTTP for Humans (给人用的 HTTP 库)

我们首先来验证一下。

在网络编程中,最最基本的任务包含:

  • 发送请求
  • 登录
  • 获取数据
  • 解析数据
  • 反序列化打印内容

我们以 GitHub 为例,先看一下使用 urllib2 要怎么做。为了把事情弄简单点,我们假设实现已经知道,GET 请求 https://api.github.com/ 返回的内容是个 JSON 格式的数据(实际上通过 content-type 也能判断)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import urllib2
import json

gh_url = 'https://api.github.com'
cs_user = 'user'
cs_psw = 'password'

req = urllib2.Request(gh_url)

password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, gh_url, cs_user, cs_psw)

auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)

urllib2.install_opener(opener)

handler = urllib2.urlopen(req)

if handler.getcode() == requests.codes.ok:
text = handler.read()
d_text = json.loads(text)
for k, v in d_text.items():
print k, v

如果运行正确,那么代码应该返回:

1
2
3
4
5
6
7
issues_url https://api.github.com/issues
current_user_repositories_url https://api.github.com/user/repos{?type,page,per_page,sort}
rate_limit_url https://api.github.com/rate_limit
repository_url https://api.github.com/repos/{owner}/{repo}
...
user_repositories_url https://api.github.com/users/{user}/repos{?type,page,per_page,sort}
team_url https://api.github.com/teams

同样的效果,用 requests 库则有如下代码:

1
2
3
4
5
6
7
8
9
10
11
import requests

cs_url = 'https://api.github.com'
cs_user = 'user'
cs_psw = 'password'

r = requests.get(cs_url, auth=(cs_user, cs_psw))

if r.status_code == requests.codes.ok
for k, v in r.json().items():
print k, v

溢美之词就不用说了,读到这里的你心里肯定只有一声「卧槽,这才是 Python 该有的样子」。那么,接下来我们看看 requests 都有哪些黑魔法。

安装

最推荐的方式,是直接安装推荐过的 Anaconda

如果你不想安装 Anaconda,那么建议你使用 pip 安装;只需在命令行下执行:

1
pip install requests

基本用法

requests 的基本用法,呃,真是不能再基本了。最基本的操作,就是以某种 HTTP 方法向远端服务器发送一个请求而已;而 requests 库就是这么做的。

1
2
3
4
5
6
7
8
9
10
import requests

cs_url = 'http://httpbin.org'

r = requests.get("%s/%s" % (cs_url, 'get'))
r = requests.post("%s/%s" % (cs_url, 'post'))
r = requests.put("%s/%s" % (cs_url, 'put'))
r = requests.delete("%s/%s" % (cs_url, 'delete'))
r = requests.patch("%s/%s" % (cs_url, 'patch'))
r = requests.options("%s/%s" % (cs_url, 'get'))

从语法上看,requests 库设计的非常自然。所谓 requests.get,就是以 GET 方式发送一个 REQUEST,得到一个 Response 类的结果,保存为 r

你可以在 r 中取得所有你想得到的和 HTTP 有关的信息。下面,我们以 GET 方法为例,依次介绍。

URL 传参 / 获取请求的 URL

如果你经常上网(废话,看到这里的都上过网吧……),一定见过类似下面的链接:

https://encrypted.google.com/search?q=hello

即:

1
<协议>://<域名>/<接口>?<键1>=<值1>&<键2>=<值2>

requests 库提供的 HTTP 方法,都提供了名为 params 的参数。这个参数可以接受一个 Python 字典,并自动格式化为上述格式。

1
2
3
4
5
6
7
import requests

cs_url = 'http://www.so.com/s'
param = {'ie':'utf-8', 'q':'query'}

r = requests.get (cs_url, params = param)
print r.url

运行将得到:

1
http://www.so.com/s?q=query&ie=utf-8

HTTP 状态码 / 重定向跳转

requests 库定义的 Response 类可以方便地获取请求的 HTTP 状态码和重定向状态。

360 公司的搜索引擎,原名「好搜」,现在改为「360 搜索」;域名也从 www.haosou.com 改成了 www.so.com。如果你在地址栏输入 www.haosou.com,那么会经由 302 跳转到 www.so.com。我们借此来演示。

1
2
3
4
5
6
7
8
9
10
import requests

cs_url = 'http://www.so.com/s'
param = {'ie':'utf-8', 'q':'query'}
r = requests.get (cs_url, params = param)
print r.url, r.status_code

cs_url = 'http://www.haosou.com/s'
r = requests.get (cs_url, params = param)
print r.url, r.status_code, r.history

结果是:

1
2
http://www.so.com/s?q=query&ie=utf-8 200
http://www.so.com/s?q=query&ie=utf-8 200 [<Response [302]>]

我们发现,requests 默认自动地处理了 302 跳转。在经过跳转的请求中,返回的 URL 和状态码都是跳转之后的信息;唯独在 history 中,用 Python 列表记录了跳转情况。

大多数情况下,自动处理是挺好的。不过,有时候我们也想单步追踪页面跳转情况。此时,可以给请求加上 allow_redirects = False 参数。

1
2
3
4
5
6
7
8
9
10
import requests

cs_url = 'http://www.so.com/s'
param = {'ie':'utf-8', 'q':'query'}
r = requests.get (cs_url, params = param)
print r.url, r.status_code

cs_url = 'http://www.haosou.com/s'
r = requests.get (cs_url, params = param, allow_redirects = False)
print r.url, r.status_code, r.history

输出结果:

1
2
http://www.so.com/s?q=query&ie=utf-8 200
http://www.haosou.com/s?q=query&ie=utf-8 302 []

不允许 requests 自动处理跳转后,返回的 URL 和状态码都符合预期了。

超时设置

requests 的超时设置以为单位。例如,对请求加参数 timeout = 5 即可设置超时为 5 秒。

1
2
3
4
5
# a very short timeout is set intentionally
import requests

cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url, timeout = 0.000001)

返回报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Traceback (most recent call last):
File "D:\test\py\test.py", line 6, in <module>
r = requests.get (cs_url, timeout = 0.000001)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\api.py", line 69, in get
return request('get', url, params=params, **kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\api.py", line 50, in request
response = session.request(method=method, url=url, **kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\sessions.py", line 465, in request
resp = self.send(prep, **send_kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\sessions.py", line 573, in send
r = adapter.send(request, **kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\adapters.py", line 419, in send
raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPConnectionPool(host='www.zhihu.com', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<requests.packages.urllib3.connection.HTTPConnection object at 0x0000000002AFABE0>, 'Connection to www.zhihu.com timed out. (connect timeout=1e-06)'))

请求头

我们先来看一下 requests 发出的 HTTP 报文默认的请求头是什么样子的。

1
2
3
4
5
import requests

cs_url = 'http://httpbin.org/get'
r = requests.get (cs_url)
print r.content

返回结果:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.7.0 CPython/2.7.10 Windows/7"
}
,

"origin": "xx.xx.xx.xx",
"url": "http://httpbin.org/get"
}

通常我们比较关注其中的 User-AgentAccept-Encoding。如果我们要修改 HTTP 头中的这两项内容,只需要将一个合适的字典参数传给 headers 即可。

1
2
3
4
5
6
import requests

my_headers = {'User-Agent' : 'From Liam Huang', 'Accept-Encoding' : 'gzip'}
cs_url = 'http://httpbin.org/get'
r = requests.get (cs_url, headers = my_headers)
print r.content

返回:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "From Liam Huang"
}
,

"origin": "xx.xx.xx.xx",
"url": "http://httpbin.org/get"
}

可以看到,UA 和 AE 都已经被修改了。

响应头

作为 HTTP 请求的响应,返回的内容中也有 HTTP 头。它是一个反序列化为 Python 字典的数据结构,可以通过 Response.headers 来查看。

1
2
3
4
5
import requests

cs_url = 'http://httpbin.org/get'
r = requests.get (cs_url)
print r.headers

返回:

1
2
3
4
5
6
7
8
9
{
"content-length": "263",
"server": "nginx",
"connection": "keep-alive",
"access-control-allow-credentials": "true",
"date": "Fri, 26 Feb 2016 10:26:17 GMT",
"access-control-allow-origin": "*",
"content-type": "application/json"
}

响应内容

字节模式 / 自动解包

长期以来,互联网都存在带宽有限的情况。因此,网络上传输的数据,很多情况下都是经过压缩的。经由 requests 发送的请求,当收到的响应内容经过 gzip 或 deflate 压缩时,requests 会自动为我们解包。我们可以用 Response.content 来获得以字节形式返回的相应内容。

1
2
3
4
5
6
7
import requests

cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url)

if r.status_code == requests.codes.ok:
print r.content

这相当于 urllib2.urlopen(url).read()

如果相应内容不是文本,而是二进制数据(比如图片),那么上述打印结果可能会糊你一脸。这里以图片为例,示例一下该怎么办。

1
2
3
4
5
6
7
8
9
import requests
from PIL import Image
from StringIO import StringIO

cs_url = 'http://liam0205.me/uploads/avatar/avatar-2.jpg'
r = requests.get (cs_url)

if r.status_code == requests.codes.ok:
Image.open(StringIO(r.content)).show()

运行无误的话,能看到我和我爱人的照片。

文本模式 / 编码

如果响应返回是文本,那么你可以用 Response.text 获得 Unicode 编码的响应返回内容。

1
2
3
4
5
6
7
import requests

cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url)

if r.status_code == requests.codes.ok:
print r.text

要获得 Unicode 编码的结果,意味着 requests 会为我们做解码工作。那么 requests 是按照何种编码去对返回结果解码的呢?

requests 会读取 HTTP header 中关于字符集的内容。如果获取成功,则会依此进行解码;若不然,则会根据响应内容对编码进行猜测。具体来说,我们可以用 Response.encoding 来查看/修改使用的编码。

1
2
3
4
5
6
7
import requests

cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url)

if r.status_code == requests.codes.ok:
print r.encoding

反序列化 JSON 数据

开篇给出的第一个 requests 示例中,特别吸引人的一点就是 requests 无需任何其他库,就能解析序列化为 JSON 格式的数据。

我们以 IP 查询 Google 公共 DNS 为例:

1
2
3
4
5
6
7
8
import requests

cs_url = 'http://ip.taobao.com/service/getIpInfo.php'
my_param = {'ip':'8.8.8.8'}

r = requests.get(cs_url, params = my_param)

print r.json()['data']['country'].encode('utf-8')

结果将输出:

1
美国

模拟登录 GitHub 看看

HTTP 协议是无状态的。因此,若不借助其他手段,远程的服务器就无法知道以前和客户端做了哪些通信。Cookie 就是「其他手段」之一。

Cookie 一个典型的应用场景,就是用于记录用户在网站上的登录状态。

  1. 用户登录成功后,服务器下发一个(通常是加密了的)Cookie 文件。
  2. 客户端(通常是网页浏览器)将收到的 Cookie 文件保存起来。
  3. 下次客户端与服务器连接时,将 Cookie 文件发送给服务器,由服务器校验其含义,恢复登录状态(从而避免再次登录)。

Cookie? 你说的是小甜点吧!

别忘了,requests 是给人类设计的 Python 库。想想使用浏览器浏览网页的时候,我们没有手工去保存、重新发送 Cookie 对吗?浏览器都为我们自动完成了。

requests 中,也是这样

当浏览器作为客户端与远端服务器连接时,远端服务器会根据需要,产生一个 SessionID,并附在 Cookie 中发给浏览器。接下来的时间里,只要 Cookie 不过期,浏览器与远端服务器的连接,都会使用这个 SessionID;而浏览器会自动与服务器协作,维护相应的 Cookie。

requests 中,也是这样。我们可以创建一个 requests.Session,尔后在该 Session 中与远端服务器通信,其中产生的 Cookie,requests 会自动为我们维护好

POST 表单

POST 方法可以将一组用户数据,以表单的形式发送到远端服务器。远端服务器接受后,依照表单内容做相应的动作。

调用 requests 的 POST 方法时,可以用 data 参数接收一个 Python 字典结构。requests 会自动将 Python 字典序列化为实际的表单内容。例如:

1
2
3
4
5
6
7
8
9
10
import requests

cs_url = 'http://httpbin.org/post'
my_data = {
'key1' : 'value1',
'key2' : 'value2'
}

r = requests.post (cs_url, data = my_data)
print r.content

返回:

1
2
3
4
5
6
7
8
{
...
"form": {
"key1": "value1",
"key2": "value2"
},

...
}

实际模拟登录 GitHub 试试看

模拟登录的第一步,首先是要搞清楚我们用浏览器登录时都发生了什么。

GitHub 登录页面是 https://github.com/login。我们首先清空浏览器 Cookie 记录,然后用 Chrome 打开登录页面。

填入 Username 和 Password 之后,我们打开 Tamper Chrome 和 Chrome 的元素审查工具(找到 Network 标签页),之后点登录按钮。

在 Tamper Chrome 中,我们发现:虽然登录页面是 https://github.com/login,但实际接收表单的是 https://github.com/session。若登录成功,则跳转到 https://github.com/ 首页,返回状态码 200

Tamper Chrome 截图

而在 Chrome 的审查元素窗口中,我们可以看到提交给 session 接口的表单信息。内里包含

  • commit
  • utf8
  • authenticity_token
  • login
  • password

Chrome 审查元素截图

其中,commitutf8 两项是定值;loginpassword 分别是用户名和密码,这很好理解。唯独 authenticity_token 是一长串无规律的字符,我们不清楚它是什么。

POST 动作发生在与 session 接口交互之前,因此可能的信息来源只有 login 接口。我们打开 login 页面的源码,试着搜索 authenticity_token 就不难发现有如下内容:

1
<input name="authenticity_token" type="hidden" value="......" />

原来,所谓的 authenticity_token 是明白卸载 HTML 页面里的,只不过用 hidden 模式隐藏起来了。为此,我们只需要使用 Python 的正则库解析一下,就好了。

这样一来,事情就变得简单起来,编码吧!

模拟登录 GitHub
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
import requests
import re

cs_url = 'https://github.com/login'
cs_user = 'user'
cs_psw = 'psw'
my_headers = {
'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36',
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding' : 'gzip',
'Accept-Language' : 'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4'
}
sss = requests.Session()
r = sss.get(cs_url, headers = my_headers)
reg = r'<input name="authenticity_token" type="hidden" value="(.*)" />'
pattern = re.compile(reg)
result = pattern.findall(r.content)
token = result[0]
my_data = {
'commit' : 'Sign in',
'utf8' : '%E2%9C%93',
'authenticity_token' : token,
'login' : cs_user,
'password' : cs_psw
}
cs_url = 'https://github.com/session'
r = sss.post(cs_url, headers = my_headers, data = my_data)
print r.url, r.status_code, r.history

输出:

1
https://github.com/ 200 [<Response [302]>]

代码很好理解,其实只是完全地模拟了浏览器的行为。

首先,我们准备好了和 Chrome 一致的 HTTP 请求头部信息。具体来说,其中的 User-Agent 是比较重要的。而后,仿照浏览器与服务器的通信,我们创建了一个 requests.Session。接着,我们用 GET 方法打开登录页面,并用正则库解析到 authenticity_token。随后,将所需的数据,整备成一个 Python 字典备用。最后,我们用 POST 方法,将表单提交到 session 接口。

最终的结果也是符合预期的:经由 302 跳转,打开了(200)GitHub 首页。

热评文章