网络连接可能随时中断,DNS解析可能失败,服务器可能宕机。
服务器可能返回错误状态码(4xx, 5xx),或者响应格式不符合预期。
请求可能因为超时、频率限制或认证失败而被拒绝。
良好的错误处理可以防止单个请求失败导致整个应用崩溃。
向用户提供清晰的错误信息,而不是让应用无声地失败。
详细的错误日志有助于快速定位和修复问题。
当主要服务不可用时,可以提供备选方案或缓存数据。
Requests库中的所有异常都继承自requests.exceptions.RequestException,而它又继承自Python标准的IOError。
理解这个继承体系很重要,因为它允许你:
| 异常类 | 触发条件 | 说明 | 严重程度 |
|---|---|---|---|
ConnectionError |
网络连接失败 | DNS解析失败、拒绝连接、网络不可达等 | 严重 |
HTTPError |
HTTP状态码4xx或5xx | 服务器返回错误状态码,如404、500等 | 警告 |
Timeout |
请求超时 | 连接超时或读取响应超时 | 警告 |
TooManyRedirects |
重定向次数过多 | 超过最大重定向次数限制 | 警告 |
SSLError |
SSL证书验证失败 | 证书无效、过期或不匹配 | 严重 |
ProxyError |
代理服务器错误 | 代理服务器连接失败或无响应 | 警告 |
ContentDecodingError |
内容解码失败 | 无法解码响应内容(如gzip损坏) | 一般 |
URLRequired |
URL缺失 | 请求没有指定有效的URL | 严重 |
try:
response = requests.get('http://nonexistent-domain.com')
except requests.exceptions.ConnectionError as e:
print(f"连接失败: {e}")
# 可能的恢复措施:
# 1. 检查网络连接
# 2. 尝试备用服务器
# 3. 使用本地缓存
try:
response = requests.get(
'https://slow-api.com/data',
timeout=5 # 5秒超时
)
except requests.exceptions.Timeout as e:
print(f"请求超时: {e}")
# 可能的恢复措施:
# 1. 增加超时时间
# 2. 实现重试机制
# 3. 使用更快的备用服务
HTTP错误(状态码4xx和5xx)不会自动引发异常,除非你显式调用response.raise_for_status()。
import requests
def fetch_data_with_http_error_handling(url):
"""处理HTTP错误的示例函数"""
try:
response = requests.get(url)
# 检查HTTP状态码,如果是错误状态码则引发HTTPError
response.raise_for_status()
# 如果状态码是2xx,处理响应
data = response.json()
return data
except requests.exceptions.HTTPError as http_err:
# HTTP错误处理
status_code = response.status_code
if status_code == 404:
print(f"资源未找到: {url}")
# 处理404错误的逻辑
return None
elif status_code == 401:
print("认证失败,需要重新登录")
# 刷新token或重新认证
return refresh_and_retry(url)
elif status_code == 403:
print("权限不足,访问被拒绝")
# 检查权限或使用其他账号
return None
elif status_code == 429:
print("请求过于频繁,被限流")
# 实现退避重试
time.sleep(5) # 等待5秒
return fetch_data_with_http_error_handling(url)
elif 500 <= status_code < 600:
print(f"服务器内部错误: {status_code}")
# 服务器错误,可以重试
return retry_with_backoff(url)
else:
print(f"HTTP错误 {status_code}: {http_err}")
return None
except requests.exceptions.RequestException as req_err:
# 其他Requests相关错误
print(f"请求失败: {req_err}")
return None
except Exception as e:
# 其他未预期的错误
print(f"未预期的错误: {e}")
return None
# 使用示例
if __name__ == "__main__":
result = fetch_data_with_http_error_handling("https://api.example.com/data")
if result:
print("数据获取成功")
else:
print("数据获取失败")
response.raise_for_status() 方法会在HTTP状态码为4xx或5xx时引发HTTPError异常。
这是处理HTTP错误的推荐方式,因为它让你能够:
超时是网络请求中最常见的异常之一。Requests允许你设置两种超时:连接超时和读取超时。
建立TCP连接的最大等待时间。如果在这个时间内无法建立连接,会引发ConnectTimeout异常。
# 设置3秒连接超时
requests.get(url, timeout=(3.05, None))
从服务器接收数据的最大等待时间。如果服务器在这个时间内没有发送数据,会引发ReadTimeout异常。
# 设置10秒读取超时
requests.get(url, timeout=(None, 10))
import requests
import time
def make_request_with_timeout(url, max_retries=3):
"""带有超时处理和重试机制的请求函数"""
# 超时配置:连接超时3秒,读取超时10秒
timeout_config = (3.05, 10)
for attempt in range(max_retries):
try:
print(f"第 {attempt + 1} 次尝试...")
response = requests.get(url, timeout=timeout_config)
response.raise_for_status()
print("请求成功!")
return response
except requests.exceptions.ConnectTimeout:
print(f"连接超时 - 服务器 {url} 无法连接")
if attempt < max_retries - 1:
wait_time = 2 ** attempt # 指数退避:1, 2, 4秒
print(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
else:
print("已达到最大重试次数")
return None
except requests.exceptions.ReadTimeout:
print(f"读取超时 - 服务器响应太慢")
if attempt < max_retries - 1:
# 对于读取超时,可以尝试增加读取超时时间
new_timeout = (timeout_config[0], timeout_config[1] * 2)
timeout_config = new_timeout
print(f"增加读取超时到 {new_timeout[1]} 秒")
else:
print("已达到最大重试次数")
return None
except requests.exceptions.Timeout:
# 通用超时处理(如果不知道是连接还是读取超时)
print(f"请求超时")
if attempt < max_retries - 1:
wait_time = 1 * (attempt + 1) # 线性退避
print(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
else:
print("已达到最大重试次数")
return None
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
return None
return None
# 使用示例
if __name__ == "__main__":
result = make_request_with_timeout("https://httpbin.org/delay/5", max_retries=3)
if result:
print(f"响应状态码: {result.status_code}")
else:
print("所有尝试都失败了")
对于临时性错误(如网络波动、服务器重启),重试机制可以显著提高请求的成功率。
使用循环和条件判断手动实现重试逻辑,最灵活但代码较多。
使用第三方库简化重试逻辑,功能强大但需要额外依赖。
使用Requests底层库的功能,最原生但配置稍复杂。
import requests
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def make_request_with_retry(url, max_retries=3, backoff_factor=1):
"""
带有重试机制的请求函数
参数:
url: 请求URL
max_retries: 最大重试次数
backoff_factor: 退避因子,用于计算重试间隔
"""
# 需要重试的异常类型
retry_exceptions = (
requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
requests.exceptions.HTTPError # 只重试某些HTTP错误
)
# 需要重试的HTTP状态码
retry_status_codes = {408, 429, 500, 502, 503, 504}
for attempt in range(max_retries):
try:
logger.info(f"尝试 {attempt + 1}/{max_retries}: {url}")
response = requests.get(url, timeout=(3.05, 10))
# 检查是否需要重试(基于状态码)
if response.status_code in retry_status_codes:
response.raise_for_status() # 这会引发HTTPError
# 请求成功
logger.info(f"请求成功,状态码: {response.status_code}")
return response
except retry_exceptions as e:
logger.warning(f"请求失败: {type(e).__name__} - {e}")
# 如果是最后一次尝试,直接抛出异常
if attempt == max_retries - 1:
logger.error(f"所有 {max_retries} 次尝试都失败了")
raise
# 计算等待时间(指数退避)
wait_time = backoff_factor * (2 ** attempt)
logger.info(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
except Exception as e:
# 其他异常不重试
logger.error(f"非重试异常: {type(e).__name__} - {e}")
raise
# 理论上不会执行到这里
return None
# 使用示例
if __name__ == "__main__":
try:
response = make_request_with_retry(
"https://httpbin.org/status/500", # 模拟服务器错误
max_retries=3,
backoff_factor=1
)
if response:
print("最终请求成功!")
except Exception as e:
print(f"所有重试都失败了: {e}")
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# 创建重试策略
retry_strategy = Retry(
total=3, # 总重试次数(包括第一次请求)
backoff_factor=1, # 退避因子
status_forcelist=[429, 500, 502, 503, 504], # 需要重试的状态码
allowed_methods=["GET", "POST"], # 允许重试的HTTP方法
raise_on_status=False # 是否在状态码错误时引发异常
)
# 创建HTTP适配器并设置重试策略
adapter = HTTPAdapter(max_retries=retry_strategy)
# 创建会话并挂载适配器
session = requests.Session()
session.mount("http://", adapter)
session.mount("https://", adapter)
# 使用带有重试机制的会话发送请求
try:
response = session.get("https://httpbin.org/status/500", timeout=5)
print(f"状态码: {response.status_code}")
print(f"重试次数: {response.raw.retries.total if response.raw else 0}")
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
# 也可以为特定请求自定义重试策略
custom_retry = Retry(
total=5,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504]
)
custom_adapter = HTTPAdapter(max_retries=custom_retry)
custom_session = requests.Session()
custom_session.mount("https://api.example.com", custom_adapter)
import requests
import time
import logging
from typing import Optional, Dict, Any
from dataclasses import dataclass
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@dataclass
class ApiClientConfig:
"""API客户端配置"""
base_url: str
timeout: tuple = (3.05, 30)
max_retries: int = 3
retry_backoff: float = 1.0
rate_limit_delay: float = 1.0
class ApiClientError(Exception):
"""自定义API客户端异常"""
pass
class ApiClient:
"""生产环境级别的API客户端"""
def __init__(self, config: ApiClientConfig):
self.config = config
self.session = self._create_session()
self.last_request_time = 0
def _create_session(self) -> requests.Session:
"""创建配置好的HTTP会话"""
session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=self.config.max_retries,
backoff_factor=self.config.retry_backoff,
status_forcelist=[408, 429, 500, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE"],
raise_on_status=False
)
# 创建适配器
adapter = HTTPAdapter(max_retries=retry_strategy)
# 挂载适配器
session.mount("http://", adapter)
session.mount("https://", adapter)
# 设置默认请求头
session.headers.update({
'User-Agent': 'MyApiClient/1.0',
'Accept': 'application/json',
'Content-Type': 'application/json'
})
return session
def _rate_limit(self):
"""简单的速率限制"""
current_time = time.time()
time_since_last = current_time - self.last_request_time
if time_since_last < self.config.rate_limit_delay:
sleep_time = self.config.rate_limit_delay - time_since_last
logger.debug(f"速率限制,等待 {sleep_time:.2f} 秒")
time.sleep(sleep_time)
self.last_request_time = time.time()
def _handle_error(self, response: Optional[requests.Response], error: Exception) -> None:
"""统一错误处理"""
if response is not None:
logger.error(
f"HTTP错误: {response.status_code} - {response.reason} - URL: {response.url}"
)
# 尝试记录响应体中的错误信息
try:
error_data = response.json()
logger.error(f"错误详情: {error_data}")
except:
logger.error(f"响应内容: {response.text[:500]}")
# 根据异常类型采取不同措施
if isinstance(error, requests.exceptions.Timeout):
logger.error("请求超时,请检查网络连接或增加超时时间")
raise ApiClientError("请求超时,请稍后重试") from error
elif isinstance(error, requests.exceptions.ConnectionError):
logger.error("网络连接失败,请检查网络设置")
raise ApiClientError("网络连接失败,请检查网络") from error
elif isinstance(error, requests.exceptions.HTTPError):
# HTTP错误已在上面处理
raise ApiClientError(f"服务器错误: {response.status_code}") from error
else:
logger.error(f"未预期的错误: {type(error).__name__} - {error}")
raise ApiClientError("请求失败,请稍后重试") from error
def request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""发送HTTP请求"""
url = f"{self.config.base_url}{endpoint}"
# 应用速率限制
self._rate_limit()
logger.info(f"发送请求: {method} {url}")
try:
# 发送请求
response = self.session.request(
method=method,
url=url,
timeout=self.config.timeout,
**kwargs
)
# 记录请求统计
logger.debug(
f"请求完成 - 状态码: {response.status_code} "
f"耗时: {response.elapsed.total_seconds():.2f}s"
)
# 检查HTTP状态码
response.raise_for_status()
# 解析响应
try:
return response.json()
except ValueError as e:
logger.warning(f"响应不是有效的JSON: {e}")
return {"text": response.text}
except requests.exceptions.RequestException as e:
self._handle_error(response if 'response' in locals() else None, e)
raise # 这行实际上不会执行,因为_handle_error已经raise了
def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""发送GET请求"""
return self.request('GET', endpoint, **kwargs)
def post(self, endpoint: str, data: Optional[Dict] = None, **kwargs) -> Dict[str, Any]:
"""发送POST请求"""
return self.request('POST', endpoint, json=data, **kwargs)
def close(self):
"""关闭会话"""
self.session.close()
logger.info("API客户端已关闭")
# 使用示例
if __name__ == "__main__":
# 配置客户端
config = ApiClientConfig(
base_url="https://api.example.com/v1",
timeout=(5, 30),
max_retries=3,
retry_backoff=1.0
)
# 创建客户端
client = ApiClient(config)
try:
# 发送请求
data = client.get("/users/123")
print(f"获取的用户数据: {data}")
# 发送POST请求
new_user = {"name": "John Doe", "email": "john@example.com"}
result = client.post("/users", data=new_user)
print(f"创建用户结果: {result}")
except ApiClientError as e:
print(f"API请求失败: {e}")
except KeyboardInterrupt:
print("\n用户中断操作")
finally:
# 确保关闭客户端
client.close()
print("程序结束")
Requests库提供了完善的异常处理机制,通过合理的错误处理可以构建健壮的应用程序。关键要点:
response.raise_for_status()处理HTTP错误良好的错误处理不仅能让应用更加稳定,还能提供更好的用户体验,是高质量代码的重要标志。