高级用法¶
本文档涵盖了 Requests 的一些更高级的功能。
会话对象¶
会话对象允许您在请求之间保留某些参数。它还保留从会话实例发出的所有请求中的 Cookie,并将使用 urllib3
的 连接池。因此,如果您向同一主机发出多个请求,则底层 TCP 连接将被重用,这可能会显著提高性能(请参阅 HTTP 持久连接)。
会话对象具有主 Requests API 的所有方法。
让我们在请求之间保留一些 Cookie
s = requests.Session()
s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get('https://httpbin.org/cookies')
print(r.text)
# '{"cookies": {"sessioncookie": "123456789"}}'
会话还可以用于向请求方法提供默认数据。这是通过向会话对象上的属性提供数据来完成的
s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})
# both 'x-test' and 'x-test2' are sent
s.get('https://httpbin.org/headers', headers={'x-test2': 'true'})
您传递给请求方法的任何字典都将与设置的会话级值合并。方法级参数会覆盖会话参数。
但是,请注意,即使使用会话,方法级参数也不会在请求之间保留。此示例只会将 Cookie 与第一个请求一起发送,但不会与第二个请求一起发送
s = requests.Session()
r = s.get('https://httpbin.org/cookies', cookies={'from-my': 'browser'})
print(r.text)
# '{"cookies": {"from-my": "browser"}}'
r = s.get('https://httpbin.org/cookies')
print(r.text)
# '{"cookies": {}}'
如果您想手动向会话添加 Cookie,请使用 Cookie 实用程序函数 来操作 Session.cookies
。
会话还可以用作上下文管理器
with requests.Session() as s:
s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
这将确保会话在退出 with
块后立即关闭,即使发生了未处理的异常。
从字典参数中删除值
有时,您需要从字典参数中省略会话级键。为此,您只需在方法级参数中将该键的值设置为 None
。它将自动被省略。
会话中包含的所有值都直接对您可用。请参阅 会话 API 文档 以了解更多信息。
请求和响应对象¶
每当调用 requests.get()
及其相关函数时,您都在做两件主要的事情。首先,您正在构建一个 Request
对象,该对象将被发送到服务器以请求或查询某些资源。其次,一旦 Requests 从服务器获取响应,就会生成一个 Response
对象。Response
对象包含服务器返回的所有信息,还包含您最初创建的 Request
对象。这是一个简单的请求,用于从维基百科的服务器获取一些非常重要的信息
>>> r = requests.get('https://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'}
但是,如果我们想要获取我们发送给服务器的标头,我们只需访问请求,然后访问请求的标头
>>> r.request.headers
{'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0'}
准备好的请求¶
每当您从 API 调用或会话调用中收到一个 Response
对象时,request
属性实际上是所使用的 PreparedRequest
。在某些情况下,您可能希望在发送请求之前对正文或标头(或其他任何内容)执行一些额外的工作。以下为此提供了简单的秘诀
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)
由于您没有对 Request
对象执行任何特殊操作,因此您立即准备它并修改 PreparedRequest
对象。然后,您使用发送到 requests.*
或 Session.*
的其他参数发送它。
但是,上述代码将失去拥有 Requests Session
对象的一些优势。特别是,Session
级别的状态(如 cookie)不会应用到您的请求中。要获取应用了该状态的 PreparedRequest
,请将对 Request.prepare()
的调用替换为对 Session.prepare_request()
的调用,如下所示
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)
resp = s.send(prepped, **settings)
print(resp.status_code)
SSL 证书验证¶
Requests 验证 HTTPS 请求的 SSL 证书,就像 Web 浏览器一样。默认情况下,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 设置了
>>> requests.get('https://github.com')
<Response [200]>
您可以将 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
实用程序处理该目录。
此受信任 CA 列表也可以通过 REQUESTS_CA_BUNDLE
环境变量指定。如果 REQUESTS_CA_BUNDLE
未设置,则 CURL_CA_BUNDLE
将用作后备。
如果将 verify
设置为 False,Requests 还可以忽略验证 SSL 证书
>>> requests.get('https://kennethreitz.org', verify=False)
<Response [200]>
请注意,当 verify
设置为 False
时,请求将接受服务器提供的任何 TLS 证书,并且会忽略主机名不匹配和/或证书过期,这将使你的应用程序容易受到中间人 (MitM) 攻击。在本地开发或测试期间,将 verify 设置为 False
可能有用。
默认情况下,verify
设置为 True。选项 verify
仅适用于主机证书。
客户端证书¶
你还可以指定一个本地证书用作客户端证书,作为一个文件(包含私钥和证书)或两个文件路径的元组
>>> requests.get('https://kennethreitz.org', cert=('/path/client.cert', '/path/client.key'))
<Response [200]>
或持久
s = requests.Session()
s.cert = '/path/client.cert'
如果你指定了错误的路径或无效的证书,你将收到 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
警告
本地证书的私钥必须未加密。目前,Requests 不支持使用加密密钥。
CA 证书¶
Requests 使用来自 certifi 包的证书。这允许用户在不更改 Requests 版本的情况下更新其受信任的证书。
在 2.16 版之前,Requests 捆绑了一组受信任的根 CA,这些 CA 来自 Mozilla 信任存储。这些证书仅在每个 Requests 版本中更新一次。当 certifi
未安装时,在使用明显较旧版本的 Requests 时,这会导致证书包极其过时。
为了安全起见,我们建议经常升级 certifi!
正文内容工作流¶
默认情况下,当你发出请求时,响应正文会立即下载。你可以覆盖此行为并推迟下载响应正文,直到使用 stream
参数访问 Response.content
属性
tarball_url = 'https://github.com/psf/requests/tarball/main'
r = requests.get(tarball_url, stream=True)
此时,仅下载响应头且连接保持打开,因此允许我们对内容检索进行条件设置
if int(r.headers['content-length']) < TOO_LONG:
content = r.content
...
你可以通过使用 Response.iter_content()
和 Response.iter_lines()
方法进一步控制工作流。或者,你可以从底层的 urllib3 urllib3.HTTPResponse
中读取未解码的正文,方法是在 Response.raw
。
如果你在发出请求时将 stream
设置为 True
,则 Requests 无法将连接释放回池,除非你使用所有数据或调用 Response.close
。这会导致连接效率低下。如果你发现自己在使用 stream=True
时部分读取请求正文(或根本不读取),则应在 with
语句中发出请求,以确保始终关闭它
with requests.get('https://httpbin.org/get', stream=True) as r:
# Do things with the response here.
长连接¶
好消息——感谢 urllib3,在会话中保持活动是 100% 自动的!在会话中发出的任何请求都将自动重用适当的连接!
请注意,只有在读取所有正文数据后,连接才会释放回池中以供重用;请务必将 stream
设置为 False
或读取 content
属性 Response
对象。
流式上传¶
Requests 支持流式上传,这允许您发送大型流或文件,而无需将它们读入内存。要进行流式传输和上传,只需为您的正文提供一个类似于文件的对象
with open('massive-body', 'rb') as f:
requests.post('http://some.url/streamed', data=f)
警告
强烈建议您在 二进制模式 中打开文件。这是因为 Requests 可能会尝试为您提供 Content-Length
标头,如果这样做,此值将设置为文件中的字节数。如果您在文本模式中打开文件,可能会发生错误。
分块编码请求¶
Requests 还支持对传出和传入请求进行分块传输编码。要发送分块编码请求,只需为您的正文提供一个生成器(或任何没有长度的迭代器)
def gen():
yield 'hi'
yield 'there'
requests.post('http://some.url/chunked', data=gen())
对于分块编码响应,最好使用 Response.iter_content()
迭代数据。在理想情况下,您将在请求中设置 stream=True
,在这种情况下,您可以通过使用 chunk_size
参数 None
调用 iter_content
来逐块迭代。如果您想设置块的最大大小,您可以将 chunk_size
参数设置为任何整数。
POST 多个多部分编码文件¶
您可以在一个请求中发送多个文件。例如,假设您想将图像文件上传到具有多个文件字段“images”的 HTML 表单
<input type="file" name="images" multiple="true" required="true"/>
要做到这一点,只需将 files 设置为 (form_field_name, file_info)
元组的列表
>>> url = 'https://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',
...
}
警告
强烈建议您在 二进制模式 中打开文件。这是因为 Requests 可能会尝试为您提供 Content-Length
标头,如果这样做,此值将设置为文件中的字节数。如果您在文本模式中打开文件,可能会发生错误。
事件挂钩¶
Requests 具有挂钩系统,您可以使用它来操作请求过程的一部分或发出事件处理信号。
可用的挂钩
响应
:从请求生成的响应。
您可以通过将 {hook_name: callback_function}
字典传递给 hooks
请求参数,逐个请求地分配挂钩函数
hooks={'response': print_url}
该 callback_function
将接收一个数据块作为其第一个参数。
def print_url(r, *args, **kwargs):
print(r.url)
您的回调函数必须处理自己的异常。任何未处理的异常都不会被静默传递,因此应由调用请求的代码处理。
如果回调函数返回一个值,则假定它将替换传入的数据。如果函数不返回任何内容,则不会影响其他任何内容。
def record_hook(r, *args, **kwargs):
r.hook_called = True
return r
让我们在运行时打印一些请求方法参数
>>> requests.get('https://httpbin.org/', hooks={'response': print_url})
https://httpbin.org/
<Response [200]>
您可以向单个请求添加多个挂钩。让我们一次调用两个挂钩
>>> r = requests.get('https://httpbin.org/', hooks={'response': [print_url, record_hook]})
>>> r.hook_called
True
您还可以向Session
实例添加挂钩。然后,您添加的任何挂钩都将在对会话发出的每个请求上调用。例如
>>> s = requests.Session()
>>> s.hooks['response'].append(print_url)
>>> s.get('https://httpbin.org/')
https://httpbin.org/
<Response [200]>
Session
可以有多个挂钩,它们将按添加顺序调用。
自定义身份验证¶
Requests 允许您指定自己的身份验证机制。
任何作为auth
参数传递给请求方法的可调用项都可以在分派之前修改请求。
身份验证实现是AuthBase
的子类,并且易于定义。Requests 在requests.auth
中提供了两种常见身份验证方案实现:HTTPBasicAuth
和HTTPDigestAuth
。
让我们假设我们有一个只有在X-Pizza
标头设置为密码值时才会响应的 Web 服务。不太可能,但请继续。
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
然后,我们可以使用我们的 Pizza Auth 发出请求
>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
<Response [200]>
流式传输请求¶
使用Response.iter_lines()
,您可以轻松地遍历流式传输 API,例如Twitter 流式传输 API。只需将stream
设置为True
,并使用iter_lines
遍历响应
import json
import requests
r = requests.get('https://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))
在将decode_unicode=True与Response.iter_lines()
或Response.iter_content()
一起使用时,您需要在服务器不提供编码时提供备用编码
r = requests.get('https://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()
# Save the first line for later or just skip it
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)
或者,您可以为整个Session
一次性配置它
import requests
proxies = {
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080',
}
session = requests.Session()
session.proxies.update(proxies)
session.get('http://example.org')
警告
设置 session.proxies
的行为可能与预期不同。提供的值将被环境代理(urllib.request.getproxies 返回的值)覆盖。要确保在存在环境代理的情况下使用代理,请明确指定所有单个请求中的 proxies
参数,如上文最初说明的那样。
有关详细信息,请参阅 #2018。
当代理配置未按上文所示按请求覆盖时,Requests 将依赖于标准环境变量 http_proxy
、https_proxy
、no_proxy
和 all_proxy
定义的代理配置。这些变量的大写变体也受支持。因此,你可以设置它们来配置 Requests(仅设置与你的需求相关的那些)
$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="http://10.10.1.10:1080"
$ export ALL_PROXY="socks5://10.10.1.10:3434"
$ python
>>> import requests
>>> requests.get('http://example.org')
要对代理使用 HTTP 基本身份验证,请在上述任何配置条目中使用 http://user:password@host/ 语法
$ export HTTPS_PROXY="http://user:pass@10.10.1.10:1080"
$ python
>>> proxies = {'http': 'http://user:pass@10.10.1.10:3128/'}
警告
在环境变量或版本控制文件中存储敏感的用户名和密码信息存在安全风险,强烈建议不要这样做。
要为特定方案和主机提供代理,请对键使用 scheme://hostname 形式。这将匹配对给定方案和确切主机的任何请求。
proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}
请注意,代理 URL 必须包含方案。
最后,请注意,对 https 连接使用代理通常需要你的本地计算机信任代理的根证书。默认情况下,Requests 信任的证书列表可以通过以下方式找到
from requests.utils import DEFAULT_CA_BUNDLE_PATH
print(DEFAULT_CA_BUNDLE_PATH)
你可以通过将 REQUESTS_CA_BUNDLE
(或 CURL_CA_BUNDLE
)环境变量设置为另一个文件路径来覆盖此默认证书包
$ export REQUESTS_CA_BUNDLE="/usr/local/myproxy_info/cacert.pem"
$ export https_proxy="http://10.10.1.10:1080"
$ python
>>> import requests
>>> requests.get('https://example.org')
SOCKS¶
版本 2.10.0 中的新增功能。
除了基本的 HTTP 代理之外,Requests 还支持使用 SOCKS 协议的代理。这是一项可选功能,需要在使用之前安装其他第三方库。
你可以从 pip
获取此功能的依赖项
$ python -m pip install requests[socks]
安装这些依赖项后,使用 SOCKS 代理就像使用 HTTP 代理一样简单
proxies = {
'http': 'socks5://user:pass@host:port',
'https': 'socks5://user:pass@host:port'
}
使用方案 socks5
会导致 DNS 解析在客户端上进行,而不是在代理服务器上进行。这与 curl 保持一致,curl 使用该方案来决定在客户端还是代理上进行 DNS 解析。如果你想在代理服务器上解析域,请使用 socks5h
作为方案。
兼容性¶
Requests 旨在与所有相关规范和 RFC 保持兼容,只要这种兼容性不会给用户造成困难。对规范的关注可能会导致一些行为,对于不熟悉相关规范的人来说可能显得不同寻常。
编码¶
当您收到响应时,Requests 会猜测在访问 Response.text
属性时用于解码响应的编码。Requests 首先会检查 HTTP 标头中的编码,如果不存在,则会使用 charset_normalizer 或 chardet 尝试猜测编码。
如果安装了 chardet
,requests
会使用它,但是对于 python3,chardet
不再是强制依赖项。chardet
库是 LGPL 许可的依赖项,某些 requests 用户无法依赖强制 LGPL 许可的依赖项。
当您在不指定 [use_chardet_on_py3]
额外信息的情况下安装 requests
,并且尚未安装 chardet
,requests
会使用 charset-normalizer
(MIT 许可)来猜测编码。
Requests 不会猜测编码的唯一情况是 HTTP 标头中不存在明确的字符集且Content-Type
标头包含 text
。在这种情况下,RFC 2616 指定默认字符集必须是 ISO-8859-1
。在这种情况下,Requests 遵循该规范。如果您需要不同的编码,您可以手动设置 Response.encoding
属性,或使用原始 Response.content
。
HTTP 动词¶
Requests 提供对几乎所有 HTTP 动词的访问:GET、OPTIONS、HEAD、POST、PUT、PATCH 和 DELETE。以下提供了在 Requests 中使用这些各种动词的详细示例,使用 GitHub API。
我们将从最常用的动词 GET 开始。HTTP GET 是一种幂等方法,用于从给定的 URL 返回资源。因此,当您尝试从 Web 位置检索数据时,您应该使用该动词。一个示例用法是尝试获取有关 GitHub 上特定提交的信息。假设我们想要在 Requests 上提交 a050faf
。我们可以像这样得到它
>>> import requests
>>> r = requests.get('https://api.github.com/repos/psf/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())
['committer', 'author', 'url', 'tree', 'sha', 'parents', 'message']
>>> print(commit_data['committer'])
{'date': '2012-05-10T11:10:50-07:00', 'email': 'me@kennethreitz.com', 'name': 'Kenneth Reitz'}
>>> print(commit_data['message'])
makin' history
到目前为止,一切都很简单。好吧,让我们稍微研究一下 GitHub API。现在,我们可以查看文档,但如果我们使用 Requests,我们可能会更有趣。我们可以利用 Requests OPTIONS 动词来查看我们刚使用的 url 上支持哪些类型的 HTTP 方法。
>>> verbs = requests.options(r.url)
>>> verbs.status_code
500
呃,什么?这没用!事实证明,GitHub 和许多 API 提供商一样,实际上并没有实现 OPTIONS 方法。这是一个令人烦恼的疏忽,但没关系,我们可以使用枯燥的文档。但是,如果 GitHub 正确地实现了 OPTIONS,它们应该在标头中返回允许的方法,例如
>>> verbs = requests.options('http://a-good-website.com/api/cats')
>>> print(verbs.headers['allow'])
GET,HEAD,POST,OPTIONS
转向文档,我们看到允许用于提交的唯一其他方法是 POST,它创建一个新的提交。由于我们正在使用 Requests 存储库,我们应该避免向其进行粗暴的 POST。相反,让我们使用 GitHub 的 Issues 功能。
此文档是针对 问题 #482 添加的。鉴于此问题已经存在,我们将使用它作为示例。让我们从获取它开始。
>>> r = requests.get('https://api.github.com/repos/psf/requests/issues/482')
>>> r.status_code
200
>>> issue = json.loads(r.text)
>>> print(issue['title'])
Feature any http verb in docs
>>> print(issue['comments'])
3
酷,我们有三个评论。我们来看一下最后一个。
>>> r = requests.get(r.url + '/comments')
>>> r.status_code
200
>>> comments = r.json()
>>> print(comments[0].keys())
['body', 'url', 'created_at', 'updated_at', 'user', 'id']
>>> print(comments[2]['body'])
Probably in the "advanced" section
嗯,这似乎是一个愚蠢的地方。让我们发布一条评论,告诉发布者他很愚蠢。发布者是谁?
>>> print(comments[2]['user']['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/psf/requests/issues/482/comments"
>>> r = requests.post(url=url, data=body)
>>> r.status_code
404
嗯,这很奇怪。我们可能需要进行身份验证。那会很痛苦,对吧?错了。Requests 使得使用许多形式的身份验证变得容易,包括非常常见的 Basic Auth。
>>> from requests.auth import HTTPBasicAuth
>>> auth = HTTPBasicAuth('fake@example.com', 'not_a_real_password')
>>> r = requests.post(url=url, data=body, auth=auth)
>>> r.status_code
201
>>> content = r.json()
>>> print(content['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/psf/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'
太棒了。一切都消失了。我最后想知道的是我使用了多少速率限制。让我们找出答案。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']
'<https://api.github.com/users/kennethreitz/repos?page=2&per_page=10>; rel="next", <https://api.github.com/users/kennethreitz/repos?page=6&per_page=10>; 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 已转向模块化内部设计。这样做部分原因是为了实现传输适配器,最初 在此处描述。传输适配器提供了一种定义 HTTP 服务交互方法的机制。特别是,它们允许您应用每个服务的配置。
Requests 附带一个传输适配器,即 HTTPAdapter
。此适配器使用功能强大的 urllib3 库提供与 HTTP 和 HTTPS 的默认 Requests 交互。每当初始化 Requests Session
时,其中一个将附加到 Session
对象用于 HTTP,另一个用于 HTTPS。
Requests 允许用户创建和使用自己的传输适配器,这些适配器提供了特定功能。创建后,可以将传输适配器挂载到会话对象,同时指示其应应用于哪些 Web 服务。
>>> s = requests.Session()
>>> s.mount('https://github.com/', MyAdapter())
mount 调用将传输适配器的特定实例注册到前缀。挂载后,使用该会话发出的任何 HTTP 请求(其 URL 以给定前缀开头)都将使用给定的传输适配器。
注意
将根据最长前缀匹配选择适配器。请注意,诸如 http://localhost
的前缀也将匹配 http://localhost.other.com
或 http://localhost@other.com
。建议使用 /
终止完整主机名。
实现传输适配器的许多细节超出了本说明文档的范围,但请参阅下一个示例以了解简单的 SSL 用例。除此之外,您可能需要对 BaseAdapter
进行子类化。
示例:特定 SSL 版本¶
Requests 团队已做出特定选择,即使用底层库(urllib3)中的默认 SSL 版本。通常情况下,这是可以的,但有时您可能会发现自己需要连接到使用与默认版本不兼容的版本的 service-endpoint。
您可以通过采用 HTTPAdapter 的大部分现有实现并添加一个参数 ssl_version(该参数会传递到 urllib3)来使用传输适配器实现此目的。我们将创建一个传输适配器,指示库使用 SSLv3
import ssl
from urllib3.poolmanager import PoolManager
from requests.adapters import HTTPAdapter
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 不会重试失败的连接。但是,可以使用功能强大的功能数组(包括回退)在 Requests Session
中实现自动重试,方法是使用 urllib3.util.Retry 类
from urllib3.util import Retry
from requests import Session
from requests.adapters import HTTPAdapter
s = Session()
retries = Retry(
total=3,
backoff_factor=0.1,
status_forcelist=[502, 503, 504],
allowed_methods={'POST'},
)
s.mount('https://', HTTPAdapter(max_retries=retries))
阻塞还是非阻塞?¶
在默认传输适配器就位的情况下,Requests 不提供任何类型的非阻塞 IO。 Response.content
属性将阻塞,直到整个响应下载完毕。如果您需要更细粒度,则库的流式传输功能(请参阅 流式传输请求)允许您一次检索较少数量的响应。但是,这些调用仍会阻塞。
如果您担心使用阻塞 IO,那么有很多项目将 Requests 与 Python 的异步框架之一结合在一起。一些优秀的示例包括 requests-threads、grequests、requests-futures 和 httpx。
标头排序¶
在不寻常的情况下,您可能希望按顺序提供标头。如果您将 OrderedDict
传递给 headers
关键字参数,这将为标头提供一个顺序。但是,将优先使用 Requests 使用的默认标头的顺序,这意味着如果您在 headers
关键字参数中覆盖默认标头,它们可能与该关键字参数中的其他标头相比显得无序。
如果这有问题,用户应考虑通过将 Session
对象的 Session.headers
设置为自定义 OrderedDict
来设置默认标头。该顺序将始终优先。
超时¶
对外部服务器的大多数请求都应附加超时,以防服务器未及时响应。默认情况下,除非显式设置超时值,否则请求不会超时。如果没有超时,您的代码可能会挂起数分钟或更长时间。
连接超时是 Requests 将等待您的客户端与远程计算机建立连接的秒数(对应于套接字上的 connect() 调用)。将连接超时设置为略大于 3 的倍数是一种很好的做法,这是默认的 TCP 数据包重传窗口。
一旦您的客户端连接到服务器并发送 HTTP 请求,读取超时就是客户端将等待服务器发送响应的秒数。(具体来说,这是客户端在服务器发送的字节之间等待的秒数。在 99.9% 的情况下,这是服务器发送第一个字节之前的时间)。
如果您为超时指定一个值,如下所示
r = requests.get('https://github.com', timeout=5)
超时值将应用于 connect
和 read
超时。如果您想分别设置这些值,请指定一个元组
r = requests.get('https://github.com', timeout=(3.05, 27))
如果远程服务器非常慢,您可以通过将 None 作为超时值传递并取一杯咖啡来告诉 Requests 无限期地等待响应。
r = requests.get('https://github.com', timeout=None)
注意
连接超时适用于对 IP 地址的每次连接尝试。如果某个域名存在多个地址,底层的 urllib3
将按顺序尝试每个地址,直到成功连接一个地址。这可能导致实际总连接超时倍数长于指定的时间,例如,同时具有 IPv4 和 IPv6 地址的无响应服务器的感知超时将加倍,因此在设置连接超时时要考虑这一点。
注意
连接超时和读取超时都不是 时钟。这意味着如果您启动一个请求,然后查看时间,然后在请求完成或超时时查看时间,实际时间可能大于您指定的时间。