news 2026/6/24 16:47:15

Python爬虫SSL证书验证失败:从诊断到根治的完整解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫SSL证书验证失败:从诊断到根治的完整解决方案

1. 项目概述:当爬虫遇上SSL证书验证

最近在维护一个基于bilibili-api的自动化数据采集项目时,遇到了一个非常典型的网络编程问题:SSL证书验证失败。具体表现是,脚本在请求B站接口时,会间歇性地抛出诸如SSLErrorCERTIFICATE_VERIFY_FAILED或者更具体的[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate这类错误。这个问题看似简单,背后却牵扯到操作系统根证书库、Python环境、网络中间设备以及目标服务器配置等多个层面,绝不是一句“关掉验证”就能草草了事的。对于依赖bilibili-api这类第三方库进行稳定数据获取的开发者来说,彻底理解和解决SSL证书验证问题,是保障服务可靠性的基本功。

SSL证书验证是现代网络通信安全的基石,它确保了你的客户端(比如你的Python脚本)正在与它声称的服务器(比如api.bilibili.com)对话,而不是一个恶意的中间人。bilibili-api库底层通常使用requestsaiohttp进行HTTP请求,这些库默认会启用严格的证书验证。当验证失败时,请求就会中止,这对于自动化任务来说是致命的。本文将从一个资深开发者的视角,深度拆解在bilibili-api项目场景下,SSL证书验证问题的各种成因、排查思路以及不同安全等级下的解决方案。我们不仅要解决问题,更要理解问题背后的“为什么”,从而在未来的开发中做到游刃有余。

2. 核心问题诊断与根因分析

遇到SSL错误,第一步绝不是盲目搜索“如何禁用SSL验证”,而是要进行系统的诊断,定位问题究竟出在链条的哪个环节。盲目禁用验证相当于在公路上拆掉了所有交通信号灯和警察,虽然车能开了,但风险极高。

2.1 错误信息的深度解读

首先,我们需要学会“阅读”错误信息。Python抛出的SSL错误信息通常包含了关键线索:

  1. ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

    • 核心线索unable to get local issuer certificate
    • 含义:客户端(你的程序)收到了服务器发来的证书,但在尝试构建证书信任链时,找不到签发该证书的中间证书或根证书。这个“找不到”的证书被称为“颁发者(Issuer)”。
    • 根因推测:这通常指向你本地操作系统或Python环境中的根证书库(CA Bundle)不完整、过时,或者没有包含B站证书链所需的那个根证书颁发机构(CA)。B站使用的证书通常由全球知名的CA(如DigiCert、GlobalSign)签发,但也可能在某些网络环境下(如企业内网代理)被替换。
  2. requests.exceptions.SSLError: HTTPSConnectionPool(host='api.bilibili.com', port=443): Max retries exceeded with url: ... (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))

    • 这是通过requests库包装后的错误,本质和上面一样。它明确了主机和端口,帮助我们确认问题发生在与api.bilibili.com:443的握手阶段。
  3. ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:997)

    • 这个错误看起来和证书无关
    • 含义:客户端和服务器在尝试建立SSL/TLS连接时,在协议版本协商上出现了问题。
    • 根因推测:这常常发生在你配置了HTTP代理,但代码错误地尝试与代理服务器建立HTTPS连接(即向代理的HTTP端口发送了HTTPS请求),或者代理服务器本身不支持SSL。此时,代理服务器返回一个普通的HTTP错误响应,但客户端期待的是TLS握手报文,于是产生了版本号错误。
  4. [Errno 104] Connection reset by peer或超时

    • 在某些严格的企业网络策略下,SSL握手失败可能直接表现为连接被对端重置或超时,而不是明确的证书错误。这增加了排查难度。

2.2 系统性排查路径

基于错误信息,我们可以遵循以下路径进行排查:

第一步:隔离环境,确认问题范围首先,在命令行中,使用openssl工具进行快速测试,这可以绕过Python和具体代码库。

openssl s_client -connect api.bilibili.com:443 -showcerts

观察命令输出。如果连接成功,你会看到完整的服务器证书链。重点关注最后几行,如果出现Verify return code: 0 (ok),说明你的系统根证书库是完整的,能够验证B站的证书。如果出现Verify return code: 20 (unable to get local issuer certificate),则证实了是本地CA证书库的问题。

第二步:检查Python的SSL模块和证书路径在Python交互环境中执行:

import ssl print(ssl.OPENSSL_VERSION) # 查看链接的OpenSSL版本 print(ssl.get_default_verify_paths()) # 查看Python默认的证书验证路径

get_default_verify_paths()会返回一个对象,其中的cafilecapath是关键。cafile通常为None,表示使用系统默认的证书文件;capath指向一个目录。在Linux/macOS上,通常是/etc/ssl/certs/usr/lib/ssl/certs。在Windows上,情况更复杂,Python可能使用它自己捆绑的证书文件(如pip安装目录下的cacert.pem),也可能依赖系统的证书存储。

第三步:检查网络中间件(代理、防火墙、安全软件)这是企业内网开发中最常见的坑。许多公司会使用中间人(MITM)代理对出站HTTPS流量进行解密和审查。为此,公司IT会在你的电脑上安装一个自定义的根证书。此时:

  • 你的浏览器因为信任了公司安装的根证书,可以正常访问所有HTTPS网站。
  • 但你的Python脚本(或openssl)使用的证书库可能不包含这个自定义根证书,导致验证失败。 判断方法:尝试在同一个网络下,用代码访问一个公认的、使用标准CA证书的网站(如https://www.google.comhttps://www.baidu.com)。如果也失败,那么极大概率是中间人代理的问题。

第四步:分析bilibili-api的请求上下文检查你的代码是否在请求中传递了特殊的headerscookies或使用了会话(Session),这些有时会影响到连接池和SSL上下文。特别是如果你复用了同一个requests.Session对象,并且之前对其verify参数做过修改,可能会影响到后续所有请求。

3. 解决方案:从临时规避到根治

根据不同的根因和安全性要求,我们可以选择不同层级的解决方案。我强烈建议优先采用根治方案,临时方案仅用于紧急排查或特定受控环境。

3.1 方案一:临时绕过验证(不推荐用于生产环境)

这是最快速但最不安全的方法,仅适用于在绝对可信的隔离环境(如本地测试、无外部风险的虚拟机)中进行问题排查

对于requests库(bilibili-api常用底层)

import requests from bilibili_api import some_module # 方法1:为单次请求禁用验证 response = requests.get('https://api.bilibili.com/xxx', verify=False) # 方法2:创建自定义会话并禁用验证(影响该会话所有请求) session = requests.Session() session.verify = False # 然后需要看bilibili-api是否支持传入自定义session,或者修改其内部使用的session

重要警告:设置verify=False会触发InsecureRequestWarning警告。你可以用urllib3来禁用这个警告,但这只是掩耳盗铃。

import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

注意:在生产环境或任何处理用户敏感数据、涉及账号登录(如B站账号)的场景下,绝对禁止使用此方法。这会让你暴露在中间人攻击风险之下,可能导致账号被盗、数据泄露。

3.2 方案二:指定自定义CA证书包(推荐用于企业代理环境)

如果你的问题根源是公司代理安装了自定义根证书,那么正确的做法是将这个根证书添加到你的信任链中。

  1. 找到证书文件:通常公司IT会提供这个证书(如company-root-ca.crt),或者你可以从浏览器中导出。

    • 在浏览器中访问任意内部HTTPS网站,点击地址栏的锁图标 -> “连接是安全的” -> “证书有效”。
    • 在证书查看器中,找到最顶层的“根证书”,将其导出为Base64编码的X.509证书(.crt或.pem格式)。
  2. 在代码中使用自定义证书包

    • 方法A:合并证书。将导出的公司根证书内容,追加到你Python环境正在使用的证书文件(如cacert.pem)末尾。然后指定这个合并后的文件。
    • 方法B:直接指定(更清晰)。将公司证书保存为独立文件,然后在请求时指定。
import requests import os # 假设公司证书路径 COMPANY_CA_BUNDLE = '/path/to/your/company-root-ca.crt' # 如果系统证书库+公司证书能验证,可以合并指定(如果系统库本身不完整,此方法可能无效) # 更可靠的方法是创建一个包含所有必要CA的bundle文件 CUSTOM_CA_BUNDLE = '/path/to/merged/cacert.pem' session = requests.Session() session.verify = CUSTOM_CA_BUNDLE # 或 COMPANY_CA_BUNDLE (如果它本身是完整的bundle) # 之后,使用这个session来初始化bilibili-api的相关对象,或者查看api是否支持传入自定义session

对于bilibili-api:你需要查阅其源码或文档,看它是否暴露了设置底层HTTP客户端参数的接口。很多封装库允许你传递一个自定义的requests.Session实例或类似的客户端配置。

3.3 方案三:更新系统/Python根证书库(根治方案)

对于unable to get local issuer certificate这类问题,最根本的解决方法是更新你本地的证书权威机构(CA)列表。

  • Linux (Debian/Ubuntu):

    sudo apt update sudo apt install ca-certificates sudo update-ca-certificates --fresh

    这个操作会更新/etc/ssl/certs目录下的证书。

  • macOS: macOS使用Keychain管理证书。通常通过系统更新来获取。你也可以尝试命令行安装:

    # 安装Homebrew的证书包(如果使用Homebrew的Python,这可能有用) brew install ca-certificates # 对于系统Python,证书通常随系统更新
  • Windows: Windows的证书存储由系统管理。更新通常通过Windows Update进行。对于Python,一个常见问题是Python安装包可能自带了一个过时的cacert.pem文件。

    1. 找到你的Python安装目录下的Lib\site-packages\pip\_vendor\certifiLib\site-packages\certifi中的cacert.pem文件。
    2. 从官方源(如curl官网)下载最新的cacert.pem文件替换它。
    3. 更优雅的方式是使用certifi包:
      pip install --upgrade certifi
      然后,在代码中显式使用certifi提供的证书路径:
      import certifi import requests session = requests.Session() session.verify = certifi.where() # 这会指向certifi包提供的最新证书文件
  • 使用certifi包(跨平台推荐): 无论什么操作系统,使用certifi包来提供CA证书是最可靠、最一致的方法。它打包了Mozilla维护的权威CA列表。

    import certifi import requests import ssl import urllib.request # 对于requests response = requests.get('https://api.bilibili.com', verify=certifi.where()) # 对于标准库urllib (如果bilibili-api底层用了它) context = ssl.create_default_context(cafile=certifi.where()) # 然后将这个context用于你的HTTP客户端

    确保你的bilibili-api依赖的HTTP客户端能接受自定义的SSL上下文或CA文件路径。

3.4 方案四:处理代理导致的SSL问题

如果错误是WRONG_VERSION_NUMBER或你明确知道身处代理环境,需要正确配置代理。

  • 正确配置HTTP代理

    import requests proxies = { 'http': 'http://your-proxy:port', 'https': 'http://your-proxy:port', # 注意:很多HTTP代理对HTTPS流量也使用http协议 # 或者,如果代理支持HTTPS隧道: # 'https': 'https://your-proxy:port', } session = requests.Session() session.proxies.update(proxies) # 如果代理需要认证 session.proxies.update({ 'http': 'http://user:pass@proxy:port', 'https': 'http://user:pass@proxy:port', })

    关键点:https的代理URL协议写http://是常见的,这表示使用HTTP CONNECT方法建立隧道。

  • 代理 + 自定义证书:如果代理同时进行了SSL中间人解密,你需要在配置代理的基础上,同时采用方案二,将代理的根证书加入信任。

4. 在bilibili-api框架下的集成实践

理论讲完了,我们落实到具体的bilibili-api项目上。这个库可能使用requestsaiohttp。我们需要找到注入自定义配置的入口。

4.1 查找配置入口

首先,查看你使用的bilibili-api版本的源码或文档。通常,会有一个全局的配置对象或客户端类。例如,库可能提供了一个set_sessionset_client的方法,或者允许在初始化某个对象时传入**kwargs来传递给底层的HTTP客户端。

假设我们发现bilibili-api内部使用了一个名为get_session的函数来获取全局的requests.Session,我们可以尝试猴子补丁(monkey-patch):

from bilibili_api import some_internal_module import requests import certifi # 创建一个符合我们要求的session custom_session = requests.Session() custom_session.verify = certifi.where() # 使用最新的CA证书 # 如果需要代理 # custom_session.proxies.update({...}) # 替换掉库内部使用的session创建函数 original_get_session = some_internal_module.get_session def patched_get_session(): return custom_session some_internal_module.get_session = patched_get_session # 现在,后续所有bilibili-api的调用都会使用我们这个加固过的session

4.2 创建自定义HTTP适配器

对于更复杂的需求,比如需要精细控制TLS版本、密码套件,或者处理特定的网络环境,可以创建自定义的HTTPAdapter

from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager import ssl import certifi class CustomSSLAdapter(HTTPAdapter): """自定义SSL适配器,强制使用特定的CA证书和TLS版本""" def init_poolmanager(self, *args, **kwargs): # 创建一个使用自定义SSL上下文的PoolManager context = ssl.create_default_context(cafile=certifi.where()) # 可选:限制TLS版本,增强安全性 context.minimum_version = ssl.TLSVersion.TLSv1_2 # 可选:设置密码套件 # context.set_ciphers('HIGH:!aNULL:!eNULL:!MD5') kwargs['ssl_context'] = context return super().init_poolmanager(*args, **kwargs) # 使用适配器 session = requests.Session() adapter = CustomSSLAdapter() session.mount('https://', adapter) session.mount('http://', adapter) # 然后将这个session应用到bilibili-api

4.3 异步环境 (aiohttp) 下的处理

如果bilibili-api使用了aiohttp(异步HTTP客户端),配置方式有所不同:

import aiohttp import ssl import certifi # 创建自定义的SSL上下文 ssl_context = ssl.create_default_context(cafile=certifi.where()) # aiohttp可能需要加载证书内容到内存 # 或者直接使用ssl.create_default_context(),系统证书库已更新时通常有效 connector = aiohttp.TCPConnector(ssl=ssl_context) # 对于高版本aiohttp,ssl参数可能是 ssl_context # 对于旧版本或特定情况,可能需要: # ssl_context = ssl.create_default_context(cafile=certifi.where()) # connector = aiohttp.TCPConnector(ssl_context=ssl_context) async with aiohttp.ClientSession(connector=connector) as session: # 将这个session传递给bilibili-api的异步客户端 # ... 调用bilibili-api异步函数 ...

同样,你需要找到bilibili-api中初始化aiohttp.ClientSession的地方并进行替换或配置。

5. 高级排查与疑难杂症

即使尝试了以上所有方法,问题可能依然存在。这时需要一些更深入的排查手段。

5.1 使用调试工具捕获握手过程

启用requestsurllib3的详细日志,可以观察HTTPS握手的每一个步骤。

import logging import urllib3 # 开启调试日志(输出会非常详细) logging.basicConfig(level=logging.DEBUG) urllib3.connectionpool.log.setLevel(logging.DEBUG)

在日志中,你可以看到客户端发送的“ClientHello”信息(包括支持的TLS版本、密码套件),以及服务器返回的证书链。这对于诊断协议版本不匹配、证书链不完整等问题非常有帮助。

5.2 检查系统时间与证书有效期

SSL证书验证严重依赖系统时间的准确性。如果你的系统时间偏差过大(比如快了几小时或慢了几小时),可能会导致证书在“生效前”或“过期后”被判定为无效,从而验证失败。务必确保操作系统的时间、时区设置正确,并且开启了网络时间同步(NTP)。

5.3 防火墙与深度包检测(DPI)干扰

在一些网络管理严格的环境中,防火墙或DPI设备可能会干扰或重置TLS握手。表现可能是随机的连接失败、超时或特定的错误码。这种情况下,通常需要与网络管理员协作,将你的应用服务器IP地址或域名加入白名单,或者了解企业特定的SSL代理配置要求。

5.4 依赖库版本冲突

确保你的requestsurllib3certifi以及bilibili-api本身都是较新的版本。旧版本可能存在已知的SSL相关bug或对现代证书链的支持问题。

pip list | grep -E "(requests|urllib3|certifi|bilibili-api)" pip install --upgrade requests urllib3 certifi bilibili-api

6. 安全最佳实践与总结

在处理完SSL证书验证问题后,我们必须回归安全本质,建立长期稳定的实践。

  1. 永不长期禁用验证verify=False只能是临时调试的“创可贴”,绝不能留在生产代码中。在代码审查中,这应该是一条红线。

  2. 锁定依赖版本,定期更新证书:在项目requirements.txt中固定certifi的版本,并建立定期更新机制。证书会过期,CA列表也会增减。

  3. 企业环境标准化:如果团队都在同一企业网络下开发,应统一将企业根证书部署到开发机、构建服务器和测试环境的信任库中。可以编写一个初始化脚本来自动化这个过程。

  4. bilibili-api贡献代码:如果你找到了一个优雅的、通用的解决方案来配置底层HTTP客户端,可以考虑向bilibili-api开源项目提交PR,增加全局配置项或更灵活的客户端注入方式,帮助社区其他开发者。

  5. 理解错误,而非屏蔽错误:每一次SSL错误都是一个学习机会。花时间读懂错误信息,用openssl s_client工具分析,理解证书链、信任锚、颁发者、主题这些基本概念。这份投入会在未来遇到更复杂的网络问题时得到回报。

回到我们最初的bilibili-api项目,SSL证书验证问题虽然棘手,但解决路径是清晰的:从精准的错误诊断开始,区分是本地证书库缺失、代理干扰还是环境配置问题;然后选择对应的解决方案,优先使用更新CA库或指定可信证书文件的方式;最后将解决方案集成到项目框架中,并建立长效的安全维护机制。这个过程不仅修复了一个bug,更是一次对网络通信安全基础的巩固。在实际操作中,我习惯在项目初始化脚本里就强制设置session.verify = certifi.where(),并做好相关的异常捕获和日志记录,将这类基础架构问题扼杀在启动阶段,让业务代码能更专注于逻辑本身。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/24 16:41:20

MATLAB GUIDE控件数据交互:handles与setappdata核心用法详解

1. 从零开始理解GUIDE中的Widget数据交互如果你刚开始用MATLAB的GUIDE做图形界面,很快就会发现一个让人头疼的问题:我在按钮A的回调函数里计算出了一个重要的结果,怎么让另一个表格(UITABLE)或者文本框显示它&#xff…

作者头像 李华
网站建设 2026/6/24 16:39:10

MATLAB eigshow SVD模式Bug修复与奇异值分解可视化教学价值重探

1. 一个被遗忘的选项如何重新进入视野最近在MATLAB社区里,一个沉寂多年的老话题又被翻了出来,起因是一份关于经典教学演示程序eigshow的Bug报告。如果你用过MATLAB,尤其是接触过线性代数相关的教学或研究,大概率见过或者听说过eig…

作者头像 李华
网站建设 2026/6/24 16:33:00

从MPC8260ADS板载PLD设计解析嵌入式系统板级控制逻辑实现

1. 项目概述与核心价值 在嵌入式系统,尤其是通信处理器开发板的设计中,如何高效、灵活地管理板上五花八门的控制信号和状态寄存器,一直是个既基础又关键的挑战。十几年前,当我第一次拿到摩托罗拉(后来的飞思卡尔&#…

作者头像 李华
网站建设 2026/6/24 16:25:32

MPC8536E USB控制器架构解析与驱动开发实践

1. MPC8536E USB控制器:从硬件接口到软件驱动的全景解析在嵌入式系统开发中,USB接口几乎是现代设备的标配。无论是作为主机连接U盘、键盘,还是作为设备被PC枚举为串口或存储,其稳定性和性能都至关重要。飞思卡尔(现恩智…

作者头像 李华
网站建设 2026/6/24 16:23:18

从“Tag”机制到链式传播:社交互动引擎的设计与运营实战

1. 项目概述:从“你被标记了”到社交互动新范式 “Tag, you’re it!” 这句话,直译过来是“标签,轮到你了!”,但它背后蕴含的,远不止字面意思。它源自经典的儿童追逐游戏,一句“你被抓住了&…

作者头像 李华
网站建设 2026/6/24 16:22:17

HV9931 LED驱动芯片图表化设计实战:从选型计算到PCB布局调试

1. 项目概述:为什么HV9931值得深挖?最近在做一个LED照明项目,客户要求驱动方案既要高效率、低成本,还得能适应宽电压输入,特别是对离线式(Off-line)应用情有独钟。翻了一圈芯片数据手册&#xf…

作者头像 李华