088、requests 库深度使用:Session、适配器、重试机制与 SSL 证书处理
088、requests 库深度使用:Session、适配器、重试机制与 SSL 证书处理
上周帮同事排查一个线上爬虫报错,日志里全是ConnectionError和SSLError,服务端那边说“我们证书没问题啊”,结果折腾了两天发现是 requests 默认的重试策略太弱,加上目标服务器用了自签名证书。这种坑我踩过不止一次,今天把 requests 库几个容易翻车的深度用法掰开揉碎讲清楚。
Session 对象:别每次都新建连接
很多人写爬虫喜欢这样:
importrequests resp=requests.get('https://api.example.com/data')每次调用都会新建 TCP 连接、完成 SSL 握手,频繁请求时性能惨不忍睹。更隐蔽的问题是——如果你需要维持 cookies 或自定义 headers,每次都得手动传一遍。
正确的做法是用 Session:
importrequests 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')# 自动携带 cookiesSession 底层维护了一个连接池(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 的封装层,控制着连接池、重试、超时等底层行为。
除了调连接池大小,适配器还能干更骚的事——比如给特定域名单独配置超时:
fromrequests.adaptersimportHTTPAdapterclassTimeoutAdapter(HTTPAdapter):def__init__(self,timeout=None,*args,**kwargs):self.timeout=timeoutsuper().__init__(*args,**kwargs)defsend(self,request,**kwargs):kwargs.setdefault('timeout',self.timeout)returnsuper().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 解析失败、服务端限流,随便一个就能让脚本崩溃。
正确做法是给适配器挂载重试策略:
fromrequests.adaptersimportHTTPAdapterfromurllib3.util.retryimportRetry 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警告:
importurllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)# 但这样只是不显示警告,安全性依然没保障使用自定义 CA 证书
内网服务如果用了自签名证书,可以把 CA 证书文件放在项目里:
resp=requests.get('https://internal-service:8443',verify='/path/to/ca-bundle.crt')如果证书是 PEM 格式的字符串,可以这样:
importcertifiimportssl# 把自定义证书追加到 certifi 的默认证书包后面custom_ca=open('/path/to/custom-ca.pem').read()withopen(certifi.where(),'a')asf:f.write(custom_ca)# 之后所有请求都会信任这个 CAresp=requests.get('https://internal-service:8443')这里踩过坑:直接修改 certifi 的证书文件是全局生效的,如果多个项目共用同一个 Python 环境,可能会互相影响。建议用环境变量REQUESTS_CA_BUNDLE指定自定义证书路径:
importos 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 工厂:
importrequestsfromrequests.adaptersimportHTTPAdapterfromurllib3.util.retryimportRetryimportosdefcreate_robust_session(pool_connections=20,pool_maxsize=50,max_retries=3,backoff_factor=1,status_forcelist=None,timeout=30,ca_bundle=None):ifstatus_forcelistisNone: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=lambdamethod,url,**kwargs:(kwargs.setdefault('timeout',timeout),super(requests.Session,session).request(method,url,**kwargs))[1]# 自定义 CA 证书ifca_bundle:session.verify=ca_bundleelifos.environ.get('REQUESTS_CA_BUNDLE'):session.verify=os.environ['REQUESTS_CA_BUNDLE']returnsession# 使用示例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()# 别忘记检查状态码exceptrequests.exceptions.RequestExceptionase: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 参数更有价值。
