Skip to content

第 9 章 · 异常处理与最佳实践

本章目标:掌握 requests 和 httpx 的异常层次,编写健壮的错误处理代码。

9.1 异常层次结构

requests 的异常继承关系清晰:

requests.exceptions.RequestException
├── ConnectionError      # 网络连接失败
│   ├── ProxyError       # 代理连接错误
│   └── SSLError         # SSL 证书错误
├── HTTPError            # HTTP 状态码错误(4xx/5xx)
├── Timeout              # 请求超时
│   ├── ConnectTimeout
│   └── ReadTimeout
├── TooManyRedirects     # 重定向过多
└── MissingSchema        # URL 缺少协议
python
import requests

try:
    resp = requests.get('https://api.example.com/data', timeout=5)
    resp.raise_for_status()  # 检查 HTTP 错误
except requests.exceptions.HTTPError as e:
    print(f'HTTP 错误: {e.response.status_code}')
except requests.exceptions.ConnectionError as e:
    print(f'连接失败: {e}')
except requests.exceptions.Timeout as e:
    print(f'超时: {e}')
except requests.exceptions.RequestException as e:
    print(f'其他错误: {e}')

9.2 httpx 异常体系

httpx 的异常命名更直观:

python
import httpx

try:
    with httpx.Client() as client:
        resp = client.get('https://api.example.com/data', timeout=5)
        resp.raise_for_status()
except httpx.ConnectTimeout:
    print('连接超时')
except httpx.ReadTimeout:
    print('读取超时')
except httpx.HTTPStatusError as e:
    print(f'HTTP {e.response.status_code}')
except httpx.RequestError as e:
    print(f'请求错误: {e}')

9.3 统一异常处理工具函数

生产代码建议封装统一的处理逻辑:

python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_session(retries=3, backoff=1) -> requests.Session:
    """创建带重试策略的 session"""
    session = requests.Session()
    retry = Retry(
        total=retries,
        backoff_factor=backoff,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "POST"]
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    return session

def safe_get(url: str, **kwargs) -> requests.Response:
    """带异常处理的 GET 请求"""
    session = create_session()
    try:
        resp = session.get(url, timeout=10, **kwargs)
        resp.raise_for_status()
        return resp
    except requests.exceptions.Timeout:
        print(f'超时: {url}')
    except requests.exceptions.HTTPError as e:
        print(f'HTTP 错误 {e.response.status_code}: {url}')
    except requests.exceptions.ConnectionError:
        print(f'连接失败: {url}')
    finally:
        session.close()

9.4 上下文管理器与资源释放

重要:始终使用 with 语句或手动关闭响应/客户端:

python
import requests

# 方式一:with 语句(推荐)
with requests.Session() as session:
    resp = session.get('https://api.example.com')
    data = resp.json()

# 方式二:手动关闭
session = requests.Session()
try:
    resp = session.get('https://api.example.com')
    data = resp.json()
finally:
    session.close()

# httpx 同样需要关闭
import httpx
with httpx.Client() as client:
    resp = client.get('https://api.example.com')

未正确关闭连接会导致 TCP 连接泄漏,最终耗尽文件描述符。

9.5 最佳实践清单

  1. 总是设置超时timeout=(connect, read)
  2. 使用 Session:复用连接,提升性能;
  3. 启用重试:对幂等请求(GET)启用自动重试;
  4. 捕获具体异常:避免裸 except Exception
  5. 检查状态码:调用 raise_for_status()
  6. 资源清理:使用 with 语句或 try/finally

9.6 本章小结

  • requests 异常继承自 RequestException,按类别细分;
  • httpx 异常命名更直观,如 ConnectTimeoutHTTPStatusError
  • 始终使用上下文管理器确保连接正确释放;
  • 封装统一的错误处理工具函数,提升代码健壮性。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. requests 中检查 HTTP 状态码错误的方法是?

2. 以下哪种做法会导致连接泄漏?

3. httpx 中捕获连接超时的异常类是?

4. 对幂等请求(如 GET)启用重试时,should_retry_on_5xx 应该设为?

🛠️ 动手实践

  1. 编写统一异常处理函数,捕获五种不同类型的异常并打印不同消息。
  2. 对比使用 with requests.Session() 和不使用的资源泄漏情况(通过 lsof 观察文件描述符)。
  3. 封装一个带重试的 safe_get 函数,测试 500 错误时的自动重试行为。

完成练习后,进入下一章:httpx 简介与同步 API