Requests 库是 Python 中用于创建 HTTP 请求的首选包。它通过直观的 API 简化了创建请求的复杂性。虽然 Requests 并非 Python 标准库的一部分,但使用它来执行 GET、POST 等 HTTP 操作仍然值得推荐。

本教程将演示如何使用标头和数据自定义请求、处理响应、进行身份验证以及使用会话和重试来优化性能。

1] 开始使用 Python 的 Requests 库

尽管 Requests 库是许多 Python 开发人员的常用工具,但它并未包含在 Python 的标准库中。这样一来,Requests 库就可以作为一个独立的项目继续更自由地发展。

Tips:如果只想使用 Python 的标准库发出 HTTP 请求,那么 Python 的 urllib.request 是一个不错的选择。

由于 Requests 是第三方库,因此需要先安装它才能在代码中使用。我们可以将 Requests 软件包安装到虚拟环境中,如果在多个项目中使用它,也可以选择将 Requests 安装到全局环境中。

无论是否在虚拟环境中工作,都可以使用如下命令安装 Requests 库:

# python -m pip install requests

使用 pip 安装完 Requests 后,即可在应用程序中使用它。导入 Requests 的步骤如下:

import requests

现在一切就绪,是时候开始 Requests 之旅了。我们的第一个目标是发出 GET 请求。

2] 生成 GET 请求

HTTP 方法(例如 GET 和 POST)指定了发出 HTTP 请求时要执行的操作。除了 GET 和 POST 之外,本教程后面还会用到其他一些常用方法。

GET 是最常用的 HTTP 方法之一,它用于从指定资源检索数据。要使用 Requests 发出 GET 请求,可以调用 requests.get() 函数来实现。

要尝试此操作,例如我们可以通过调用 get() 并传入以下 URL 向 GitHub 的 REST API 发出 GET 请求:

>>> import requests
>>> requests.get("https://api.github.com")
<Response [200]>

当收到这个响应时,恭喜!我们已经成功发出了第一个请求。现在,将更深入地了解该请求的响应。

3] 检查响应

现在定义一个 Response 为包含请求结果的对象。现在再次发出相同的请求,这次将返回值存储在变量中,以便我们可以更仔细地查看其属性和行为:

>>> import requests
>>> response = requests.get("https://api.github.com")

在这个例子中,我们收集了 request.get() 的返回值。它是一个 Response 实例,并将其存储在一个名为 respond 的变量中。现在,我们就可以使用 respond 来查看有关 GET 请求结果的大量信息。

4] 使用状态代码

我们可以从 Response 中收集的第一条信息是状态码。状态码会告知请求的状态。

例如,200 OK 状态码表示请求成功,而 404 NOT FOUND 状态码表示未找到想要查找的资源。还有许多其他可能的状态码,可以让我们具体了解请求的进展情况。

通过访问 .status_code,就可以查看服务器返回的状态码:

>>> response.status_code
200

.status_code 返回 200,这意味着我们的请求成功,服务器已响应我们发出的请求并返回了数据。

有时,可以使用此信息在代码中做出判断:

if response.status_code == 200:
    print("Success !")
elif response.status_code == 404:
    print("Not Found .")

根据条件判断,如果服务器返回 200 状态码,则程序将打印”Success !”。如果返回 404,则程序将打印”Not Found .”。

Requests 进一步简化了此过程。如果在布尔上下文(例如条件语句)中使用 Response 实例,则当状态码小于 400 时,它将被评估为 True,否则为 False。

这意味着我们可以通过重写 if 语句来修改上一个示例:

if response:
    print("Success!")
else:
    raise Exception(f"Non-success status code: {response.status_code}")

在上面的代码片段中,我们隐式检查响应的 .status_code 是否在 200 到 399 之间。如果不是,则引发异常,并显示一条错误消息,其中包含包裹在 f 字符串中的非成功状态代码。

注意:此真值测试之所以可行,是因为 .__bool__() 是 Response 的一个重载方法。这意味着 Response 的适配默认行为在确定对象的真值时会考虑状态码。

请记住,此方法不会验证状态码是否等于 200。这是因为 200 到 399 范围内的其他状态码,例如 204 NO CONTENT 和 304 NOT MODIFIED,也被视为成功,因为它们提供了一些可行的响应。

例如,状态码 204 表示响应成功,但消息正文中没有返回任何内容。

因此,请确保仅在需要了解请求是否总体成功时才使用这种便捷的简写方式。然后,如有必要,我们则需要根据确定的状态码适当地处理响应。

假设不想在 if 语句中检查响应的状态码,相反,我们想使用 Request 的内置功能在请求失败时引发异常。则可以使用 .raise_for_status() 来实现:

import requests
from requests.exceptions import HTTPError

URLS = ["https://api.github.com", "https://api.github.com/invalid"]

for url in URLS:
    try:
        response = requests.get(url)
        response.raise_for_status()
    except HTTPError as http_err:
        print(f"HTTP error occurred: {http_err}")
    except Exception as err:
        print(f"Other error occurred: {err}")
    else:
        print("Success!")

如果调用 .raise_for_status() 函数,Requests 会针对 400 到 600 之间的状态码抛出 HTTPError。如果状态码指示请求成功,程序将继续运行而不会抛出该异常。

现在,我们已经了解了很多如何处理从服务器返回的响应状态码的知识。但是,当发出 GET 请求时,我们很少只关心响应的状态码。通常,我们希望看到更多信息。接下来,继续学习如何查看服务器在响应正文中返回的实际数据。

5] 访问响应内容

GET 请求的响应通常在消息正文中包含一些有价值的信息,称为有效负载。使用 Response 的属性和方法,我们可以以多种格式查看有效负载。

要以字节为单位查看响应的内容,则使用 .content 属性:

>>> import requests

>>> response = requests.get("https://api.github.com")
>>> response.content
b'{"current_user_url":"https://api.github.com/user", ...}'

>>> type(response.content)
<class 'bytes'>

虽然 .content 允许我们访问响应有效负载的原始字节,但通常使用 UTF-8 等字符编码将它们转换为字符串。当访问 .text 属性时,response 将为我们执行此操作:

>>> response.text
{
  "current_user_url": "https://api.github.com/user",
  "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
  "authorizations_url": "https://api.github.com/authorizations",
  "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
  "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
  "emails_url": "https://api.github.com/user/emails",
  "emojis_url": "https://api.github.com/emojis",
  "events_url": "https://api.github.com/events",
  "feeds_url": "https://api.github.com/feeds",
  "followers_url": "https://api.github.com/user/followers",
  "following_url": "https://api.github.com/user/following{/target}",
  "gists_url": "https://api.github.com/gists{/gist_id}",
  "hub_url": "https://api.github.com/hub",
  "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
  "issues_url": "https://api.github.com/issues",
  "keys_url": "https://api.github.com/user/keys",
  "label_search_url": "https://api.github.com/search/labels?q={query}&repository_id={repository_id}{&page,per_page}",
  "notifications_url": "https://api.github.com/notifications",
  "organization_url": "https://api.github.com/orgs/{org}",
  "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
  "organization_teams_url": "https://api.github.com/orgs/{org}/teams",
  "public_gists_url": "https://api.github.com/gists/public",
  "rate_limit_url": "https://api.github.com/rate_limit",
  "repository_url": "https://api.github.com/repos/{owner}/{repo}",
  "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
  "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
  "starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
  "starred_gists_url": "https://api.github.com/gists/starred",
  "topic_search_url": "https://api.github.com/search/topics?q={query}{&page,per_page}",
  "user_url": "https://api.github.com/users/{user}",
  "user_organizations_url": "https://api.github.com/user/orgs",
  "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
  "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"
}

>>> type(response.text)
<class 'str'>

由于将字节解码为字符串需要编码方案,因此,如果未指定编码方案,Requests 将尝试根据响应标头猜测编码。我们同样可以在访问 .text 之前设置 .encoding 来指定显式编码:

>>> response.encoding = "utf-8"  # Optional: Requests infers this.
>>> response.text
'{"current_user_url":"https://api.github.com/user", ...}'

如果仔细查看响应,就会发现它实际上是序列化的 JSON 内容。要获取字典,你可以从 .text 中获取 str,然后使用 json.loads() 对其进行反序列化。不过,完成此任务的直接方法是使用 .json():

>>> response.json()
{'current_user_url': 'https://api.github.com/user', ...}

>>> type(response.json())
<class 'dict'>

.json() 的返回值类型是字典,因此我们可以通过键来访问对象中的值:

>>> response_dict = response.json()
>>> response_dict["emojis_url"]
'https://api.github.com/emojis'

状态码和消息体可以做很多事情。但如果需要更多信息,例如响应本身的元数据,则需要查看响应的标头。

6] 查看响应标头

响应标头可以提供有用的信息,例如响应有效负载的内容类型以及响应的缓存时长。要查看这些标头,请访问 .headers:

>>> import requests

>>> response = requests.get("https://api.github.com")
>>> response.headers
{'Server': 'github.com',
...
'X-GitHub-Request-Id': 'AE83:3F40:2151C46:438A840:65C38178'}

.headers 属性返回一个字典类型的对象,允许我们通过键来访问标头值。例如,要查看响应有效负载的内容类型,就可以访问”Content-Type”:

>>> response.headers["Content-Type"]
'application/json; charset=utf-8'

这个类似字典的 headers 对象有一些特殊之处。HTTP 规范将 headers 定义为不区分大小写的,这意味着我们可以可以访问它们而不必关心它们的大小写,下面的代码同样有效:

>>> response.headers["content-type"]
'application/json; charset=utf-8'

无论是使用”content-type”,还是使用”Content-Type”键,都会获得相同的值。

现在我们已经了解了 Response 中最有用的属性和方法,并且对 Requests 的基本用法有了很好的了解。也可以从互联网获取内容并处理收到的响应。

但互联网不仅仅是简单的 URL。之后,我们将自定义 GET 请求以考虑查询字符串参数时,也可以查看响应会如何变化。

7] 添加查询字符串参数

自定义 GET 请求的一种常见方法是通过 URL 中的查询字符串参数传递值。要使用 get() 来实现这一点,就需要将数据传递给 params。例如,以下代码可以使用 GitHub 的仓库搜索 API 来查找热门的 Python 仓库:

import requests

response = requests.get(
    "https://api.github.com/search/repositories",
    params={"q": "language:python", "sort": "stars", "order": "desc"},
)

json_response = response.json()
popular_repositories = json_response["items"]
for repo in popular_repositories[:3]:
    print(f"Name: {repo['name']}")
    print(f"Description: {repo['description']}")
    print(f"Stars: {repo['stargazers_count']}\n")

通过将字典传递给 get() 的 params 参数,就可以修改搜索 API 返回的结果。

也可以将 params 作为字典(如刚才所示)或元组列表传递给 get():

>>> import requests

>>> requests.get(
...     "https://api.github.com/search/repositories",
...     [("q", "language:python"), ("sort", "stars"), ("order", "desc")],
... )

甚至可以将值作为字节传递:

>>> requests.get(
...     "https://api.github.com/search/repositories",
...     params=b"q=language:python&sort=stars&order=desc",
... )

查询字符串对于参数化 GET 请求非常有用。另一种自定义请求的方法是添加或修改发送的标头。

8] 自定义请求标头

要自定义标头,可以使用 headers 参数将 HTTP 标头字典传递给 get() 。例如,我们可以更改之前的搜索请求,通过在 Accept 标头中指定 text-match 媒体类型,使其在结果中突出显示匹配的搜索词:

import requests

response = requests.get(
    "https://api.github.com/search/repositories",
    params={"q": '"python"'},
    headers={"Accept": "application/vnd.github.text-match+json"},
)

json_response = response.json()
first_repository = json_response["items"][0]
print(first_repository["text_matches"][0]["matches"])

Accept 标头告知服务器应用程序可以处理哪些内容类型。在本例中,由于我们希望突出显示匹配的搜索词,因此使用了标头值”application/vnd.github.text-match+json”。这是一个专有的 GitHub Accept 标头,其内容采用特殊的 JSON 格式。

运行此 Python 脚本时,将得到以下所示的结果:

[{'text': 'Python', 'indices': [0, 6]}]

在了解更多自定义请求的方法之前,我们可以通过了解其他 HTTP 方法来拓宽视野。

9] 使用其他 HTTP 方法

除了 GET 之外,其他常用的 HTTP 方法包括 POST、PUT、DELETE、HEAD、PATCH 和 OPTIONS。对于每种 HTTP 方法,Requests 都提供了一个与 get() 签名类似的函数。

注意:要尝试这些 HTTP 方法,我们可以向 httpbin.org 发出对应的请求。httpbin 服务是由 Requests 的原作者 Kenneth Reitz 创建的一项重要资源。该服务接受测试请求并返回与请求相关的数据。

以下是 Requests 库对于各种 HTTP 方法的示例,可以看出,Requests 为各种方法提供了类似的操作方式:

>>> import requests
<Response [200]>
>>> requests.get("https://httpbin.org/get")
<Response [200]>
>>> requests.post("https://httpbin.org/post", data={"key": "value"})
<Response [200]>
>>> requests.put("https://httpbin.org/put", data={"key": "value"})
<Response [200]>
>>> requests.delete("https://httpbin.org/delete")
<Response [200]>
>>> requests.head("https://httpbin.org/get")
<Response [200]>
>>> requests.patch("https://httpbin.org/patch", data={"key": "value"})
<Response [200]>
>>> requests.options("https://httpbin.org/get")
<Response [200]>

在上面的示例中,我们调用了每个函数,使用相应的 HTTP 方法向 httpbin 服务发出请求。

所有这些函数都是 requests.request() 的高级快捷方式,它将方法名称作为其第一个参数:

>>> requests.request("GET", "https://httpbin.org/get")
<Response [200]>

我们可以使用等效的低级函数调用,但 Python 的 Requests 库的强大之处在于其人性化的高级接口。我们可以像以前一样检查响应:

>>> response = requests.head("https://httpbin.org/get")
>>> response.headers["Content-Type"]
'application/json'

>>> response = requests.delete("https://httpbin.org/delete")
>>> json_response = response.json()
>>> json_response["args"]
{}

无论使用哪种方法,都会获得一个 Response 对象,该对象提供对标头、响应主体、状态代码等属性用于访问相应内容。

接下来,我们将仔细讲解 POST、PUT 和 PATCH 方法,并了解它们与其他请求类型的区别。

10] 发送请求数据

根据 HTTP 规范,POST、PUT 以及不太常见的 PATCH 请求会通过消息体传递数据,而不是通过查询字符串中的参数。使用 Requests,就可以将此有效负载传递给相应函数的 data 参数。

data 参数可以接受字典、元组列表、字节或类似文件的对象。我们需要根据与之交互的服务的具体需求来调整在请求体中发送的数据。

例如,如果请求的内容类型为”application/x-www-form-urlencoded”,则可以将表单数据作为字典发送:

>>> import requests

>>> requests.post("https://httpbin.org/post", data={"key": "value"})
<Response [200]>

也可以将相同的数据作为元组列表发送:

>>> requests.post("https://httpbin.org/post", data=[("key", "value")])
<Response [200]>

如果需要发送 JSON 数据,可以使用 json 参数。当我们通过 json 参数传递 JSON 数据时,Requests 会序列化传递的数据并为自动添加正确的 Content-Type 标头。

正如之前所了解的,httpbin 服务会接受测试请求并返回与请求相关的数据。例如,我们可以使用它来检查基本的 POST 请求:

>>> response = requests.post("https://httpbin.org/post", json={"key": "value"})
>>> json_response = response.json()
>>> json_response["data"]
'{"key": "value"}'
>>> json_response["headers"]["Content-Type"]
'application/json'

从响应中可以看出,服务器收到了我们发送的请求数据和标头。Requests 还会以 PreparationRequest 的形式提供这些信息,下面会更详细地介绍。

11] 预检查请求

当发出请求时,Requests 库会在实际将其发送到目标服务器之前进行准备。请求准备包括验证标头和序列化 JSON 内容等操作。

我们可以通过访问 Response 对象上的 .request 来查看 PreparationedRequest 对象:

>>> import requests

>>> response = requests.post("https://httpbin.org/post", json={"key":"value"})

>>> response.request
<PreparedRequest [POST]>

>>> response.request.headers["Content-Type"]
'application/json'

>>> response.request.url
'https://httpbin.org/post'

>>> response.request.body
b'{"key": "value"}'

检查 PreparedRequest 可以让我们访问有关正在发出的请求的各种信息,例如有效负载、URL、标头、身份验证等等。

到目前为止,我们已经发出了许多不同类型的请求,但它们都有一个共同点:它们都是针对公共 API 的未经身份验证的请求。以下,我们将尝如何处理遇到的许多服务都要求以某种方式进行身份验证的情况。

12] 使用身份验证

身份验证可以帮助服务了解访客的身份。通常,我们可以通过 Authorization 标头或服务定义的自定义标头传递数据,从而向服务器提供凭证。到目前为止,我们看到的 Requests 中的所有函数都提供了一个名为 auth 的参数,它允许直接传递凭证:

>>> import requests

>>> response = requests.get(
...     "https://httpbin.org/basic-auth/user/passwd",
...     auth=("user", "passwd")
... )

>>> response.status_code
200
>>> response.request.headers["Authorization"]
'Basic dXNlcjpwYXNzd2Q='

如果在元组中传递给 auth 的凭据有效,则请求成功。

当我们将凭据以元组形式传递给 auth 参数时,Requests 会在底层使用 HTTP 的基本访问身份验证方案来应用这些凭据。

现在需要想知道 Requests 设置为 Authorization 标头值的字符串”Basic dXNlcjpwYXNzd2Q=”的来源。简而言之,它是用户名和密码的 Base64 编码字符串,前缀为”Basic”:

首先,Requests 将请求提供的用户名和密码组合起来,并在它们之间插入一个冒号。因此,对于用户名”user”和密码”passwd”,组合后的字符串会变成”user:passwd”。

然后,Requests 使用 base64.b64encode() 将此字符串进行 Base64 编码。该编码将”user:passwd”字符串转换为”dXNlcjpwYXNzd2Q=”。

最后,Requests 会在此 Base64 字符串前面添加”Basic”。

这就是上例中 Authorization 标头的最终值变成 Basic dXNlcjpwYXNzd2Q= 的原因。

需要注意的是,HTTP 基本身份验证 (BA) 本身并不十分安全,因为任何人都可以解码 Base64 字符串来获取其中的凭据。因此,务必始终通过 HTTPS 发送这些请求,因为 HTTPS 会加密整个请求并提供额外的保护。

我们也可以通过使用 HTTPBasicAuth 传递显式基本身份验证凭据来发出相同的请求:

>>> from requests.auth import HTTPBasicAuth
>>> requests.get(
...     "https://httpbin.org/basic-auth/user/passwd",
...     auth=HTTPBasicAuth("user", "passwd")
... )
<Response [200]>

虽然基本身份验证无需明确指定,但我们可能希望使用其他方法进行身份验证。Requests 提供了其他开箱即用的身份验证方法,例如 HTTPDigestAuth 和 HTTPProxyAuth。

GitHub 的已验证用户 API 就是一个需要身份验证的实际 API 示例。此端点提供有关已验证用户个人资料的信息。

如果尝试在不使用凭据的情况下发出请求,则会看到状态码为 401 Unauthorized:

>>> requests.get("https://api.github.com/user")
<Response [401]>

如果我们在访问需要身份验证凭据的服务时未提供,则会收到 HTTP 错误代码作为响应。

要向 GitHub 的身份验证用户 API 发出请求,首先需要生成一个具有 read:user 作用域的个人访问令牌。然后,就可以将此令牌作为元组中的第二个元素传递给 get():

>>> import requests

>>> token = ""
>>> response = requests.get(
...     "https://api.github.com/user",
...     auth=("", token)
... )
>>> response.status_code
200

正如我们之前提到的,这种方法将凭据传递给 HTTPBasicAuth,后者需要用户名和密码,并将凭据作为带有前缀”Basic”的 Base64 编码字符串发送:

>>> response.request.headers["Authorization"]
'Basic OmdocF92dkd...WpremM0SGRuUGY='

这种方法虽然有效,但并非使用 Bearer 令牌进行身份验证的正确方法——而且使用空字符串输入多余的用户名也显得很别扭。

我们于是就可以使用 Requests 提供自己的身份验证机制来解决这个问题。要尝试一下,请创建 AuthBase 的子类并实现 .__call__() 方法:

from requests.auth import AuthBase

class TokenAuth(AuthBase):
    """Implements a token authentication scheme."""

    def __init__(self, token):
        self.token = token

    def __call__(self, request):
        """Attach an API token to the Authorization header."""
        request.headers["Authorization"] = f"Bearer {self.token}"
        return request

在这里,我们自定义的 TokenAuth 机制会接收一个令牌,然后将该令牌包含在请求的 Authorization 标头中,同时将建议的”Bearer”前缀设置为该字符串。

之后,现在可以使用此自定义令牌身份验证来调用 GitHub 的已验证用户 API:

>>> import requests
>>> from custom_token_auth import TokenAuth

>>> token = ""
>>> response = requests.get(
...     "https://api.github.com/user",
...     auth=TokenAuth(token)
... )

>>> response.status_code
200
>>> response.request.headers["Authorization"]
'Bearer ghp_b...Tx'

我们自定义的 TokenAuth 为 Authorization 标头创建了一个格式良好的字符串。这为我们提供了一种更直观、更可重用的方式来处理基于令牌的身份验证方案,例如 GitHub API 部分所需的方案。

注意:虽然可以在自定义身份验证类之外构建身份验证字符串并将其直接通过标头传递,但不建议这样做,因为它可能会导致意外行为。

当我们尝试直接使用标头设置身份验证凭据时,Requests 可能会在内部覆盖我们的输入。例如,如果有一个提供身份验证凭据的 .netrc 文件,就会发生这种情况。如果没有使用 auth 提供身份验证方法,Requests 将尝试从 .netrc 文件获取凭据。

不良的身份验证机制可能导致安全漏洞。除非服务出于某种原因需要自定义身份验证机制,否则最好坚持使用可靠的方法,例如内置的基本身份验证或 OAuth(例如,通过 Requests-OAuthlib)。

考虑安全性时,就需要使用请求处理 TLS/SSL 证书。

13] 与服务器安全通信

每当要发送或接收的数据属于敏感数据时,安全性就至关重要。通过 HTTP 与安全站点通信的方式是使用传输层安全性 (TLS) 建立加密连接。TLS 是安全套接字层 (SSL) 的后继者,可在安全通信中提供增强的安全性和效率。然而,程序员仍然经常使用 SSL 而不是 TLS。

Requests 默认会为我们验证服务器的数字证书,于是我们也很少需要对此行为进行调整。但是,在某些情况下,我们可能需要自定义验证过程。

例如,当我们在具有自定义证书颁发机构的公司环境中工作时,就可能需要提供自己的证书包:

>>> import requests

>>> requests.get(
...     "https://internal-api.company.com",
...     verify="/path/to/company-ca.pem"
... )
<Response [200]>

如果需要信任多个自定义证书颁发机构,也可以提供一个包含证书文件的目录。

如果在开发过程中调试证书问题,那么可能会想完全禁用证书验证。虽然以下这种方法有效,但它会带来严重的安全风险,因此切勿在生产环境中使用这种方法:

>>> requests.get("https://api.github.com", verify=False)
InsecureRequestWarning: Unverified HTTPS request is being made to host
⮑ 'api.github.com'. Adding certificate verification is strongly advised.
⮑ See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
⮑  warnings.warn(
<Response [200]>

Requests 会警告这种危险的做法,因为禁用验证会容易受到中间人攻击。

注意:Requests 使用一个名为 certifi 的软件包来提供证书颁发机构。这让 Requests 知道它可以信任哪些颁发机构。为了尽可能保证连接的安全,应该定期更新 certifi。

既然已经了解如何安全地处理证书验证,我们可能也想知道如何让程序高效运行。

14] 性能优化

使用 Requests 时,尤其是在生产应用环境中,务必考虑其性能影响。超时控制、会话和重试限制等功能可以帮助保持应用程序平稳运行。

注意:Requests 不直接支持异步 HTTP 请求。如果程序需要异步支持,可以尝试 AIOHTTP 或 HTTPX。后者库与 Requests 的语法广泛兼容。

即使在同步代码中,我们同样也可以使用一些重要的技巧来优化 HTTP 请求并避免性能瓶颈。

设置请求超时

当我们向外部服务发出请求时,如果系统必须等待响应才能继续处理。假如应用程序等待响应的时间过长,则服务请求可能会阻塞,用户体验可能会受到影响,或者后台作业可能会挂起。

默认情况下,请求会无限期地等待响应,因此应该始终指定超时时长,以防止这些问题发生。要设置请求的超时时间,则使用 timeout 参数。timeout 可以是整数或浮点数,表示超时前等待响应的秒数:

>>> requests.get("https://api.github.com", timeout=1)
<Response [200]>

>>> requests.get("https://api.github.com", timeout=0.01)
Traceback (most recent call last):
  ...
requests.exceptions.ConnectTimeout:
⮑ HTTPSConnectionPool(host='api.github.com', port=443):
⮑ Max retries exceeded with url: / (Caused by ConnectTimeoutError(...))

第一个请求成功完成,因为服务器在一秒的超时限制内做出了响应。相比之下,第二个请求失败了,因为超时时间设置为极短的十毫秒,在建立连接之前就已经超时。

我们还可以将包含以下两个元素的元组传递给 timeout 函数:

  1. Connect timeout:允许客户端与服务器建立连接的时间
  2. Read timeout:客户端建立连接后等待响应的时间

这两个元素都应该是数字,可以是 int 或 float 类型:

>>> requests.get("https://api.github.com", timeout=(3.05, 5))
<Response [200]>

如果请求在 3.05 秒内建立连接,并在连接建立后 5 秒内收到数据,则响应将按原样返回。如果请求超时,则该函数将引发 ConnectTimeout 或 ReadTimeout 异常,它们是更通用的 Timeout 异常的子类:

import requests
from requests.exceptions import Timeout

try:
    response = requests.get("https://api.github.com", timeout=(3.05, 5))
except Timeout:
    print("The request timed out")
else:
    print("The request did not time out")

使用 Session 对象重用连接

到目前为止,我们一直在处理诸如 get() 和 post() 之类的高级请求 API。这些函数是对发出请求时所发生事件的抽象。它们隐藏了实现细节,例如如何管理连接,因此无需担心这些细节。

在这些抽象之下是一个名为 Session 的类。如果需要微调对请求方式的控制或提高请求的性能,则可能需要直接使用 Session 实例。

Session 用于在多个请求之间持久化参数。例如,如果想在多个请求中使用相同的身份验证,则可以使用 Session:

import requests
from custom_token_auth import TokenAuth

TOKEN = "<YOUR_GITHUB_PA_TOKEN>"

with requests.Session() as session:
    session.auth = TokenAuth(TOKEN)
    first_response = session.get("https://api.github.com/user")
    second_response = session.get("https://api.github.com/user")

print(first_response.headers)
print(second_response.json())

在此示例中,我们使用上下文管理器来确保会话在不再需要资源时释放它们。

在第 7 行中,我们使用自定义的 TokenAuth 将 GitHub 用户的凭据附加到会话对象。每个会话只需执行一次此操作,然后就可以发出多个经过身份验证的请求。只要会话持续,请求就会保留凭据。

在第 8 行和第 9 行中,继续使用 session.get() 而不是 request.get() 向经过身份验证的用户 API 发出两个请求。

会话的主要性能优化体现在持久连接上。当我们的应用使用 Session 连接到服务器时,它会将该连接保留在连接池中。当应用想要再次连接到同一服务器时,它将重用池中的连接,而不是建立新的连接。

重试失败请求

当请求失败时,可能希望应用程序重试相同的请求。但是,Requests 默认不会为我们执行此操作。要应用此功能,就需要实现一个自定义传输适配器。

传输适配器允许我们为与之交互的每个服务定义一组配置。例如,假设希望所有发送至 https://api.github.com 的请求在最终引发 RetryError 之前重试两次。在这种情况下,就需要构建一个传输适配器,设置其 max_retries 参数,并将其挂载到现有的 Session 中:

import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import RetryError
from urllib3.util.retry import Retry

retry_strategy = Retry(
    total=2,
    status_forcelist=[429, 500, 502, 503, 504]
)
github_adapter = HTTPAdapter(max_retries=retry_strategy)

with requests.Session() as session:
    session.mount("https://api.github.com", github_adapter)
    try:
        response = session.get("https://api.github.com/")
    except RetryError as err:
        print(f"Error: {err}")

在此示例中,我们已设置会话,以便在对 GitHub API 的请求未按预期工作时,最多重试两次。Retry 对象可让我们精细控制哪些状态代码应触发重试。

max_retries 参数可以接受整数或 Retry 对象。使用 Retry 对象可以详细控制哪些失败需要重试。虽然在基本情况下仍然可以传递一个简单的整数,但使用 Retry 对象是更现代的方法。

当我们将 HTTPAdapter 挂载到会话时,会话将遵循其对每个 https://api.github.com 请求的配置。

以上就是关于 Requests 库的全部内容,因为学习了如何使用 Requests,所以就已经具备了探索广阔的 Web 服务世界的能力,并使用它们提供的引人入胜的数据构建出色的应用程序。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注