1. 项目概述当爬虫遇上SSL一场加密世界的握手失败做Python爬虫的朋友估计没几个没被HTTPSConnectionPool(hostxxx, port443): 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:1000))))这类报错折磨过。这串看起来又臭又长的错误信息本质上是你的爬虫程序在尝试与一个HTTPS网站建立安全连接时SSL/TLS握手失败了。简单来说你的程序客户端无法验证对方服务器服务端出示的“数字身份证”SSL证书是否真实可信。这不仅仅是“加个s”那么简单。HTTPS协议的核心是安全而安全的基础是信任。SSL证书由受信任的证书颁发机构签发你的操作系统或Python环境里预置了一份这些“权威机构”的名单称为根证书库。当你的爬虫访问https://example.com时服务器会出示它的证书你的程序会拿着这个证书去根证书库里核对签发链。如果核对不上或者证书过期、域名不匹配程序就会出于安全考虑果断拒绝连接并抛出我们看到的这个错误。这个问题在爬虫开发中极其常见尤其是当你面对一些内部系统、使用自签名证书的测试环境、或者证书配置不那么规范的网站时。新手往往会感到困惑而老手则有一套成熟的应对策略。今天我们就来彻底拆解这个问题从根上理解它为何发生并给出三种从“临时绕过”到“根治解决”的实战方法让你下次再遇到时能从容应对。2. 核心原理拆解SSL/TLS握手与证书验证链要解决问题先得明白问题出在哪。我们不能满足于“加个verifyFalse参数就完事”得知道背后的门道。2.1 HTTPS连接建立的核心步骤当你用requests.get(‘https://example.com’)时底层通常是urllib3会发起一个复杂的握手过程TCP三次握手首先建立基础的网络连接。Client Hello客户端你的爬虫向服务器发送支持的TLS版本、加密套件列表等信息。Server Hello服务器选择一种双方都支持的TLS版本和加密套件并将其SSL证书发送给客户端。证书验证这是报错发生的核心环节。客户端收到证书后会进行一系列验证证书有效性检查证书是否在有效期内。域名匹配检查证书中声明的域名Common Name或Subject Alternative Names是否与你正在访问的域名匹配。颁发链验证关键服务器证书通常不是由根证书机构直接签发而是存在一个中间证书链。客户端需要逐级验证直到找到一个它信任的、预置在本地根证书库中的根证书。如果任何一环的签名验证失败或找不到可信的根证书验证就会失败。密钥交换验证通过后客户端生成一个“预主密钥”用服务器证书中的公钥加密后发送给服务器。加密通信双方根据预主密钥生成相同的会话密钥之后的所有通信都用此密钥加密。我们的报错就卡在第4步。unable to get local issuer certificate这个提示非常明确客户端找不到签发服务器证书的那个“颁发者”Issuer的证书也就是说证书链在本地断掉了无法追溯到任何一个受信任的根证书。2.2 Python环境中的证书库Python本身不维护证书库它依赖于操作系统或指定的外部文件。macOS / Linux通常使用系统的证书存储如macOS的KeychainLinux上/etc/ssl/certs/目录。Windows使用系统的证书存储。特殊情况在某些Python发行版如一些老版本的Anaconda或纯净安装环境下可能会使用一个独立的证书文件包例如certifi库提供的cacert.pem文件。requests库及其底层的urllib3默认会尝试自动定位并使用这个证书库。当它找不到或者证书库不完整、过期时验证失败就会发生。注意自签名证书之所以会触发此错误是因为它根本不是由公共的受信CA签发的它的根证书不在任何公共根证书库中。内部CA企业自建的证书也需要将其根证书安装到客户端的信任库中才能被验证。3. 方法一临时绕过验证verifyFalse这是最广为人知、最快能让代码跑起来的方法但也是风险最高、最不推荐在生产环境中使用的方法。3.1 具体操作与代码示例在requests请求中将verify参数设置为False。import requests url https://your-internal-site.com response requests.get(url, verifyFalse) print(response.status_code)对于使用aiohttp的异步爬虫方法类似import aiohttp import asyncio async def fetch(): connector aiohttp.TCPConnector(sslFalse) # 关键参数 async with aiohttp.ClientSession(connectorconnector) as session: async with session.get(https://your-internal-site.com) as resp: print(await resp.text()) asyncio.run(fetch())3.2 深入解析与巨大风险verifyFalse到底做了什么它完全跳过了上述核心步骤4证书验证。你的程序对服务器说“把你的证书给我看看……算了不用看了我相信你。” 这带来了两个严重问题中间人攻击风险攻击者可以轻易地在你的网络路径上伪装成目标服务器与你建立“安全”连接从而窃听、篡改你传输的所有数据包括Cookie、Session、登录凭证等敏感信息。爬虫可能因此获取到伪造的数据或者泄露自身的认证信息。无法识别服务器身份你无法确认你正在连接的是否真的是your-internal-site.com而不是一个钓鱼网站。3.3 适用场景与强烈警告仅适用于本地开发、测试环境访问已知绝对安全的自签名服务。快速调试临时确认是否是证书问题导致连接失败。实操心得如果使用了verifyFalse务必在代码中添加醒目的注释说明原因和潜在风险并标记为待修复项。绝对不要将带有verifyFalse的代码提交到生产环境或公共仓库。运行时会收到一个严重的警告InsecureRequestWarning: Unverified HTTPS request is being made.。你可以暂时屏蔽它但这只是掩耳盗铃。import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)结论将此方法视为“创可贴”仅作临时止血之用绝非长久之计。4. 方法二指定自定义CA证书包或单个证书这是解决自签名或内部CA证书问题的正确姿势。原理是告诉你的爬虫程序“不要只用系统默认的信任名单额外也信任我给你的这个些证书。”4.1 操作步骤详解步骤1获取目标服务器的证书或CA根证书对于自签名证书你需要直接拿到该服务器的.crt或.pem格式证书文件。对于内部CA你需要拿到签发服务器证书的那个内部CA的根证书或中间证书。通常可以从内部IT部门获取或者从已配置好的浏览器/系统中导出。步骤2在代码中指定证书路径将verify参数指向你获得的证书文件。import requests url https://internal-api.company.com # 指向单个CA证书文件 response requests.get(url, verify/path/to/your/company_root_ca.crt) # 或者如果你有多个证书可以将它们合并到一个.pem文件中然后指定这个文件 # response requests.get(url, verify/path/to/your/custom_ca_bundle.pem)步骤3可选推荐创建自定义证书包如果你需要访问多个使用不同内部CA的站点管理多个单独证书文件很麻烦。可以创建一个自定义的证书包文件准备一个文本文件例如my_custom_ca_bundle.pem。将系统默认的证书包如certifi.where()返回的路径内容复制进去。将你的内部CA证书内容追加到这个文件的末尾。在代码或环境变量中全局指定使用这个自定义包。import requests import certifi # 方法A在单个请求中指定 custom_bundle /path/to/my_custom_ca_bundle.pem response requests.get(https://site1.com, verifycustom_bundle) # 方法B不推荐全局修改但有时必要临时替换默认证书路径 import os os.environ[REQUESTS_CA_BUNDLE] custom_bundle os.environ[SSL_CERT_FILE] custom_bundle # 影响更底层的库 response requests.get(https://site1.com) # 现在会使用自定义包4.2 使用certifi管理证书certifi是一个提供Mozilla维护的权威CA证书集的Python包。requests默认使用它。你也可以利用它来管理自定义证书。import certifi from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.ssl_ import create_urllib3_context # 创建一个自定义的SSL上下文加载额外证书 def create_ssl_context_with_extra_ca(ca_cert_path): context create_urllib3_context() context.load_verify_locations(cafileca_cert_path) # 加载额外CA # 注意这里不是替换是额外加载。系统默认的certifi证书仍在。 return context # 创建适配器并使用 class CustomCAAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): context create_ssl_context_with_extra_ca(/path/to/extra_ca.crt) kwargs[ssl_context] context return super().init_poolmanager(*args, **kwargs) session requests.Session() session.mount(https://internal-domain.com, CustomCAAdapter()) response session.get(https://internal-domain.com/api/data)这种方法更为精细可以为特定域名挂载特定的证书适配器不影响其他网站的访问。实操心得获取正确的证书文件是关键。有时服务器发送的是证书链你需要确保你的证书文件包含了完整的链服务器证书中间CA证书或者直接使用根CA证书。可以使用浏览器访问该网站点击地址栏锁图标 - “连接是安全的” - “证书信息”然后导出证书通常选择“PEM (证书)”格式来快速获取服务器证书链。但这通常只包含服务器和中间证书可能不包含根证书对于内部CA可能还是需要从IT部门获取根证书。合并证书包时确保是简单的文本拼接每个证书是标准的-----BEGIN CERTIFICATE-----和-----END CERTIFICATE-----格式。5. 方法三修复系统/Python环境证书库根治方法这是最一劳永逸的方法旨在修复unable to get local issuer certificate这个根本原因——让系统或Python环境能够找到正确的颁发者证书。5.1 更新系统的根证书Ubuntu/Debian:sudo apt update sudo apt install --reinstall ca-certificates sudo update-ca-certificates --freshCentOS/RHEL/Fedora:sudo yum update ca-certificates # 或 sudo dnf update ca-certificatesmacOS: 系统会自动更新。你也可以通过钥匙串访问应用检查“系统根证书”是否正常。Windows: 通过Windows Update更新或在“运行”中输入certmgr.msc管理计算机证书。5.2 更新Python的certifi证书包certifi包可能过时。更新它pip install --upgrade certifi升级后certifi.where()会返回新的证书包路径。5.3 安装缺失的特定根证书到系统库如果问题是某个特定的公共CA比如Let‘s Encrypt的ISRG Root X1不在你的旧系统库中你需要手动安装它。以Let‘s Encrypt的ISRG Root X1证书为例从权威网站下载根证书如从 Let‘s Encrypt官网 下载isrgrootx1.pem。将其放入系统证书目录并更新。Ubuntu/Debian:sudo cp isrgrootx1.pem /usr/local/share/ca-certificates/ sudo update-ca-certificatesCentOS/RHEL:sudo cp isrgrootx1.pem /etc/pki/ca-trust/source/anchors/ sudo update-ca-trust extract对于Python确保它使用的是系统证书库或者重启Python进程使其生效。5.4 强制Python使用系统证书库有时Python特别是某些虚拟环境或打包环境可能没有正确指向系统证书。可以尝试设置环境变量# 在运行Python脚本之前设置 export SSL_CERT_FILE/etc/ssl/certs/ca-certificates.crt # Linux典型路径 export REQUESTS_CA_BUNDLE/etc/ssl/certs/ca-certificates.crt或者在代码中设置import os os.environ[REQUESTS_CA_BUNDLE] /etc/ssl/certs/ca-certificates.crt os.environ[SSL_CERT_FILE] /etc/ssl/certs/ca-certificates.crt实操心得在Docker容器中运行爬虫时经常遇到证书问题。一个良好的实践是在Dockerfile中基础镜像阶段就更新证书库。FROM python:3.9-slim RUN apt-get update apt-get install -y --no-install-recommends ca-certificates rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, main.py]如果你使用的是pyinstaller等工具打包的独立可执行文件证书路径可能会被打包进二进制文件。需要测试在目标机器上是否正常工作可能需要通过--add-data参数将自定义证书包一起打包。6. 高级场景与疑难排查6.1 处理证书域名不匹配错误有时证书是有效的但访问的域名和证书中声明的域名SAN不匹配会报错CertificateError: hostname ‘xxx‘ doesn‘t match‘。。对于爬虫这可能发生在访问IP地址直接对应HTTPS服务。访问内部域名但证书是通配符或另一个域名。解决方案自定义主机名验证。警告这同样会降低安全性需谨慎使用。import ssl import requests from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager class InsecureHostnameAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): # 创建一个不验证主机名的SSL上下文 context ssl.create_default_context() context.check_hostname False context.verify_mode ssl.CERT_NONE # 注意这里连证书本身也不验证了 kwargs[ssl_context] context return super().init_poolmanager(*args, **kwargs) # 更安全的方式仅禁用主机名检查但仍验证证书有效性需要先按方法二信任证书 class CustomHostnameAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): context ssl.create_default_context() context.check_hostname False # context.verify_mode ssl.CERT_REQUIRED # 默认就是REQUIRED context.load_verify_locations(cafile/path/to/trusted_ca.crt) kwargs[ssl_context] context return super().init_poolmanager(*args, **kwargs) session requests.Session() session.mount(https://, CustomHostnameAdapter()) # 为所有HTTPS连接挂载 # 或 session.mount(https://specific-ip.com, CustomHostnameAdapter()) response session.get(https://192.168.1.100/api)6.2 调试与诊断技巧当遇到SSL错误时不要盲目尝试先诊断使用OpenSSL命令行诊断openssl s_client -connect example.com:443 -showcerts这个命令会输出服务器返回的完整证书链。仔细查看最后是否包含Verify return code: 0 (ok)。如果不是0会给出具体错误码。查看输出中的证书链看中间证书和根证书是否齐全。在Python中获取证书信息import ssl import socket hostname ‘example.com‘ context ssl.create_default_context() with socket.create_connection((hostname, 443)) as sock: with context.wrap_socket(sock, server_hostnamehostname) as ssock: cert ssock.getpeercert() print(ssl.DER_cert_to_PEM_cert(ssock.getpeercert(binary_formTrue)))这可以帮助你查看Python实际接收到的证书内容。检查requests使用的证书路径import requests print(requests.certs.where()) # 旧版本 import certifi print(certifi.where()) # 新版本/直接使用certifi6.3 网络代理环境下的SSL问题如果你的爬虫通过公司代理如Zscaler, Blue Coat等上网这些代理通常会进行SSL中间人解密。这意味着你的爬虫实际是与代理服务器进行SSL握手而不是目标网站。代理服务器会使用它自己的证书通常是公司内部CA签发来与你建立连接。解决方案你必须将公司代理的根证书安装到你的爬虫运行环境的信任库中即方法二。通常公司IT部门会提供这个根证书。将其安装到系统或指定给requests使用。常见报错在这种情况下错误信息可能不仅仅是unable to get local issuer certificate还可能伴随[SSL: TLSV1_ALERT_UNKNOWN_CA]等提示核心原因都是不信任代理CA的证书。7. 安全实践总结与最终建议面对SSL证书验证失败我们的应对策略应该有一个清晰的优先级首选安全方法三 方法二。首先尝试更新系统/Python证书库到最新。如果问题依旧确认是否为自签名/内部CA证书然后获取并信任该特定CA证书。这是唯一适用于生产环境的方案。次选临时/调试方法一。仅在绝对安全的测试环境或快速调试时使用并务必添加警告禁用和清晰注释。高级定制对于复杂场景如特定域名绕过主机名检查、代理环境使用自定义HTTPAdapter和SSLContext进行精细控制。给爬虫开发者的最终建议尊重robots.txt在开始爬取任何网站前检查其robots.txt文件遵守规则控制请求速率使用time.sleep或更智能的限流避免对目标网站造成过大压力。这是负责任的爬虫伦理。设置合理的请求头模拟真实浏览器User-Agent处理Cookie和Session。处理异常网络请求充满不确定性SSL错误只是其中一种。务必使用try...except包裹请求并合理处理requests.exceptions.SSLError,ConnectionError,Timeout等异常。考虑使用Scrapy等框架对于大型爬虫项目使用Scrapy框架可以更好地管理请求、并发、去重和异常处理其内置的RetryMiddleware也能更好地应对临时性SSL错误。SSL证书验证是网络安全的重要基石。作为爬虫开发者我们既要克服技术障碍让程序跑起来更要理解背后的安全逻辑避免在无意中引入严重的安全漏洞。希望这三种方法能成为你工具箱中的得力助手助你高效、安全地畅游数据世界。