088、requests 库深度使用:Session、适配器、重试机制与 SSL 证书处理
上周帮同事排查一个线上爬虫报错,日志里全是 ConnectionError 和 SSLError,服务端那边说“我们证书没问题啊”,结果折腾了两天发现是 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 里最让人头疼的问题之一。常见场景:
- 自签名证书:内网服务常用,requests 默认会验证失败
- 证书过期:生产环境偶尔会遇到
- 证书链不完整:某些中间件配置不当
忽略证书验证(仅限测试环境)
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}")
个人经验性建议
-
永远不要在生产环境用
verify=False。如果遇到 SSL 错误,先排查证书问题,而不是跳过验证。我见过太多人图省事直接关验证,结果被中间人攻击搞崩了系统。 -
重试策略要配合业务场景。写操作(POST/PUT)重试要谨慎,最好实现幂等性检查。读操作(GET)可以放心重试,但注意不要无限重试,设置
total上限。 -
连接池大小不是越大越好。调大
pool_maxsize能提高并发能力,但也会占用更多内存和文件描述符。Linux 系统默认ulimit -n是 1024,别超过这个数。 -
日志里记录 SSL 证书信息。调试 SSL 问题时,用
requests.get(..., verify=False)临时测试可以,但记得在日志里打印证书指纹,方便后续排查。 -
用
mount做精细化控制。不同 API 可能有不同的重试策略和超时要求,别用一个 Session 打天下。给内网服务、外网 API、第三方服务分别挂载不同的适配器。
最后说一句:requests 库虽然简单,但底层 urllib3 的能力远超你的想象。花时间理解 Session、适配器、重试机制这些概念,比背一百个 API 参数更有价值。
695

被折叠的 条评论
为什么被折叠?



