4、requests库入门:发送GET/POST请求、传递参数、处理响应、超时与重试

在Python生态中,requests库是处理HTTP请求的事实标准。它以其简洁的API、直观的语义和强大的功能,极大地降低了网络编程的门槛。无论是调用RESTful API、爬取网页数据,还是对接云服务,requests都是开发者工具箱中不可或缺的利器。本章将深入剖析requests库的核心用法,从最基础的GET/POST请求出发,逐步覆盖参数传递、响应对象解析、超时控制以及重试机制,帮助你建立一套稳健、高效的HTTP客户端开发方法论。

requests 库请求-响应核心链路 客户端 (Client) requests.get() requests.post() Session 会话 timeout / retry 请求参数 (Params) URL 路径参数 Query String (?key=value) JSON / Form 数据体 请求头 (Headers) User-Agent / Accept Authorization / Cookie 目标服务器 (Server) API 网关 / Web 服务 处理请求 返回状态码 + 响应体 重定向 / 缓存策略 异常与错误处理 响应对象 (Response) status_code / headers text / json() / content encoding / url / history raise_for_status() 超时与重试 connect timeout read timeout HTTPAdapter + Retry 指数退避策略 图:requests 库请求-响应核心链路与关键控制点

上图展示了requests库在一次完整HTTP事务中的核心链路。客户端通过requests构造请求,携带参数与请求头,发送至目标服务器;服务器处理完毕后返回响应对象,客户端再对响应进行解析。超时与重试机制贯穿于整个链路,是保障客户端健壮性的关键防线。下面我们将逐一拆解每个环节的具体实现与最佳实践。

4.1 发送GET与POST请求:核心原理与基础操作

核心原理:HTTP协议定义了多种请求方法,其中GET和POST是最常用的两种。GET请求用于获取资源,其参数通常附加在URL的查询字符串(Query String)中,具有幂等性(多次请求结果相同)。POST请求用于提交数据,其参数位于请求体(Request Body)中,常用于创建或修改资源,不具备幂等性。requests库将这两种方法封装为get()和post()函数,底层通过urllib3实现HTTP连接管理。

操作流程:

  1. 导入库:import requests
  2. 构造请求:调用requests.get(url, params=None, headers=None, timeout=None)或requests.post(url, data=None, json=None, headers=None, timeout=None)
  3. 发送请求:函数内部自动完成DNS解析、TCP连接、TLS握手(HTTPS)、发送请求头与请求体。
  4. 获取响应:返回一个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。
风险提示:在传递敏感信息(如密码、)时,务必使用HTTPS协议,并避免将敏感参数放在URL中(GET请求),因为URL可能被服务器日志、浏览器历史或代理服务器记录。

4.3 处理响应:状态码、响应头、文本与JSON解析

核心原理:Response对象封装了服务器返回的所有数据。其核心属性包括:status_code(HTTP状态码)、headers(响应头,大小写不敏感)、text(解码后的文本内容)、content(原始字节)、json()(将JSON文本解析为Python对象)、encoding(文本编码)、url(最终请求URL,跟随重定向后可能变化)、history(重定向历史列表)。

操作流程:

  1. 检查状态码:使用resp.status_code或resp.ok(200-399为True)。
  2. 读取响应头:通过resp.headers.get('Content-Type')等获取元信息。
  3. 解析内容:根据Content-Type选择resp.text(HTML/XML)、resp.json()(JSON)或resp.content(二进制如图片、文件)。
  4. 异常处理:调用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()分块写入。
实战技巧:对于需要频繁解析JSON的API调用,可以封装一个通用函数,统一处理状态码检查、JSON解析和异常捕获,减少重复代码。

4.4 超时与重试:构建稳健的请求策略

核心原理:网络请求可能因网络延迟、服务器过载或DNS解析失败而长时间无响应。设置timeout参数可以控制请求在指定时间内未完成则抛出异常。timeout可以是一个浮点数(连接和读取共用超时时间),也可以是一个元组(connect_timeout, read_timeout),分别控制连接阶段和读取阶段。重试机制则是在请求失败后自动重新发送请求,通常配合指数退避(Exponential Backoff)策略,避免对服务器造成过大压力。

操作流程:

  1. 设置超时:在get()或post()中传入timeout=(3, 5),表示连接超时3秒,读取超时5秒。
  2. 配置重试:使用requests.adapters.HTTPAdapter和urllib3.util.retry.Retry,将重试策略挂载到Session上。
  3. 定义重试条件:可以指定对哪些状态码(如500、502、503)和哪些异常(如连接错误、超时)进行重试。
  4. 设置退避因子:重试间隔时间按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时,请求可能挂起数分钟甚至更久,严重影响程序效率。
风险提示:重试机制虽然能提高成功率,但也会放大对服务器的压力。在爬虫场景中,过快的重试频率可能导致IP被封禁。建议在重试间隔中加入随机抖动(Jitter),并遵守目标网站的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()分块处理。