Requests 模块中文文档(二)

原文链接 -> 传送门

The User Guide(2)

目录

The User Guide(2)

五、高级用法

六、身份认证


五、高级用法

本篇文档涵盖了 Requests 的一些高级特性。

Session 对象

Session 对象允许你能够在跨请求的同时保持某些参数,它也会在同一个 Session 实例发出的所有请求之间保持 cookies,并且在此期间使用 urllib3 的 connection pooling(连接池)功能。所以,如果你向同一主机发送多个请求,底层的 TCP 连接将会被重用,从而带来显著的性能提升(参见 HTTP persistent connection)。

Session 对象拥有那些主要的 Requests API 的所有方法。

下面实验一下在跨请求中保持 cookies:

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"}}'

Sessions 也可用来为请求方法提供默认数据,这是通过为一个 Session 对象的属性赋值来实现的:

s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})

# both 'x-test' and 'x-test2' are sent
s.get('http://httpbin.org/headers', headers={'x-test2': 'true'})

任何你传递给请求方法的字典都会与已经设置的会话层数据进行合并,方法层的参数将覆盖会话的参数。

不过需要注意的是,就算使用了会话,方法层的参数也不会被跨请求保持。下面的例子只会和第一个请求发送 cookies ,而非第二个:

s = requests.Session()

r = s.get('http://httpbin.org/cookies', cookies={'from-my': 'browser'})
print(r.text)
# '{"cookies": {"from-my": "browser"}}'

r = s.get('http://httpbin.org/cookies')
print(r.text)
# '{"cookies": {}}'

如果你要手动为会话添加 cookie,可以使用 Cookie 系列函数来操纵 Session.cookies

Sessions 还可以被用作上下文管理器:

with requests.Session() as s:
    s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')

这样就能确保 with 区块退出后会话能被关闭,即使发生了异常也一样。

从字典参数中移除一个值

有时候你可能会想省略字典参数中一些会话层的键。要做到这一点,你只需简单地在方法层参数中将那个键的值设置为 None ,那个键就会被自动省略掉。

包含在一个会话中的所有数据你都可以直接使用。学习更多细节请阅读 会话 API 文档

请求和响应对象

任何时候进行了类似 requests.get() 的调用,你其实做的就是两件事儿:第一,构建一个 Request 对象,该对象将被发送到某个服务器请求或查询一些资源;第二,一旦 Requests 得到一个从服务器返回的响应就会产生一个 Response 对象。该 Response 对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象。

下面是一个简单的请求,从维基的服务器上获取一些非常重要的信息:

>>> r = requests.get('http://en.wikipedia.org/wiki/Monty_Python')

如果我们想访问服务器返回的响应头部信息,可以这样做:

>>> r.headers
{'content-length': '56170', 'x-content-type-options': 'nosniff', 'x-cache':
'HIT from cp1006.eqiad.wmnet, MISS from cp1010.eqiad.wmnet', 'content-encoding':
'gzip', 'age': '3080', 'content-language': 'en', 'vary': 'Accept-Encoding,Cookie',
'server': 'Apache', 'last-modified': 'Wed, 13 Jun 2012 01:33:50 GMT',
'connection': 'close', 'cache-control': 'private, s-maxage=0, max-age=0,
must-revalidate', 'date': 'Thu, 14 Jun 2012 12:59:39 GMT', 'content-type':
'text/html; charset=UTF-8', 'x-cache-lookup': 'HIT from cp1006.eqiad.wmnet:3128,
MISS from cp1010.eqiad.wmnet:80'}

然而,如果获取我们发送到服务器的请求头部,可以直接访问该请求的 headers 属性:

>>> r.request.headers
{'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0'}

预备请求

当你从 API 或者 Session 调用中收到一个 Response 对象时,request 属性实际上使用的是 PreparedRequest。这玩意儿有什么用呢?比如有时候在发送请求之前,你如果需要对 body 或者 header(或者其它什么东西)做一些额外处理就很有用了,像下面这样:

from requests import Request, Session

s = Session()

req = Request('POST', url, data=data, headers=headers)
prepped = req.prepare()

# do something with prepped.body
prepped.body = 'No, I want exactly this as the body.'

# do something with prepped.headers
del prepped.headers['Content-Type']

resp = s.send(prepped,
    stream=stream,
    verify=verify,
    proxies=proxies,
    cert=cert,
    timeout=timeout
)

print(resp.status_code)

然而,上述代码会失去 Requests Session 对象的一些优势,尤其 Session 级别的状态,例如 cookies 就不会被应用到你的请求上去。要获取一个带有状态的 PreparedRequest, 请使用 Session.prepare_request() 取代 Request.prepare(),像下面这样:

from requests import Request, Session

s = Session()
req = Request('GET',  url, data=data, headers=headers)

prepped = s.prepare_request(req)

# do something with prepped.body
prepped.body = 'Seriously, send exactly these bytes.'

# do something with prepped.headers
prepped.headers['Keep-Dead'] = 'parrot'

resp = s.send(prepped,
    stream=stream,
    verify=verify,
    proxies=proxies,
    cert=cert,
    timeout=timeout
)

print(resp.status_code)

当你使用预备请求流的时候,请记住它并不会考虑到用户环境。如果你希望通过设置环境变量来改变请求行为的时候,这可能会带来一些问题。例如:在 REQUESTS_CA_BUNDLE 中指定的自签名 SSL 证书将不被考虑。因此,SSL:CERTIFICATE_VERIFY_FAILED 被抛出。你可以通过将环境设置合并到会话中来解决此问题:

from requests import Request, Session

s = Session()
req = Request('GET', url)

prepped = s.prepare_request(req)

# Merge environment settings into session
settings = s.merge_environment_settings(prepped.url, None, None, None, None)
resp = s.send(prepped, **settings)

print(resp.status_code)

SSL 证书验证

就像 web 浏览器一样,Requests 可以为 HTTPS 请求验证 SSL 证书。SSL 验证默认是开启的,如果证书验证失败,Requests 会抛出 SSLError:

>>> requests.get('https://requestb.in')
requests.exceptions.SSLError: hostname 'requestb.in' doesn't match either of '*.herokuapp.com', 'herokuapp.com'

由于该域名我没有设置 SSL,所以失败了。但 Github 设置了 SSL:

>>> requests.get('https://github.com', verify=True)

你可以为 verify 传入 CA_BUNDLE 文件的路径,或者包含可信任 CA 证书文件的文件夹路径:

>>> requests.get('https://github.com', verify='/path/to/certfile')

或者将其保持在会话中:

s = requests.Session()
s.verify = '/path/to/certfile'

注释

如果 verify 设为文件夹路径,文件夹必须通过 OpenSSL 提供的 c_rehash 工具处理。

你还可以通过 REQUESTS_CA_BUNDLE 环境变量定义可信任 CA 列表。

如果你将 verify 设置为 False,Requests 也能忽略对 SSL 证书的验证。

>>> requests.get('https://kennethreitz.org', verify=False)

默认情况下, verify 是设置为 True,verify 选项仅应用于主机证书。

客户端证书

你也可以指定一个本地证书作为客户端证书,可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:

>>> requests.get('https://kennethreitz.org', cert=('/path/client.cert', '/path/client.key'))

或者将其保持在会话中:

s = requests.Session()
s.cert = '/path/client.cert'

如果你指定了一个错误路径或一个无效的证书,Requests 会扔给你一个 SSLError:

>>> requests.get('https://kennethreitz.org', cert='/wrong_path/client.pem')
SSLError: [Errno 336265225] _ssl.c:347: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib

警告

本地证书的私有 key 必须是解密状态,因为当前 Requests 还不支持使用加密的 key。

CA 证书

Requests 默认附带了一套它信任的根证书,来自于 Mozilla trust store。然而,它们只有在每次 Requests 更新时才会被更新。这就意味着如果你固定使用某一版本的 Requests,你的证书有可能已经 OUT 了。

从 Requests 2.4.0 版之后,如果系统中装了 certifi 包,Requests 会试图使用它里边的证书。这样用户就可以在不修改代码的情况下更新他们的可信任证书啦。

为了安全起见,我们建议你经常更新 certifi!

响应体内容工作流

默认情况下,当你发起网络请求后响应体便会立即被下载。你可以通过 stream 参数修改这个行为,推迟下载响应体直到 Response.content 属性被访问为止:

tarball_url = 'https://github.com/requests/requests/tarball/master'
r = requests.get(tarball_url, stream=True)

此时仅有响应头被下载下来,连接保持打开状态,因此允许我们根据条件获取内容:

if int(r.headers['content-length']) < TOO_LONG:
  content = r.content
  ...

你可以进一步使用 Response.iter_content() 和 Response.iter_lines() 方法来控制工作流,或者以 Response.raw 从底层 urllib3 的 urllib3.HTTPResponse 中读取未解码的响应体。

如果你在请求中把 stream 参数设为 True,Requests 无法将连接释放回连接池,除非你消耗了所有的数据,或者调用了 Response.close() 方法。这样会带来连接效率低下的问题。如果你指定 stream=True 的同时还在部分读取响应体(或者完全没有读取),那么你就应该考虑使用 with 语句发送请求,这样可以保证请求一定会被关闭:

with requests.get('http://httpbin.org/get', stream=True) as r:
    # Do things with the response here.

保持活动状态

好消息 —— 归功于 urllib3,在同一会话内的持久连接是 100% 自动处理的!同一会话中发出的任何请求都会自动复用恰当的连接!

注意:只有等到所有的响应体数据被读取完毕后,连接才能被释放回连接池以待重用;所以,请确保将 stream 参数设置为 False 或读取 Response 对象的 content 属性。

流上传

Requests 支持以流的形式上传数据,该特性允许你发送尺寸巨大的数据流或文件而无需先把它们读入内存。要使用流上传,仅需为你的请求体提供一个类文件对象即可:

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

警告

我们强烈建议你用二进制模式(binary mode)来打开文件。因为 Requests 可能会为你提供头部信息中的 Content-Length,在这种情况下该值会被设置为文件的字节数。如果你用文本模式打开文件,就可能会出现错误。

分块编码请求

Requests 也支持分块传输编码,要发送一个分块编码请求,仅需为你的请求体提供一个生成器(或任意没有具体长度的迭代器):

def gen():
    yield 'hi'
    yield 'there'

requests.post('http://some.url/chunked', data=gen())

对于分块编码请求的响应,最好的方法是使用 Response.iter_content() 方法对其数据进行迭代。理想的情况是你设置了 stream=True,这样你就可以通过调用 iter_content() 方法并将 chunk_size 参数设为 None,从而实现按块进行迭代。如果你要设置分块的最大体积,你可以把 chunk_size 参数设为任意整数。

POST 多个分块编码的文件

你可以在一个请求中发送多个文件。例如,假设你要上传多个图像文件到一个 HTML 表单中,可以使用一个叫做 "images" 的多文件 field 即可:

实现它,只需要在一个包含元组的列表中指定文件,其中元组结构为 (form_field_name, file_info)

>>> url = 'http://httpbin.org/post'
>>> multiple_files = [
        ('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
        ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
>>> r = requests.post(url, files=multiple_files)
>>> r.text
{
  ...
  'files': {'images': 'data:image/png;base64,iVBORw ....'}
  'Content-Type': 'multipart/form-data; boundary=3131623adb2043caaeb5538cc7aa0b3a',
  ...
}

警告

我们强烈建议你用二进制模式(binary mode)来打开文件。因为 Requests 可能会为你提供头部信息中的 Content-Length,在这种情况下该值会被设置为文件的字节数。如果你用文本模式打开文件,就可能会出现错误。

事件钩子

Requests 有一个钩子系统,你可以用它来控制部分请求过程,或信号事件处理。

可用的钩子:

response:

从一个 Request 中产生的响应。

你可以通过传递一个 {hook_name: callback_function} 字典给 hooks 请求参数为每个请求分配一个钩子函数:

hooks=dict(response=print_url)

callback_function() 函数会接受一个数据块作为它的第一个参数。

def print_url(r, *args, **kwargs):
    print(r.url)

如果在执行回调函数的时候发生错误,系统会抛出一个警告。

如果回调函数返回一个值,默认以该值替换传进来的数据;如果函数并未返回任何东西,则没有什么其他影响。

我们来在运行期间打印一些请求方法的参数:

>>> requests.get('http://httpbin.org', hooks=dict(response=print_url))
http://httpbin.org

自定义认证

Requests 允许你使用自定义身份验证机制。

任何传递给请求方法的 auth 参数的可调用对象,在请求发出前都有机会进行修改。

自定义身份验证机制是作为 requests.auth.AuthBase 的子类来实现的,也非常容易定义。Requests 在 requests.auth 中提供了两种常见的的身份验证方案: HTTPBasicAuth 和 HTTPDigestAuth

假设我们现在有一个 web 服务,仅在 X-Pizza 头被设置为一个密码值的情况下才会有响应。虽然在现实中这不大可能出现,但就以它为例来讲解吧!

from requests.auth import AuthBase

class PizzaAuth(AuthBase):
    """Attaches HTTP Pizza Authentication to the given Request object."""
    def __init__(self, username):
        # setup any auth-related data here
        self.username = username

    def __call__(self, r):
        # modify and return the request
        r.headers['X-Pizza'] = self.username
        return r

然后就可以使用我们的 PizzaAuth 来进行网络请求了:

>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))

Requests 流

使用 Response.iter_lines() 方法可以很方便地对流 API(例如 Twitter 的流 API )进行迭代。简单地设置 stream 参数为 True 即可使用 iter_lines() 方法进行迭代:

import json
import requests

r = requests.get('http://httpbin.org/stream/20', stream=True)

for line in r.iter_lines():

    # filter out keep-alive new lines
    if line:
        decoded_line = line.decode('utf-8')
        print(json.loads(decoded_line))

当在 Response.iter_lines() 或 Response.iter_content() 方法中使用 decode_unicode=True 时,你需要提供一个回退编码,以防服务器没有提供默认编码而导致错误:

r = requests.get('http://httpbin.org/stream/20', stream=True)

if r.encoding is None:
    r.encoding = 'utf-8'

for line in r.iter_lines(decode_unicode=True):
    if line:
        print(json.loads(line))

警告

iter_lines 不保证重进入时的安全性。多次调用该方法 会导致部分收到的数据丢失。如果你要在多处调用它,就应该使用生成的迭代器对象:

lines = r.iter_lines()
# 保存第一行以供后面使用,或者直接跳过
first_line = next(lines)

for line in lines:
    print(line)

代理

如果需要使用代理,你可以通过为任意请求方法提供 proxies 参数来配置请求:

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 来配置代理:

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

To use HTTP Basic Auth with your proxy, use the http://user:password@host/ syntax:

如果代理需要使用 HTTP Basic Auth,使用 http://user:password@host/ 的语法:

proxies = {'http': 'http://user:[email protected]:3128/'}

要为某个特定的连接方式或者主机设置代理,可以直接将 scheme://hostname 作为 key 使用,它会针对指定的主机和连接方式进行匹配:

proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}

注意,代理 URL 中必须指明连接方式。

SOCKS

Requests 2.10.0 版新增功能。

除了基本的 HTTP 代理,Request 还支持 SOCKS 协议的代理。这是一个可选功能,需要安装第三方库方可使用。

你可以使用 pip 安装该功能所需的依赖:

$ pip install requests[socks]

安装好依赖以后,使用 SOCKS 代理就和使用 HTTP 代理一样简单:

proxies = {
    'http': 'socks5://user:pass@host:port',
    'https': 'socks5://user:pass@host:port'
}

使用 socks5 连接方式使得 DNS 解析在客户端进行而不是代理服务器;如果你希望域名的 DNS 解析是在代理服务器上进行的,那么请使用 socks5h 连接方式代替。

合规性

为了不给你们的使用造成不必要的困难,Requests 的设计几乎符合所有相关的规范和 RFCs。

但请理解,有时候为了考虑迎合规范的需求,Requests 可能会做出一些大家看着有点奇怪的行为……

编码

前面我们说过,当你收到一个响应时,Requests 会猜测响应的编码方式,用于在你调用 Response.text() 方法时进行解码。Requests 首先在 HTTP 头部检测是否存在指定的编码方式,如果不存在,则会使用 chardet 来尝试猜测编码方式。

注:关于使用 chardet 检测编码的技能,在《一次性解决你所有的编码检测问题》一文中有讲解,感兴趣的童鞋可以参考下^_^

只有当 HTTP 头部中没有明确指定字符集,并且 Content-Type 头部字段包含 text 值时,Requests 才不会去猜测编码。在这种情况下,RFC 2616 指定默认字符集必须是 ISO-8859-1,Requests 遵从这一规范。如果你需要一种不同的编码方式,你可以手动设置 Response.encoding 属性,或使用原始的 Response.content

HTTP 动词

Requests 提供了几乎所有 HTTP 动词的功能:GET、OPTIONS、HEAD、POST、PUT、PATCH 和 DELETE。下面我们通过一些详细的例子,演示在 Requests 中如何使用这些动词。

我们从最常使用的动词 GET 开始。HTTP GET 是一个幂等方法(所谓幂等方法,是指该方法多次调用返回的效果是一致的),它的作用是从给定的 URL 返回一个资源。当你试图从一个 web 位置获取数据时,你应该使用这个动词。

下面这个例子尝试从 GitHub 上获取关于一个指定的 commit。假设我们像通过 Requests 来获取 commit a050faf 的数据,可以这样做:

>>> import requests
>>> r = requests.get('https://api.github.com/repos/requests/requests/git/commits/a050faf084662f3a352dd1a941f2c7c9f886d4ad')

我们应该确认 GitHub 是否有正确地响应请求,如果是,我们要继续弄清楚响应内容具体是什么类型,比如这样:

>>> if r.status_code == requests.codes.ok:
...     print(r.headers['content-type'])
...
application/json; charset=utf-8

可见,GitHub 返回了 JSON 数据,非常好,这样就可以使用 r.json 方法把这个返回的数据解析成 Python 对象。

>>> commit_data = r.json()

>>> print(commit_data.keys())
[u'committer', u'author', u'url', u'tree', u'sha', u'parents', u'message']

>>> print(commit_data[u'committer'])
{u'date': u'2012-05-10T11:10:50-07:00', u'email': u'[email protected]', u'name': u'Kenneth Reitz'}

>>> print(commit_data[u'message'])
makin' history

到目前为止,一切都非常简单。现在,我们来研究一下 GitHub 的 API,我们可以翻查文档,但如果使用 Requests 来研究也许会更有意思一些。我们可以借助 Requests 的 OPTIONS 动词来看看我们刚使用过的 url 支持哪些 HTTP 方法。

>>> verbs = requests.options(r.url)
>>> verbs.status_code
500

什么?这是什么鬼?!返回的信息毫无帮助嘛!原来 GitHub 与许多 API 提供方一样,实际上并没有实现 OPTIONS 方法。真是让人感到气愤!但没关系,我们还可以查找那枯燥的文档。不过话说回来,如果 GitHub 正确地实现了 OPTIONS,那么服务器应该在响应头中返回允许用户使用的 HTTP 方法,例如:

>>> verbs = requests.options('http://a-good-website.com/api/cats')
>>> print(verbs.headers['allow'])
GET,HEAD,POST,OPTIONS

转而去查看文档,我们看到对于提交信息,另一个允许的方法是 POST,它会创建一个新的提交。由于我们正在使用 Requests 代码库,我们应尽可能避免对它发送笨拙的 POST。作为替代,我们来玩玩 GitHub 的 Issue 功能。

下面代码访问的是 Issue #482,该问题已经存在,我们就以它为例讲解:

>>> r = requests.get('https://api.github.com/requests/kennethreitz/requests/issues/482')
>>> r.status_code
200

>>> issue = json.loads(r.text)

>>> print(issue[u'title'])
Feature any http verb in docs

>>> print(issue[u'comments'])
3

酷,有三个评论,我们看下最后一个说的什么:

>>> r = requests.get(r.url + u'/comments')
>>> r.status_code
200

>>> comments = r.json()

>>> print(comments[0].keys())
[u'body', u'url', u'created_at', u'updated_at', u'user', u'id']

>>> print(comments[2][u'body'])
Probably in the "advanced" section

嗯,看起来似乎是个愚蠢的回复。现在让我们发表个评论来告诉这位评论者他到底有愚蠢……那么,这个评论者是谁呢?

>>> print(comments[2][u'user'][u'login'])
kennethreitz

好吧,我们来告诉这个叫做 Kenneth 的家伙(其实是作者自己),这个例子应该放在快速上手指南中。根据 GitHub API 文档,其方法是 POST 到该话题。我们不妨试试看:

>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it!"})
>>> url = u"https://api.github.com/repos/requests/requests/issues/482/comments"

>>> r = requests.post(url=url, data=body)
>>> r.status_code
404

额,怎么返回 404 了呢?有点诡异……可能我们需要验证身份……那就有点麻烦了,对吧?不对。Requests 简化了多种身份验证形式的使用,包括非常常见的 Basic Auth。

>>> from requests.auth import HTTPBasicAuth
>>> auth = HTTPBasicAuth('[email protected]', 'not_a_real_password')

>>> r = requests.post(url=url, data=body, auth=auth)
>>> r.status_code
201

>>> content = r.json()
>>> print(content[u'body'])
Sounds great! I'll get right on it.

太棒了!噢,不!我原本是想说等我一会,因为我得去喂我的猫(没想到作者也是猫奴(>^ω^<)喵)。如果我能够编辑这条评论那就好了!幸运的是,GitHub 允许我们使用另一个 HTTP 动词 PATCH 来编辑评论。试试看吧:

>>> print(content[u"id"])
5804413

>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it once I feed my cat."})
>>> url = u"https://api.github.com/repos/requests/requests/issues/comments/5804413"

>>> r = requests.patch(url=url, data=body, auth=auth)
>>> r.status_code
200

非常棒!现在,我们来折磨一下这个叫做 Kenneth 的家伙,我决定要让他急得团团转,也不告诉他背后到底是谁在捣蛋……我的意思说时我现在想删除这条评论了……

GitHub 允许我们使用 DELETE 方法来删除评论:

>>> r = requests.delete(url=url, auth=auth)
>>> r.status_code
204
>>> r.headers['status']
'204 No Content'

很好,评论消失了。最后一件我想知道的事情是我已经使用了多少限额(ratelimit)。查查看,GitHub 在响应头部发送这个信息,因此不必下载整个网页,我将使用一个 HEAD 请求来获取响应头:

>>> r = requests.head(url=url, auth=auth)
>>> print(r.headers)
...
'x-ratelimit-remaining': '4995'
'x-ratelimit-limit': '5000'
...

很好。是时候写个 Python 程序以各种刺激的方式滥用 GitHub 的 API 了,因为上面显示我们还可以使用 4995 次呢!

自定义动词

有时候你会碰到一些服务器,基于某些原因,它们或者要求用户使用上述 HTTP 动词之外的自定义动词。比如说 WEBDAV 服务器会要求你使用 MKCOL 方法。不过别担心,Requests 一样可以搞定它们。你可以使用内建的 .request 方法,例如:

>>> r = requests.request('MKCOL', url, data=data)
>>> r.status_code
200 # Assuming your call was correct

这样你就可以使用服务器要求的任意动词了。

响应头链接字段

许多 HTTP API 都有响应头链接字段的功能,它们使得 API 能够更好地自我描述和自我显露。

GitHub 在 API 中为 分页 使用这些功能,例如:

>>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10'
>>> r = requests.head(url=url)
>>> r.headers['link']
'; rel="next", ; rel="last"'

Requests 会自动解析这些响应头链接字段,并使得它们非常易于使用:

>>> r.links["next"]
{'url': 'https://api.github.com/users/kennethreitz/repos?page=2&per_page=10', 'rel': 'next'}

>>> r.links["last"]
{'url': 'https://api.github.com/users/kennethreitz/repos?page=7&per_page=10', 'rel': 'last'}

传输适配器

从 v1.0.0 以后,Requests 的内部采用了模块化设计。部分原因是为了实现传输适配器(Transport Adapters),你可以看看关于它的最早描述。传输适配器提供了一个机制,让你可以为 HTTP 服务定义交互方法,尤其是它允许你应用服务前的配置。

Requests 自带了一个传输适配器,也就是 HTTPAdapter。这个适配器使用了强大的 urllib3 库,为 Requests 提供了默认的 HTTP 和 HTTPS 交互。每当 Session 被初始化,就会有适配器附着在 Session 上,其中一个供 HTTP 使用,另一个供 HTTPS 使用。

Requests 允许用户创建和使用他们自己的传输适配器,实现他们需要的特殊功能。创建好以后,传输适配器可以被加载到一个会话对象上,附带着一个说明,告诉会话适配器应该应用在哪个 web 服务上。

>>> s = requests.Session()
>>> s.mount('http://www.github.com', MyAdapter())

这个 mount 调用会注册一个传输适配器的特定实例到一个前缀上面。加载以后,任何使用该会话的 HTTP 请求,只要其 URL 是以给定的前缀开头,该传输适配器就会被使用到。

传输适配器的众多实现细节不在本文档的覆盖范围内,不过你可以看看接下来这个简单的 SSL 案例。更多的用法,你也许该考虑为 BaseAdapter 创建子类。

案例:指定 SSL 版本

Requests 开发团队刻意指定了内部库(urllib3)的默认 SSL 版本。一般情况下这样做没有问题,不过有时候你可能会需要连接到一个服务节点,而该节点使用了和默认不同的 SSL 版本。

你可以使用传输适配器解决这个问题,通过利用 HTTPAdapter 现有的大部分实现,再加上一个 ssl_version 参数并将它传递到 urllib3 中。我们会创建一个传输适配器,用来告诉 urllib3 让它使用 SSLv3:

import ssl

from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager


class Ssl3HttpAdapter(HTTPAdapter):
    """"Transport adapter" that allows us to use SSLv3."""

    def init_poolmanager(self, connections, maxsize, block=False):
        self.poolmanager = PoolManager(
            num_pools=connections, maxsize=maxsize,
            block=block, ssl_version=ssl.PROTOCOL_SSLv3)

阻塞和非阻塞

使用默认的传输适配器,Requests 不提供任何形式的非阻塞 IO。Response.content 属性会阻塞,直到整个响应下载完成。如果你需要更多精细控制,该库的数据流功能(见上面流请求章节) 允许你每次接受少量的一部分响应,不过这些调用依然是阻塞式的。

如果你对于阻塞式 IO 有所顾虑,还有很多项目可以供你使用,它们结合了 Requests 和 Python 的某个异步框架。典型的优秀例子是 requests-threadsgrequests 和 requests-futures

Header 排序

在某些特殊情况下你也许需要按照次序来提供 headers,如果你向 headers 关键字参数传入一个 OrderedDict,就可以向提供一个带排序的 headers。然而,Requests 使用的默认 headers 的次序会被优先选择,这意味着如果你在 headers 关键字参数中覆盖了默认 headers,和关键字参数中别的 headers 相比,它们也许看上去会是次序错误的。

如果这个对你来说是个问题,那么用户应该考虑在 Session 对象上面设置默认 headers,只要将 Session 设为一个定制的 OrderedDict 即可。这样就会让它成为优选的次序。

超时

为防止服务器不能及时响应,大部分发至外部服务器的请求都应该带着 timeout 参数。在默认情况下,除非显式指定了 timeout 值,requests 是不会自动进行超时处理的。如果没有 timeout,你的代码可能会挂起若干分钟甚至更长时间。

连接超时指的是在你的客户端实现到远端机器端口的连接时(对应的是 connect()),Request 会等待的秒数。一个很好的实践方法是把连接超时设为比 3 的倍数略大的一个数值,因为 TCP 数据包重传窗口 的默认大小是 3。

一旦你的客户端连接到了服务器并且发送了 HTTP 请求,读取超时指的就是客户端等待服务器发送请求的时间(它指的是客户端要等待服务器发送字节之间的时间。在 99.9% 的情况下这指的是服务器发送第一个字节之前的时间)。

如果你指定了一个值作为 timeout,像这样:

r = requests.get('https://github.com', timeout=5)

timeout 还可以分别指定 connect 和 read 两者的超时时间,只需要传入一个元组即可:

r = requests.get('https://github.com', timeout=(3.05, 27))

如果远端服务器很慢,你可以让 Requests 一直等下去……传入一个 None 作为 timeout 值,然后就冲杯咖啡泡妹子去吧。

r = requests.get('https://github.com', timeout=None)

六、身份认证

这一篇我们讨论五花八门的身份认证方式在 Requests 下的使用。

许多 web 服务都需要进行身份认证,并且认证的种类也是千奇百怪的。所以,下面我们从简单到复杂给大家概述一下 Requests 中可以使用的一些身份认证方式。

基本认证

许多要求身份认证的 web 服务都接受 HTTP Basic Auth。这是最简单的一种身份认证,并且 Requests 对这种认证方式的支持直接是“开箱即用”的程度。

发送带 HTTP Basic Auth 的请求非常简单:

>>> from requests.auth import HTTPBasicAuth
>>> requests.get('https://api.github.com/user', auth=HTTPBasicAuth('user', 'pass'))

事实上,由于 HTTP Basic Auth 随处可见,所以 Requests 也就为其提供了简写的使用方式:

>>> requests.get('https://api.github.com/user', auth=('user', 'pass'))

像这样直接在一个元组中提供认证信息与上面那个 HTTPBasicAuth 例子的效果是完全相同的。

netrc 认证

如果认证方法没有收到 auth 参数,Requests 将尝试从用户的 netrc 文件中获取需要的认证身份(netrc 文件会覆盖 headers= 中指定的原始 HTTP 认证信息)。

如果找到了域名对应的认证信息,就会以 HTTP Basic Auth 的形式发送请求。

摘要式认证

另一种非常流行的 HTTP 身份认证形式是摘要式认证,Requests 对它的支持也是“开箱即用”:

>>> from requests.auth import HTTPDigestAuth
>>> url = 'http://httpbin.org/digest-auth/auth/user/pass'
>>> requests.get(url, auth=HTTPDigestAuth('user', 'pass'))

OAuth 1 认证

Oauth 是一种常见的 Web APIs 认证方式。requests-oauthlib 库可以让 Requests 简单地创建 OAuth 认证的请求:

>>> import requests
>>> from requests_oauthlib import OAuth1

>>> url = 'https://api.twitter.com/1.1/account/verify_credentials.json'
>>> auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET',
...               'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')

>>> requests.get(url, auth=auth)

关于 OAuth 工作流程的更多信息,请参见 OAuth 官方网站。关于 requests-oauthlib 库的文档和用例,请参见 GitHub 的 requests_oauthlib 代码库。

OAuth 2 与 OpenID 连接认证

requests-oauthlib 库还可以处理 OAuth 2,OAuth 2 是 OpenID Cennect 的基础机制。请查看 requests-oauthlib OAuth2 文档以了解 OAuth 2 的各种认证管理流程:

  • Web Application Flow
  • Mobile Application Flow
  • Legacy Application Flow
  • Backend Application Flow

其他认证形式

Requests 的设计架构允许你将其他形式的身份认证以简易的方式插入其中。开源社区的成员时常为更复杂或不那么常用的身份认证形式编写认证处理插件。其中一些最优秀的已被收集在 Requests organization页面中,包括:

  • Kerberos
  • NTLM

如果你想使用其中任何一种身份认证形式,直接去到它们的 GitHub 页面,依照说明安装使用即可。

新的认证形式

如果你还是找不到所需要的身份认证形式或者觉得现有的你不满意,你也可以自己来尝试实现它。Requests 的设计使得你非常容易就可以添加自己的身份认证模块。

要想自己来实现,就从 AuthBase 继承一个子类,并实现 __call__() 方法:

>>> import requests
>>> class MyAuth(requests.auth.AuthBase):
...     def __call__(self, r):
...         # Implement my authentication
...         return r
...
>>> url = 'http://httpbin.org/get'
>>> requests.get(url, auth=MyAuth())

当一个身份认证模块被附加到一个请求上时,在设置 request 期间就会调用该模块。因此,__call__ 方法必须完成使得身份认证生效的所有事情。一些身份认证形式会额外地添加钩子来提供进一步的功能。

最后,你还可以在 Requests organization 页面的 auth.py 文件中找到更多示例。

 

你可能感兴趣的:(Python进阶实战)