高级用法¶
本文档涵盖了 Requests 的一些更高级的功能。
会话对象¶
Session 对象允许您在请求之间保留某些参数。它还会在从 Session 实例发出的所有请求中保留 cookie,并将使用 的urllib3
连接池。因此,如果您向同一主机发出多个请求,则底层 TCP 连接将被重用,这可以显著提高性能(请参阅HTTP 持久连接)。
Session 对象具有主要 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"}}'
会话还可用于向请求方法提供默认数据。这是通过向 Session 对象的属性提供数据来实现的:
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,而不会发送第二个请求的 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
对象,该对象将被发送到服务器以请求或查询某些资源。其次,Response
一旦 Requests 从服务器获得响应,就会生成一个对象。该Response
对象包含服务器返回的所有信息,还包含Request
您最初创建的对象。这是一个从 Wikipedia 的服务器获取一些非常重要的信息的简单请求:
>>> 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'}
准备好的请求¶
每当您Response
从 API 调用或 Session 调用收到对象时,该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
-level 状态(例如 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)
当您使用准备好的请求流时,请记住它不考虑环境。如果您使用环境变量来更改请求的行为,这可能会导致问题。例如:在中指定的自签名 SSL 证书REQUESTS_CA_BUNDLE
将不予考虑。因此会抛出。您可以通过将环境设置明确合并到您的会话中来避免此行为: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 会像 Web 浏览器一样验证 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 确实有:
>>> requests.get('https://github.com')
<Response [200]>
您可以将verify
路径传递给具有受信任 CA 的证书的 CA_BUNDLE 文件或目录:
>>> requests.get('https://github.com', verify='/path/to/certfile')
或持续:
s = requests.Session()
s.verify = '/path/to/certfile'
笔记
如果verify
设置为目录路径,则必须使用c_rehash
OpenSSL 提供的实用程序处理该目录。
此受信任 CA 列表也可以通过REQUESTS_CA_BUNDLE
环境变量指定。如果REQUESTS_CA_BUNDLE
未设置,CURL_CA_BUNDLE
将用作后备。
verify
如果设置为 False,请求也可以忽略验证 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!
主体内容工作流程¶
默认情况下,当您发出请求时,会立即下载响应主体。您可以覆盖此行为并推迟下载响应主体,直到使用参数访问属性Response.content
为止stream
:
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
为,除非您使用完所有数据或调用,否则 Requests 无法将连接释放回池中。这可能会导致连接效率低下。如果您在使用时发现自己只读取了部分请求主体(或根本没有读取它们),则应在语句内发出请求以确保它始终处于关闭状态:True
Response.close
stream=True
with
with requests.get('https://httpbin.org/get', stream=True) as r:
# Do things with the response here.
活着¶
好消息——感谢 urllib3,会话中的 keep-alive 是 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
在请求中设置 ,在这种情况下,您可以通过使用 参数调用 来逐块迭代iter_content
。如果要设置块的最大大小,可以将参数设置chunk_size
为任何整数。None
chunk_size
POST 多个 Multipart-Encoded 文件¶
您可以在一个请求中发送多个文件。例如,假设您要将图像文件上传到具有多个文件字段“图像”的 HTML 表单:
<input type="file" name="images" multiple="true" required="true"/>
为此,只需将文件设置为以下元组列表:(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 有一个钩子系统,你可以使用它来操纵请求过程的各个部分,或者信号事件处理。
可用的钩子:
response
:从请求生成的响应。
您可以通过将字典传递给请求参数来根据每个请求分配钩子函数
:{hook_name: callback_function}
hooks
hooks={'response': print_url}
它将callback_function
接收一块数据作为其第一个参数。
def print_url(r, *args, **kwargs):
print(r.url)
您的回调函数必须处理自己的异常。任何未处理的异常都不会被默默传递,因此应由调用 Requests 的代码处理。
如果回调函数返回一个值,则假定它将替换传入的数据。如果该函数不返回任何内容,则不会影响其他任何内容。
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
钩子可以有多个,它们将按照添加的顺序被调用。
自定义身份验证¶
请求允许您指定自己的身份验证机制。
作为参数传递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
然后,我们可以使用 Pizza Auth 发出请求:
>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
<Response [200]>
流式请求¶
您Response.iter_lines()
可以轻松迭代流式 API,例如Twitter Streaming 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))
当将decrypt_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')
警告
设置的行为可能与预期不同。提供的值将被环境代理( urllib.request.getproxiessession.proxies
返回的值)覆盖。为了确保在存在环境代理的情况下使用代理,请在所有单个请求上明确指定参数,如上文最初所述。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 Basic Auth,请 在上述任何配置条目中使用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')
袜子¶
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 一致,它使用方案来决定是在客户端还是代理上进行 DNS 解析。如果您想在代理服务器上解析域,请使用方案socks5h
。
遵守¶
Requests 旨在遵守所有相关规范和 RFC,只要这种遵守不会给用户带来困难。对规范的这种关注可能会导致一些行为,对于那些不熟悉相关规范的人来说,这些行为可能看起来不寻常。
编码¶
当您收到响应时,Requests 会猜测在您访问属性时用于解码响应的编码Response.text
。Requests 将首先检查 HTTP 标头中的编码,如果没有,则使用
charset_normalizer
或chardet尝试猜测编码。
如果chardet
已安装,requests
则使用它,但对于 python3 来说,它
chardet
不再是强制依赖项。该chardet
库是 LGPL 许可的依赖项,并且请求的某些用户不能依赖强制 LGPL 许可的依赖项。
当您安装时requests
没有指定[use_chardet_on_py3]
额外内容,并且chardet
尚未安装,则requests
使用charset-normalizer
(MIT 许可)来猜测编码。
只有在 HTTP 标头中没有显式字符集且标Content-Type
头包含时,Requests 才会猜测编码。text
在这种情况下,RFC 2616规定默认字符集必须是ISO-8859-1
。在这种情况下,Requests 遵循规范。如果您需要不同的编码,您可以手动设置属性Response.encoding
,或使用原始Response.content
。
HTTP 动词¶
Requests 提供对几乎所有 HTTP 动词的访问:GET、OPTIONS、HEAD、POST、PUT、PATCH 和 DELETE。下面提供了使用 GitHub API 在 Requests 中使用这些不同动词的详细示例。
我们将从最常用的动词开始:GET。HTTP GET 是一种幂等方法,它从给定的 URL 返回资源。因此,它是您在尝试从 Web 位置检索数据时应该使用的动词。一个示例用法是尝试从 GitHub 获取有关特定提交的信息。假设我们想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 方法。不要担心,这些方法仍然可以与请求一起使用。它们使用内置方法.request
。例如:
>>> r = requests.request('MKCOL', url, data=data)
>>> r.status_code
200 # Assuming your call was correct
利用这一点,您可以使用服务器允许的任何方法动词。
链接标题¶
许多 HTTP API 都具有 Link 标头。它们使 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 附带一个传输适配器,即。此适配器使用强大的urllib3HTTPAdapter
库提供默认的 Requests 与 HTTP 和 HTTPS 交互。每当初始化 Requests 时,其中一个适配器都会附加到HTTP 对象,另一个适配器会附加到 HTTPS 对象。Session
Session
Requests 允许用户创建并使用自己的传输适配器来提供特定功能。创建后,传输适配器可以安装到 Session 对象,同时指示应将其应用于哪些 Web 服务。
>>> s = requests.Session()
>>> s.mount('https://github.com/', MyAdapter())
挂载调用将传输适配器的特定实例注册到前缀。挂载后,使用该会话发出的任何 HTTP 请求(其 URL 以给定前缀开头)都将使用给定的传输适配器。
笔记
将根据最长前缀匹配来选择适配器。请注意前缀如http://localhost
也将匹配http://localhost.other.com
或http://localhost@other.com
。建议使用 来终止完整主机名/
。
实现传输适配器的许多细节超出了本文档的范围,但请查看下一个简单 SSL 用例示例。要了解更多信息,您可以考虑对 进行子类化
BaseAdapter
。
示例:特定 SSL 版本¶
Requests 团队已做出特定选择,即使用底层库 ( urllib3 ) 中默认的 SSL 版本。通常情况下,这是可以的,但有时,您可能会发现自己需要连接到使用与默认版本不兼容的版本的服务端点。
您可以使用传输适配器来实现这一点,方法是采用 HTTPAdapter 的大部分现有实现,并添加一个传递给urllib3 的参数ssl_version。我们将创建一个传输适配器,指示库使用 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 不会重试失败的连接。但是,可以Session
使用
urllib3.util.Retry类在 Requests 中实现具有一系列强大功能的自动重试,包括退避:
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 的异步框架之一相结合。一些很好的例子是request-threads、grequests、request-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 地址的无响应服务器的感知超时时间将加倍,因此在设置连接超时时要考虑到这一点。
笔记
连接超时和读取超时都不是挂钟。这意味着如果您启动请求并查看时间,然后查看请求完成或超时的时间,则实际时间可能大于您指定的时间。