4、requests库入门:发送GET/POST请求、传递参数、处理响应、超时与重试
在Python生态中,requests库是处理HTTP请求的事实标准。它以其简洁的API、直观的语义和强大的功能,极大地降低了网络编程的门槛。无论是调用RESTful API、爬取网页数据,还是对接云服务,requests都是开发者工具箱中不可或缺的利器。本章将深入剖析requests库的核心用法,从最基础的GET/POST请求出发,逐步覆盖参数传递、响应对象解析、超时控制以及重试机制,帮助你建立一套稳健、高效的HTTP客户端开发方法论。
上图展示了requests库在一次完整HTTP事务中的核心链路。客户端通过requests构造请求,携带参数与请求头,发送至目标服务器;服务器处理完毕后返回响应对象,客户端再对响应进行解析。超时与重试机制贯穿于整个链路,是保障客户端健壮性的关键防线。下面我们将逐一拆解每个环节的具体实现与最佳实践。
4.1 发送GET与POST请求:核心原理与基础操作
核心原理:HTTP协议定义了多种请求方法,其中GET和POST是最常用的两种。GET请求用于获取资源,其参数通常附加在URL的查询字符串(Query String)中,具有幂等性(多次请求结果相同)。POST请求用于提交数据,其参数位于请求体(Request Body)中,常用于创建或修改资源,不具备幂等性。requests库将这两种方法封装为get()和post()函数,底层通过urllib3实现HTTP连接管理。
操作流程:
- 导入库:
import requests - 构造请求:调用
requests.get(url, params=None, headers=None, timeout=None)或requests.post(url, data=None, json=None, headers=None, timeout=None) - 发送请求:函数内部自动完成DNS解析、TCP连接、TLS握手(HTTPS)、发送请求头与请求体。
- 获取响应:返回一个
Response对象,包含服务器返回的所有信息。
案例拆解:假设我们需要从GitHub API获取用户信息,并创建一个新的Gist(代码片段)。
import requests
# GET 请求示例
url = "https://api.github.com/users/octocat"
resp = requests.get(url, timeout=5)
print(f"状态码: {resp.status_code}")
print(f"响应头 Content-Type: {resp.headers.get('Content-Type')}")
user_data = resp.json()
print(f"用户名: {user_data['login']}, 公开仓库数: {user_data['public_repos']}")
# POST 请求示例
post_url = "https://api.github.com/gists"
headers = {
"Authorization": " YOUR_PERSONAL_ACCESS_",
"Accept": "application/vnd.github+json"
}
payload = {
"description": "示例 Gist",
"public": True,
"files": {
"hello.py": {
"content": "print('Hello, requests!')"
}
}
}
resp_post = requests.post(post_url, json=payload, headers=headers, timeout=10)
if resp_post.status_code == 201:
print(f"Gist 创建成功: {resp_post.json()['html_url']}")
else:
print(f"创建失败: {resp_post.status_code} - {resp_post.text}")
常见误区:
- 混淆
data与json参数:data用于发送表单编码(application/x-www-form-urlencoded)或原始字节,json用于发送JSON序列化数据(application/json)。如果API期望JSON格式,使用data传递字典会导致服务器解析失败。 - 忽略响应状态码:仅检查
resp.text而不检查status_code,可能导致在4xx/5xx错误时仍继续解析无效数据。 - 硬编码URL参数:直接在URL字符串中拼接参数容易出错(如未转义特殊字符),应使用
params字典自动编码。
resp.request.url和resp.request.headers来确认实际发送的请求内容,这有助于排查参数编码或请求头缺失的问题。
4.2 传递参数:Query String、Form Data与JSON Body
核心原理:HTTP请求的参数传递位置决定了其编码方式。GET请求的参数放在URL的?之后,以key=value对形式存在,多个参数用&连接。POST请求的参数可以放在请求体中,常见编码格式有application/x-www-form-urlencoded(表单)、multipart/form-data(文件上传)和application/json(结构化数据)。requests库根据传入的参数类型自动设置Content-Type头。
操作流程:
- Query String参数:使用
params字典,requests会自动进行URL编码。 - Form Data参数:使用
data字典,编码为key=value&key2=value2。 - JSON Body参数:使用
json字典,自动序列化为JSON字符串并设置Content-Type: application/json。
案例拆解:以百度搜索为例,演示GET参数传递;以登录接口为例,演示POST表单与JSON两种方式。
import requests
# GET 请求传递 Query String
search_url = "https://www.baidu.com/s"
params = {"wd": "Python requests 教程", "ie": "utf-8"}
resp = requests.get(search_url, params=params, timeout=5)
print(f"最终请求URL: {resp.url}") # 观察自动编码后的URL
# POST 请求传递 Form Data
login_url = "https://httpbin.org/post"
form_data = {"username": "admin", "password": "123456"}
resp_form = requests.post(login_url, data=form_data, timeout=5)
print(f"Form 提交结果: {resp_form.json()['form']}")
# POST 请求传递 JSON Body
json_data = {"username": "admin", "password": "123456", "remember": True}
resp_json = requests.post(login_url, json=json_data, timeout=5)
print(f"JSON 提交结果: {resp_json.json()['json']}")
常见误区:
- 参数值为列表时:如果
params中的某个键对应一个列表,requests会将其编码为多个同名参数(如tag=a&tag=b),但某些后端框架可能只接受逗号分隔或JSON格式,需根据API文档调整。 - 混合使用
data和json:两者同时传入时,json优先,data会被忽略。 - 忽略字符编码:对于非ASCII字符,
requests会自动进行UTF-8编码,但若服务器返回的Content-Type未指定字符集,可能导致乱码,需手动设置resp.encoding。
4.3 处理响应:状态码、响应头、文本与JSON解析
核心原理:Response对象封装了服务器返回的所有数据。其核心属性包括:status_code(HTTP状态码)、headers(响应头,大小写不敏感)、text(解码后的文本内容)、content(原始字节)、json()(将JSON文本解析为Python对象)、encoding(文本编码)、url(最终请求URL,跟随重定向后可能变化)、history(重定向历史列表)。
操作流程:
- 检查状态码:使用
resp.status_code或resp.ok(200-399为True)。 - 读取响应头:通过
resp.headers.get('Content-Type')等获取元信息。 - 解析内容:根据
Content-Type选择resp.text(HTML/XML)、resp.json()(JSON)或resp.content(二进制如图片、文件)。 - 异常处理:调用
resp.raise_for_status(),若状态码为4xx/5xx则抛出HTTPError异常。
案例拆解:下载一张图片并保存到本地,同时处理可能出现的HTTP错误。
import requests
img_url = "https://httpbin.org/image/png"
try:
resp = requests.get(img_url, timeout=10)
resp.raise_for_status() # 如果状态码不是200,抛出异常
print(f"图片类型: {resp.headers.get('Content-Type')}")
print(f"图片大小: {len(resp.content)} 字节")
with open("downloaded_image.png", "wb") as f:
f.write(resp.content)
print("图片下载成功")
except requests.exceptions.HTTPError as e:
print(f"HTTP错误: {e}")
except requests.exceptions.ConnectionError:
print("网络连接失败,请检查网络")
except requests.exceptions.Timeout:
print("请求超时")
常见误区:
- 直接调用
json()而不检查状态码:如果服务器返回500错误且响应体不是JSON,json()会抛出ValueError,应先检查状态码或使用try-except。 - 忽略
encoding属性:某些服务器返回的Content-Type头缺少charset参数,requests会使用默认的ISO-8859-1,导致中文乱码。此时应手动设置resp.encoding = 'utf-8'。 - 大文件下载直接使用
resp.content:对于几百MB的文件,一次性加载到内存可能导致内存溢出,应使用stream=True配合iter_content()分块写入。
4.4 超时与重试:构建稳健的请求策略
核心原理:网络请求可能因网络延迟、服务器过载或DNS解析失败而长时间无响应。设置timeout参数可以控制请求在指定时间内未完成则抛出异常。timeout可以是一个浮点数(连接和读取共用超时时间),也可以是一个元组(connect_timeout, read_timeout),分别控制连接阶段和读取阶段。重试机制则是在请求失败后自动重新发送请求,通常配合指数退避(Exponential Backoff)策略,避免对服务器造成过大压力。
操作流程:
- 设置超时:在
get()或post()中传入timeout=(3, 5),表示连接超时3秒,读取超时5秒。 - 配置重试:使用
requests.adapters.HTTPAdapter和urllib3.util.retry.Retry,将重试策略挂载到Session上。 - 定义重试条件:可以指定对哪些状态码(如500、502、503)和哪些异常(如连接错误、超时)进行重试。
- 设置退避因子:重试间隔时间按
backoff_factor * (2 ** retry_count)计算,避免立即重试导致雪崩。
案例拆解:构建一个带超时和重试的Session,用于爬取一个不稳定的API。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# 配置重试策略
retry_strategy = Retry(
total=3, # 最大重试次数
connect=3, # 连接失败重试次数
read=3, # 读取失败重试次数
status=3, # 状态码触发重试次数
backoff_factor=1, # 退避因子,间隔为 1s, 2s, 4s
status_forcelist=[500, 502, 503, 504], # 触发重试的状态码
allowed_methods=["GET", "POST"] # 允许重试的HTTP方法
)
# 创建Session并挂载适配器
session = requests.Session()
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 使用Session发送请求
try:
resp = session.get("https://httpbin.org/status/503", timeout=(3, 5))
print(f"最终状态码: {resp.status_code}")
except requests.exceptions.RetryError as e:
print(f"重试次数耗尽: {e}")
except requests.exceptions.Timeout:
print("请求超时")
常见误区:
- 对POST请求盲目重试:POST请求可能产生非幂等副作用(如创建订单),重试可能导致重复提交。应仅在明确API支持幂等或使用唯一请求ID时对POST启用重试。
- 重试次数过多:设置
total=10且退避因子较大时,总等待时间可能超过用户容忍度,应根据业务场景合理设置。 - 忽略超时设置:不设置
timeout时,请求可能挂起数分钟甚至更久,严重影响程序效率。
robots.txt协议。
4.5 知识结构表格与总结复盘
| 功能模块 | 核心API/参数 | 典型场景 | 注意事项 |
|---|---|---|---|
| GET请求 | requests.get(url, params) |
获取资源、搜索查询 | 参数自动编码,URL长度限制 |
| POST请求 | requests.post(url, data/json) |
提交表单、创建资源 | 区分data与json格式 |
| 响应解析 | resp.status_code, resp.json() |
API数据提取 | 先检查状态码,再解析内容 |
| 超时控制 | timeout=(connect, read) |
防止请求挂起 | 连接与读取超时分开设置 |
| 重试机制 | HTTPAdapter + Retry |
网络不稳定、服务器临时故障 | 注意幂等性,设置退避策略 |
总结复盘:本章围绕requests库的四大核心能力展开。发送GET/POST请求是基础,关键在于理解HTTP语义与参数传递位置;处理响应时,应养成“先检查状态码,再解析内容”的习惯,并注意编码问题;超时与重试是生产级代码的必备要素,能显著提升程序的鲁棒性。在实际项目中,建议将请求逻辑封装为独立的服务层,统一管理Session、超时、重试和日志,这样既能复用代码,也便于后期维护和监控。
- 所有请求必须显式设置
timeout,推荐使用元组形式(3, 5)。 - 使用
Session对象复用TCP连接,提升性能。 - 对重试策略设置
status_forcelist和backoff_factor,避免无差别重试。 - 在解析JSON前调用
raise_for_status(),简化异常处理流程。 - 对于文件下载,使用
stream=True和iter_content()分块处理。