# Requests 库的使用 Python 的标准库 urllib 提供了大部分 HTTP 功能,但使用起来较繁琐。通常,我们会使用另外一个优秀的第三方库:[Requests](https://github.com/kennethreitz/requests),它的标语是:**Requests: HTTP for Humans**。 Requests 提供了很多功能特性,几乎涵盖了当今 Web 服务的需求,比如: - 浏览器式的 SSL 验证 - 身份认证 - Keep-Alive & 连接池 - 带持久 Cookie 的会话 - 流下载 - 文件分块上传 下面,我们将从以下几个方面介绍 Requests 库: - HTTP 请求 - HTTP 响应 - cookie - 会话对象 - 代理 - 身份认证 # HTTP 请求 我们知道,一个 HTTP 请求由三部分构成: - **请求行**:包含请求方法(比如 GET, POST)、请求地址和 HTTP 协议版本 - **请求头**:包含一系列的键值对 - **请求正文(可选)** 如图所示: ![](https://ooo.0o0.ooo/2016/12/05/58456e61d5d47.png) Requests 提供了几乎所有 HTTP 动词的功能:GET、OPTIONS、HEAD、POST、PUT、PATCH、DELETE,另外,它提供了 `headers` 参数让我们根据需求定制请求头。 使用 Requests 发送一个请求很方便,比如: ``` import requests r = requests.get("http://httpbin.org/get") r = requests.post("http://httpbin.org/post") r = requests.put("http://httpbin.org/put") r = requests.delete("http://httpbin.org/delete") r = requests.head("http://httpbin.org/get") r = requests.options("http://httpbin.org/get") ``` 下面,我们重点讲一下 GET 请求,POST 请求和定制请求头。 ## GET 请求 使用 Requests 发送 GET 请求非常简单,如下: ```python import requests r = requests.get("http://httpbin.org/get") ``` 在有些情况下,URL 会带参数,比如 `https://segmentfault.com/blogs?page=2`,这个 URL 有一个参数 page,值为 2。Requests 提供了 `params` 关键字参数,允许我们以一个字典来提供这些参数,比如: ```python import requests payload = {'page': '1', 'per_page': '10'} r = requests.get("http://httpbin.org/get", params=payload) ``` 通过打印该 URL,我们可以看到 URL 已被正确编码: ```python >>> print r.url http://httpbin.org/get?per_page=10&page=1 ``` 需要注意的是字典里值为 None 的键不会被添加到 URL 的查询字符串中。 ## POST 请求 使用 Requests 发送 POST 请求也很简单,如下: ```python import requests r = requests.post("http://httpbin.org/post") ``` 通常,我们在发送 POST 请求时还会附上数据,比如发送编码为表单形式的数据或编码为 JSON 形式的数据,这时,我们可以使用 Requests 提供的 `data` 参数。 - 发送编码为表单形式的数据 通过给 `data` 参数传递一个 `dict`,我们的数据字典在发出请求时会被自动编码为表单形式,比如: ```python import requests payload = {'page': 1, 'per_page': 10} r = requests.post("http://httpbin.org/post", data=payload) ``` 看看返回的内容(省略了部分数据): ```python >>> print r.text { ... "form": { "page": "1", "per_page": "10" }, ... } ``` - 发送编码为 JSON 形式的数据 如果给 `data` 参数传递一个 `string`,我们的数据会被直接发布出去,比如: ```python import json import requests payload = {'page': 1, 'per_page': 10} r = requests.post("http://httpbin.org/post", data=json.dumps(payload)) ``` 看看返回: ```python >>> print r.text { "args": {}, "data": "{\"per_page\": 10, \"page\": 1}", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Content-Length": "27", "Host": "httpbin.org", "User-Agent": "python-requests/2.9.1" }, "json": { "page": 1, "per_page": 10 }, "origin": "13.75.42.240", "url": "http://httpbin.org/post" } ``` 在上面,我们自行对 `dict` 进行了编码,这种方式等价于使用 `json` 参数,而给它传递 `dict`,如下: ```python import requests payload = {'page': 1, 'per_page': 10} r = requests.post("http://httpbin.org/post", json=payload) ``` 这种做法跟上面的做法是等价的,数据在发出时会被自动编码。 ## 请求头 有时,我们需要为请求添加 HTTP 头部,我们可以**通过传递一个 `dict` 给 `headers` 参数来实现**。比如: ```python import requests url = 'http://httpbin.org/post' payload = {'page': 1, 'per_page': 10} headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} r = requests.post("http://httpbin.org/post", json=payload, headers=headers) ``` 发送到服务器的请求的头部可以通过 `r.request.headers` 访问: ``` >>> print r.request.headers {'Content-Length': '27', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Connection': 'keep-alive', 'Content-Type': 'application/json'} ``` 服务器返回给我们的响应头部信息可以通过 `r.headers` 访问: ``` >>> print r.headers {'Content-Length': '462', 'Server': 'nginx', 'Connection': 'close', 'Access-Control-Allow-Credentials': 'true', 'Date': 'Mon, 05 Dec 2016 15:41:05 GMT', 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json'} ``` # HTTP 响应 HTTP 响应与 HTTP 请求相似,由三部分组成: - **状态行**:包含 HTTP 协议版本、状态码和状态描述,以空格分隔 - **响应头**:包含一系列的键值对 - **响应正文** 如图所示: ![](https://ooo.0o0.ooo/2016/12/05/58456e62d49d6.png) 当我们使用 `requests.*` 发送请求时,Requests 做了两件事: - 构建一个 Request 对象,该对象会根据请求方法或相关参数发起 HTTP 请求 - 一旦服务器返回响应,就会产生一个 Response 对象,该响应对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象 对于响应状态码,我们可以访问响应对象的 `status_code` 属性: ```python import requests r = requests.get("http://httpbin.org/get") print r.status_code # 输出 200 ``` 对于响应正文,我们可以通过多种方式读取,比如: - 普通响应,使用 `r.text` 获取 - JSON 响应,使用 `r.json()` 获取 - 二进制响应,使用 `r.content` 获取 - 原始响应,使用 `r.raw` 获取 ## 普通响应 我们可以使用 `r.text` 来读取 unicode 形式的响应,看看例子: ```python import requests r = requests.get("https://github.com/timeline.json") print r.text print r.encoding # 输出 {"message":"Hello there, wayfaring stranger. If you’re reading this then you probably didn’t see our blog post a couple of years back announcing that this API would go away: http://git.io/17AROg Fear not, you should be able to get what you need from the shiny new Events API instead.","documentation_url":"https://developer.github.com/v3/activity/events/#list-public-events"} utf-8 ``` Requests 会自动解码来自服务器的内容,大多数 unicode 字符集都能被正确解码。 ## JSON 响应 对于 JSON 响应的内容,我们可以使用 `json()` 方法把返回的数据解析成 Python 对象。 看看例子: ```python import requests r = requests.get("https://github.com/timeline.json") if r.status_code == 200: print r.headers.get('content-type') print r.json() # 输出 application/json; charset=utf-8 {u'documentation_url': u'https://developer.github.com/v3/activity/events/#list-public-events', u'message': u'Hello there, wayfaring stranger. If you\u2019re reading this then you probably didn\u2019t see our blog post a couple of years back announcing that this API would go away: http://git.io/17AROg Fear not, you should be able to get what you need from the shiny new Events API instead.'} ``` 如果 JSON 解码失败,`r.json()` 就会抛出异常,比如: ```python import requests r = requests.get("https://www.baidu.com") if r.status_code == 200: print r.headers.get('content-type') print r.json() # 输出 text/html --------------------------------------------------------------------------- ValueError Traceback (most recent call last) in () 1 if r.status_code == 200: 2 print r.headers.get('content-type') ----> 3 print r.json() 4 .... .... ValueError: No JSON object could be decoded ``` ## 二进制响应 我们也可以以字节的方式访问响应正文,访问 `content` 属性可以获取二进制数据,比如用返回的二进制数据创建一张图片: ```python import requests url = 'https://github.com/reactjs/redux/blob/master/logo/logo.png?raw=true' r = requests.get(url) image_data = r.content # 获取二进制数据 with open('/Users/Ethan/Downloads/redux.png', 'wb') as fout: fout.write(image_data) ``` ## 原始响应 在少数情况下,我们可能想获取来自服务器的原始套接字响应,这可以通过访问响应对象的 `raw` 属性来实现,但要确保在初始请求中设置了 `stream=True`,比如: ```python import requests url = 'https://github.com/reactjs/redux/blob/master/logo/logo.png?raw=true' r = requests.get(url, stream=True) print r.raw r.raw.read(10) # 输出 '\x89PNG\r\n\x1a\n\x00\x00' ``` # 重定向 默认情况下,除了 HEAD,Requests 会自动处理所有重定向。我们可以使用响应对象的 `history` 属性来追踪重定向,**Response.history** 是一个 Response 对象的列表,这个对象列表按照从最老到最近的请求进行排序。 比如,点击某些网站的链接,它会将页面重定向到其他网站: ```python >>> import requests >>> headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} >>> r = requests.get('https://toutiao.io/shares/789751/url', headers=headers) >>> r.status_code 200 >>> r.url # 发生了重定向,响应对象的 url,跟请求对象不一样 u'https://funhacks.net/2016/12/06/flask_react_news/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io' >>> r.history [, ] >>> r.history[0].text u'You are being redirected.' >>> r.history[1].text u'You are being redirected.' ``` 可以看到,我们访问网址 `https://toutiao.io/shares/789751/url` 被重定向到了下面的链接: ```python u'https://funhacks.net/2016/12/06/flask_react_news/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io' ``` 我们还看到 `r.history` 包含了两个 Response 对象列表,我们可以用它来追踪重定向。 如果请求方法是 GET、POST、PUT、OPTIONS、PATCH 或 DELETE,我们可以通过 `all_redirects` 参数禁止重定向: ```python >>> import requests >>> headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} >>> r = requests.get('https://toutiao.io/shares/789751/url', headers=headers, allow_redirects=False) >>> r.status_code 200 >>> r.url # 禁止重定向,响应对象的 url 跟请求对象一致 u'https://toutiao.io/shares/789751/url' >>> r.history [] >>> r.text u'You are being redirected.' ``` # Cookie - 如果某个响应包含一些 cookie,我们可以直接访问它们,比如: ```python >>> import requests >>> url = 'http://exmaple.com/some/cookie/setting/url' >>> r = requests.get(url) >>> r.cookies['some_key'] 'some_value' ``` - 发送 cookies 到服务器,可以使用 `cookies` 参数: ```python >>> import requests >>> url = 'http://httpbin.org/cookies' >>> cookies = dict(key1='value1') >>> r = requests.get(url, cookies=cookies) >>> r.text u'{\n "cookies": {\n "key1": "value1"\n }\n}\n' >>> print r.text { "cookies": { "key1": "value1" } } ``` # 会话对象 我们知道,HTTP 协议是无状态的,这意味着每个请求都是独立的,如果后续的处理需要用到前面的信息,则数据需要重传,为了解决这个问题,我们可以使用 Cookie 或 Session 来存储某些特定的信息,比如用户名、密码等,这样,当用户在不同 Web 页面跳转或再次登录网站时,就不用重新输入用户名和密码(当然,如果 Cookie 被删除和 Session 过期则需要重新输入)。 Requests 提供了会话对象让我们能够跨请求保持某些参数,也可以在同一个 Session 实例发出的所有请求之间保持 Cookie。 下面,我们看看会话对象的使用。 下面是一个跨请求保持 Cookie 的例子: ```python >>> import requests >>> s = requests.Session() >>> s.get('http://httpbin.org/cookies/set/sessioncookie/123456789') >>> r = s.get("http://httpbin.org/cookies") >>> print r.text { "cookies": { "sessioncookie": "123456789" } } ``` 会话还可用来为请求方法提供缺省数据,通过设置会话对象的属性来实现: ```python import requests s = requests.Session() s.auth = ('user', 'pass') s.headers.update({'x-test': 'true'}) # x-test 和 x-test2 都会被发送 s.get('http://httpbin.org/headers', headers={'x-test2': 'true'}) ``` # 代理 Requests 支持基本的 HTTP 代理 和 SOCKS 代理(2.10.0 新增功能)。 ## HTTP 代理 如果需要使用 HTTP 代理,我们可以为任意请求方法提供 `proxies` 参数,如下: ```python import requests proxies = { "http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080", } requests.get("http://example.org", proxies=proxies) ``` 我们也可以通过设置环境变量 `HTTP_PROXY` 和 `HTTPS_PROXY` 来配置代理: ```python $ export HTTP_PROXY="http://10.10.1.10:3128" $ export HTTPS_PROXY="http://10.10.1.10:1080" $ python >>> import requests >>> requests.get("http://example.org") ``` ## SOCKS 代理 Requess 自 2.10.0 版起,开始支持 SOCKS 协议的代理,如果要使用,我们还需安装一个第三方库: ``` $ pip install requests[socks] ``` SOCKS 代理的使用和 HTTP 代理类似: ```python import requests proxies = { "http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port", } requests.get("http://example.org", proxies=proxies) ``` # 身份认证 大部分 Web 服务都需要身份认证,并且有多种不同的认证类型,比如: - 基本身份认证 - 摘要式身份认证 - OAuth 认证 下面介绍一下基本身份认证和 OAuth 认证。 ## 基本身份认证 基本身份认证(HTTP Basic Auth)是最简单的一种身份认证,一般需要身份认证的 Web 服务也都接受 HTTP Basic Auth,Requests 提供了非常简单的形式让我们使用 HTTP Basic Auth: ```python >>> from requests.auth import HTTPBasicAuth >>> requests.get('https://api.github.com/user', auth=HTTPBasicAuth('user', 'pass')) ``` 由于 HTTP Basic Auth 非常常见,Requests 提供了一种简写的形式: ```python requests.get('https://api.github.com/user', auth=('user', 'pass')) ``` ## OAuth 2 认证 OAuth 是一种常见的 Web API 认证方式,目前的版本是 2.0。Requests 并不直接支持 OAuth 认证,而是要配合另外一个库一起使用,该库是 [requests-oauthlib](https://github.com/requests/requests-oauthlib)。 下面以 GitHub 为例,介绍一下 OAuth 2 认证。 ```python >>> # Credentials you get from registering a new application >>> client_id = '' >>> client_secret = '' >>> # OAuth endpoints given in the GitHub API documentation >>> authorization_base_url = 'https://github.com/login/oauth/authorize' >>> token_url = 'https://github.com/login/oauth/access_token' >>> from requests_oauthlib import OAuth2Session >>> github = OAuth2Session(client_id) >>> # Redirect user to GitHub for authorization >>> authorization_url, state = github.authorization_url(authorization_base_url) >>> print 'Please go here and authorize,', authorization_url >>> # Get the authorization verifier code from the callback url >>> redirect_response = raw_input('Paste the full redirect URL here:') >>> # Fetch the access token >>> github.fetch_token(token_url, client_secret=client_secret, >>> authorization_response=redirect_response) >>> # Fetch a protected resource, i.e. user profile >>> r = github.get('https://api.github.com/user') >>> print r.content ``` 更多关于 OAuth 工作流程的信息,可以参考 [OAuth 官方网站](https://oauth.net/),关于 `requests-oauthlib` 库的使用,可以参考[官方文档](https://requests-oauthlib.readthedocs.io/en/latest/#)。 # 小结 - 任何时候调用 requests.*() 你都在做两件主要的事情。其一,你在构建一个 Request 对象, 该对象将被发送到某个服务器请求或查询一些资源。其二,一旦 requests 得到一个从 服务器返回的响应就会产生一个 Response 对象。该响应对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象。 # 参考资料 - [Requests: Requests 2.10.0 文档](http://docs.python-requests.org/zh_CN/latest/) - [如何理解HTTP协议的 “无连接,无状态” 特点?](http://blog.csdn.net/tennysonsky/article/details/44562435) - [理解OAuth 2.0 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) - [作为客户端与HTTP服务交互 — python3-cookbook 2.0.0 文档](http://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p01_interact_with_http_services_as_client.html)