Python requests 遇到 BAD_ECPOINT 的终极解决方案一句话总结SSLError(1, [SSL: BAD_ECPOINT] bad ecpoint)发生在TLS 握手层与证书验证无关verifyFalse无效。根因是目标服务器使用 ECDH 压缩曲线OpenSSL 1.1 拒绝握手。Windows schannel 无此限制所以本地正常。最终方案用curl_cffi替代requests绕过 Python_ssl模块。问题场景湖南生态环境厅网站sthjt.hunan.gov.cn在 Docker 容器python:3.12-slim-bullseyeOpenSSL 1.1中请求时直接报错requests.exceptions.SSLError: HTTPSConnectionPool(hoststhjt.hunan.gov.cn, port443): Max retries exceeded (Caused by SSLError(SSLError(1, [SSL: BAD_ECPOINT] bad ecpoint (_ssl.c:1000))))但Windows 本地运行完全正常。根因分析为什么verifyFalse无效SSL/TLS 握手分为两层┌─────────────────────────────────┐ │ 证书验证层 (verifyTrue/False) │ ← verifyFalse 只跳过这一层 ├─────────────────────────────────┤ │ TLS 握手层 (密钥交换/加密套件) │ ← BAD_ECPOINT 发生在这里 └─────────────────────────────────┘BAD_ECPOINT发生在 TLS 握手阶段的ECDH 密钥交换环节——服务器返回了压缩格式的椭圆曲线点compressed EC pointOpenSSL 1.1.0 出于安全考虑拒绝接受。这是握手层面的拒绝比证书验证更底层。为什么 Windows 正常Linux 报错Python 的 SSL/TLS 能力来自 CPython 编译时链接的 SSL 后端平台SSL 后端对压缩 EC 点的态度Windowsschannel系统自带容忍Linux (OpenSSL 1.1)OpenSSL拒绝Linux (OpenSSL 3.x)OpenSSL拒绝macOSSecure Transport容忍关键点Python 的_ssl模块是 C 扩展在 CPython 编译时就链接好了运行时无法替换。apt install openssl或pip install pyOpenSSL都改变不了_ssl的行为。尝试过的无效方案按时间顺序以下方案均失败方案结果原因verify_ssl False❌错误在握手层不在证书层_LegacySSLAdapterSECLEVEL1❌安全级别与 EC 点压缩是正交问题_LegacySSLAdapterSECLEVEL0OP_LEGACY_SERVER_CONNECT❌同上OpenSSL 1.1 根本不允许压缩 EC 点降级到python:3.12-slim-bullseyeOpenSSL 1.1❌OpenSSL 1.1.0 开始就拒绝压缩 EC 点了云端浏览器 API 代理❌API 服务端自身崩溃api_server.py内部报错最终方案curl_cffi原理curl_cffi 通过 CFFI 直接调用 libcurl完全不经过 Python 的_ssl模块。TLS 握手由 curl 自己的 SSL 后端处理从根本上规避了 Python_ssl的限制。此外它还支持TLS 指纹伪装模拟 Chrome/Firefox/Safari 的 JA3 指纹顺带解决基于 TLS 指纹的反爬。安装pipinstallcurl_cffi支持 Python 3.10有预编译的 manylinux/musllinux wheel无需编译。代码改动改动量极小——curl_cffi的 API 与requests完全兼容# 之前importrequests sessionrequests.Session()# 之后fromcurl_cffiimportrequests sessionrequests.Session(impersonatechrome124)# 模拟 Chrome 124 TLS 指纹完整代码湖南 pipeline 实际改动fromcurl_cffiimportrequestsclassHunanPipeline(BasePipeline):province湖南def_create_session(self)-requests.Session:使用 curl_cffi SessionChrome 指纹 libcurl TLS绕过 BAD_ECPOINTsessionrequests.Session(impersonatechrome124)session.headers.update({User-Agent:Mozilla/5.0 ... Chrome/124.0.0.0 Safari/537.36,Accept:text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,})returnsessiondefcollect(self,session,year_filter,first_page_onlyFalse):# ... session.get(url) 用法完全不变respsession.get(url,timeout30)defprocess_item(self,session,item):# ... 同样不变respsession.get(item[url],timeout30)不需要verify_ssl False、不需要_LegacySSLAdapter、不需要改 Dockerfile 基础镜像。注意事项curl_cffi的异常类继承自自己的curl_cffi.requests.errors.RequestsError不是标准库的requests.RequestException。如果基类用except requests.RequestException做重试需要改为同时捕获两者的基类或用except Exception兜底。impersonate参数可选的浏览器版本chrome110、chrome120、chrome124、firefox、safari等。国内政府网站建议用chrome124。详情页的超时时间建议 30s政府网站响应可能较慢。推广适用场景这个方案不仅适用于BAD_ECPOINT同样的思路可以解决其他 Python_ssl层面的问题SSLError: unsupported protocol— 服务器仅支持老旧 TLS 版本SSLError: dh key too small— DH 密钥长度不足SSLError: no shared cipher— 加密套件不兼容TLS 指纹反爬JA3 检测只要问题根因在 Python_ssl模块层面且无法通过升级/配置 OpenSSL 解决就可以考虑用curl_cffi绕开。参考博客方案参考解决Python报错SSLError如果试了网上一大堆方法还不行看看这个吧 — 感谢 kikn0928 的分享curl_cffi GitHubcurl_cffi PyPI