088、requests 库深度使用:Session、适配器、重试机制与 SSL 证书处理

088、requests 库深度使用:Session、适配器、重试机制与 SSL 证书处理

上周帮同事排查一个线上爬虫报错,日志里全是 ConnectionErrorSSLError,服务端那边说“我们证书没问题啊”,结果折腾了两天发现是 requests 默认的重试策略太弱,加上目标服务器用了自签名证书。这种坑我踩过不止一次,今天把 requests 库几个容易翻车的深度用法掰开揉碎讲清楚。

Session 对象:别每次都新建连接

很多人写爬虫喜欢这样:

import requests
resp = requests.get('https://api.example.com/data')

每次调用都会新建 TCP 连接、完成 SSL 握手,频繁请求时性能惨不忍睹。更隐蔽的问题是——如果你需要维持 cookies 或自定义 headers,每次都得手动传一遍。

正确的做法是用 Session:

import requests

session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'application/json'
})
# 这里踩过坑:Session 的 headers 是持久化的,但如果你在单个请求里传了同名 header,会覆盖 session 级别的
resp1 = session.get('https://api.example.com/login', params={'user': 'admin'})
resp2 = session.get('https://api.example.com/profile')  # 自动携带 cookies

Session 底层维护了一个连接池(urllib3 的 PoolManager),默认最多保持 10 个连接。如果你并发请求量大,记得调大这个值:

session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=20, pool_maxsize=50)
session.mount('https://', adapter)
session.mount('http://', adapter)

mount 方法的作用是把适配器绑定到特定协议前缀上。这里 https://http:// 分别挂载,别只挂一个,否则另一个协议会走默认适配器。

适配器(Adapter):定制你的 HTTP 行为

适配器是 requests 里容易被忽略但极其强大的组件。它本质上是 urllib3 的封装层,控制着连接池、重试、超时等底层行为。

除了调连接池大小,适配器还能干更骚的事——比如给特定域名单独配置超时:

from requests.adapters import HTTPAdapter

class TimeoutAdapter(HTTPAdapter):
    def __init__(self, timeout=None, *args, **kwargs):
        self.timeout = timeout
        super().__init__(*args, **kwargs)
    
    def send(self, request, **kwargs):
        kwargs.setdefault('timeout', self.timeout)
        return super().send(request, **kwargs)

session = requests.Session()
# 给内网 API 设置 30 秒超时,外网 API 用默认
session.mount('https://internal-api.company.com', TimeoutAdapter(timeout=30))
session.mount('https://api.github.com', TimeoutAdapter(timeout=10))

别这样写:直接在 requests.get() 里传 timeout 参数,那只是单次请求生效。用适配器可以全局控制,维护起来省心得多。

重试机制:别让网络波动搞崩你的程序

requests 默认不重试,遇到网络错误直接抛异常。生产环境里这等于自杀——网络抖动、DNS 解析失败、服务端限流,随便一个就能让脚本崩溃。

正确做法是给适配器挂载重试策略:

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

retry_strategy = Retry(
    total=3,  # 总重试次数(包括第一次请求)
    backoff_factor=1,  # 退避因子:重试间隔 = backoff_factor * (2 ** (重试次数 - 1))
    status_forcelist=[429, 500, 502, 503, 504],  # 哪些状态码触发重试
    allowed_methods=['GET', 'POST', 'PUT'],  # 哪些 HTTP 方法允许重试
    raise_on_status=False  # 别这样写:设为 True 的话,重试耗尽后还会抛异常,但异常信息不友好
)

adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount('https://', adapter)
session.mount('http://', adapter)

这里踩过坑:backoff_factor 的默认值是 0,意味着重试间隔为 0 秒,等于瞬间重试,对缓解服务端压力毫无帮助。建议至少设为 1,这样第一次重试等待 1 秒,第二次 2 秒,第三次 4 秒。

status_forcelist 里我加了 429(Too Many Requests),因为很多 API 限流后会返回这个状态码,重试时配合退避策略能有效避免被封。

SSL 证书处理:自签名证书与证书验证

SSL 错误是 requests 里最让人头疼的问题之一。常见场景:

  1. 自签名证书:内网服务常用,requests 默认会验证失败
  2. 证书过期:生产环境偶尔会遇到
  3. 证书链不完整:某些中间件配置不当

忽略证书验证(仅限测试环境)

resp = requests.get('https://internal-service:8443', verify=False)
# 别这样写:生产环境绝对不要用 verify=False,等于裸奔

更安全的做法是捕获 requests.packages.urllib3.exceptions.InsecureRequestWarning 警告:

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 但这样只是不显示警告,安全性依然没保障

使用自定义 CA 证书

内网服务如果用了自签名证书,可以把 CA 证书文件放在项目里:

resp = requests.get('https://internal-service:8443', verify='/path/to/ca-bundle.crt')

如果证书是 PEM 格式的字符串,可以这样:

import certifi
import ssl

# 把自定义证书追加到 certifi 的默认证书包后面
custom_ca = open('/path/to/custom-ca.pem').read()
with open(certifi.where(), 'a') as f:
    f.write(custom_ca)

# 之后所有请求都会信任这个 CA
resp = requests.get('https://internal-service:8443')

这里踩过坑:直接修改 certifi 的证书文件是全局生效的,如果多个项目共用同一个 Python 环境,可能会互相影响。建议用环境变量 REQUESTS_CA_BUNDLE 指定自定义证书路径:

import os
os.environ['REQUESTS_CA_BUNDLE'] = '/path/to/custom-ca-bundle.crt'

客户端证书认证(双向 SSL)

有些高安全要求的服务需要客户端提供证书:

resp = requests.get(
    'https://secure-service:443',
    cert=('/path/to/client.crt', '/path/to/client.key'),
    verify='/path/to/ca-bundle.crt'
)

cert 参数可以传元组 (证书文件, 私钥文件),也可以传单个文件路径(如果证书和私钥合并在一起)。

实战组合:一个健壮的 Session 封装

把上面这些整合起来,写一个生产可用的 Session 工厂:

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

def create_robust_session(
    pool_connections=20,
    pool_maxsize=50,
    max_retries=3,
    backoff_factor=1,
    status_forcelist=None,
    timeout=30,
    ca_bundle=None
):
    if status_forcelist is None:
        status_forcelist = [429, 500, 502, 503, 504]
    
    retry_strategy = Retry(
        total=max_retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
        allowed_methods=['GET', 'POST', 'PUT', 'DELETE'],
        raise_on_status=False
    )
    
    adapter = HTTPAdapter(
        pool_connections=pool_connections,
        pool_maxsize=pool_maxsize,
        max_retries=retry_strategy
    )
    
    session = requests.Session()
    session.mount('https://', adapter)
    session.mount('http://', adapter)
    
    # 默认超时
    session.request = lambda method, url, **kwargs: (
        kwargs.setdefault('timeout', timeout),
        super(requests.Session, session).request(method, url, **kwargs)
    )[1]
    
    # 自定义 CA 证书
    if ca_bundle:
        session.verify = ca_bundle
    elif os.environ.get('REQUESTS_CA_BUNDLE'):
        session.verify = os.environ['REQUESTS_CA_BUNDLE']
    
    return session

# 使用示例
session = create_robust_session(
    pool_connections=30,
    pool_maxsize=100,
    max_retries=5,
    backoff_factor=2,
    timeout=15
)

try:
    resp = session.get('https://api.example.com/data')
    resp.raise_for_status()  # 别忘记检查状态码
except requests.exceptions.RequestException as e:
    print(f"请求失败: {e}")

个人经验性建议

  1. 永远不要在生产环境用 verify=False。如果遇到 SSL 错误,先排查证书问题,而不是跳过验证。我见过太多人图省事直接关验证,结果被中间人攻击搞崩了系统。

  2. 重试策略要配合业务场景。写操作(POST/PUT)重试要谨慎,最好实现幂等性检查。读操作(GET)可以放心重试,但注意不要无限重试,设置 total 上限。

  3. 连接池大小不是越大越好。调大 pool_maxsize 能提高并发能力,但也会占用更多内存和文件描述符。Linux 系统默认 ulimit -n 是 1024,别超过这个数。

  4. 日志里记录 SSL 证书信息。调试 SSL 问题时,用 requests.get(..., verify=False) 临时测试可以,但记得在日志里打印证书指纹,方便后续排查。

  5. mount 做精细化控制。不同 API 可能有不同的重试策略和超时要求,别用一个 Session 打天下。给内网服务、外网 API、第三方服务分别挂载不同的适配器。

最后说一句:requests 库虽然简单,但底层 urllib3 的能力远超你的想象。花时间理解 Session、适配器、重试机制这些概念,比背一百个 API 参数更有价值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值