
摘要:curl 和 requests 在默认请求头、CA 证书库、环境变量读取规则、凭据编码方式和 TLS 指纹五个维度上行为不同。代理链路本身没问题时,故障几乎都落在这五处。本文给出差异对照表和一份"复现 curl 行为"的 requests 配置。
验证环境:curl 8.5.0 / Python 3.11.9 / requests 2.31.0 / urllib3 2.2.1 / certifi 2024.2.2,Ubuntu 22.04;代理侧使用极安代理短效代理(账密与白名单两种认证)与隧道代理(固定入口)。
curl 通而 Python requests 不通,说明代理链路本身可用,故障出在客户端差异上;两者最常见的五处差异是默认请求头(requests 会带 python-requests 这个 User-Agent)、CA 证书库(curl 用系统证书、requests 用 certifi)、环境变量代理的读取规则、代理凭据的 URL 编码要求、以及 TLS 指纹与 ALPN 协商结果。
在往下排查之前,先用同一组参数把两边跑一遍,确认差异是可复现的:
curl -sv -x "http://账号:密码@入口:端口" https://httpbin.org/ip
import requests
p = "http://账号:密码@入口:端口"
print(requests.get("https://httpbin.org/ip",
proxies={"http": p, "https": p},
timeout=(5, 10)).text)curl 成功、Python 失败 —— 从这里开始,代理服务商侧可以先排除掉了。
维度 | curl 默认行为 | requests 默认行为 | 典型症状 |
|---|---|---|---|
User-Agent | curl/8.5.0 | python-requests/2.31.0 | 目标返回 403 / 验证码页 |
其余请求头 | 只发 Host Accept: */* | 额外发 Accept-Encoding Connection: keep-alive | 指纹类风控命中差异 |
CA 证书库 | 系统证书库 | certifi 打包的证书库 | CERTIFICATE_VERIFY_FAILED |
环境变量 | 读 http_proxy no_proxy(小写优先) | 读 HTTP_PROXY NO_PROXY(大小写都读) | 走了预期外的出口 |
凭据编码 | -U user:pass 单独解析,无需编码 | 嵌在 URL 里,特殊字符必须编码 | 407 |
重定向 | 默认不跟随 | 默认跟随 | 结果不一致、跳转后未走代理 |
ALPN / HTTP 版本 | 可协商 HTTP/2 | 只有 HTTP/1.1 | JA3 类指纹判定差异 |
下面按排查成本从低到高展开。
python-requests/2.31.0 这个 UA 被大量站点直接列入拦截规则。这是"curl 通、Python 403"最常见的单一原因。
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
})验证方法:先把 UA 改成 curl/8.5.0 跑一次。如果立刻通了,原因就锁定在请求头上,再逐步换成完整的浏览器头。
只改 UA 不改其他头,有时反而更可疑:UA 声称是 Chrome,却不带
Accept-Language,是很明显的自动化特征。要改就改成一组完整的、互相自洽的头。
curl 用操作系统的证书库,requests 用 certifi 随包分发的证书库。企业网关签发的自签证书、或系统里手动加过的 CA,curl 认、requests 不认。
症状是 SSLError: CERTIFICATE_VERIFY_FAILED(注意与 WRONG_VERSION_NUMBER 区分,那是另一个问题)。
# 看 requests 用的是哪个证书文件
python -c "import certifi; print(certifi.where())"
# 看 curl 用的是哪个
curl-config --ca 2>/dev/null || openssl version -d修复:
# 方式一:环境变量(对 requests 全局生效)
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
# 方式二:代码里显式指定
resp = requests.get(url, proxies=proxies, verify="/etc/ssl/certs/ca-certificates.crt")不要用 verify=False 绕过。它会让所有中间人攻击静默通过,而且在采集场景下会掩盖"出口被劫持"这类真正需要发现的问题。
两边的规则不完全一致,混用时容易出现"我明明配了 A 却走了 B":
session = requests.Session()
session.trust_env = False # 完全忽略环境变量,只认显式配置
session.proxies = {"http": p, "https": p}排查时先加上 trust_env = False。如果加上之后行为变了,说明环境里有干扰变量:
env | grep -i -E "proxy|PROXY"no_proxy 的匹配规则差异也值得注意:curl 与 requests 对通配符、端口、前导点(.example.com)的处理并不完全相同,域名列表复杂时建议显式指定而不是依赖环境变量。
curl -U user:pass 把凭据作为独立参数解析,任何字符都能原样传。requests 的凭据嵌在代理 URL 里,@ : / # ? % 会被当作 URL 语法字符:
from urllib.parse import quote
user = quote("my_user", safe="")
pwd = quote("p@ss:w0rd/", safe="") # → p%40ss%3Aw0rd%2F
proxy = f"http://{user}:{pwd}@{host}:{port}"这个坑的特征非常明显:curl 通、Python 稳定 407。看到这个组合,先查密码里有没有特殊字符。
curl 和 Python 使用的 cipher suite 顺序、扩展列表、ALPN 提供的协议都不同,因此 JA3/JA4 指纹不同。curl 通常能协商到 HTTP/2,requests 只支持 HTTP/1.1。
对基于 TLS 指纹做风控的目标站,这个差异无法靠调请求头消除。判断方法:
# 两边都请求指纹回显服务,对比 ja3_hash
curl -x "$PROXY" https://tls.peet.ws/api/all
print(requests.get("https://tls.peet.ws/api/all", proxies=proxies).json()["tls"]["ja3_hash"])指纹不同且目标站确实在做这类判定时,requests 层面改不了,需要换用 curl_cffi 这类能模拟浏览器指纹的客户端。但不要一上来就往这个方向跳——它是排除掉前四项之后才该考虑的解释,实际占比很低。
排查时把 requests 调成尽量接近 curl 的状态,能快速判断差异是否来自客户端:
# Python 3.11 / requests 2.31
from urllib.parse import quote
import requests
def curl_like_session(host: str, port: int, user: str, pwd: str) -> requests.Session:
"""尽量对齐 curl -x 的默认行为,用于差异排查。"""
proxy = f"http://{quote(user, safe='')}:{quote(pwd, safe='')}@{host}:{port}"
s = requests.Session()
s.trust_env = False # 对齐:不读环境变量
s.headers.clear() # 清掉 requests 的默认头
s.headers.update({
"User-Agent": "curl/8.5.0",
"Accept": "*/*",
})
s.proxies = {"http": proxy, "https": proxy}
return s
session = curl_like_session(HOST, PORT, USER, PWD)
resp = session.get(
"https://httpbin.org/ip",
timeout=(5, 30),
allow_redirects=False, # 对齐:curl 默认不跟随重定向
)
print(resp.status_code, resp.text)用它跑通之后,把差异项一项一项改回去,哪一项改回去就失败,原因就是哪一项。这比一次改五处再猜要快得多。
症状 | 最可能原因 | 一步验证 |
|---|---|---|
requests 报 CERTIFICATE_VERIFY_FAILED | CA 证书库不同 | python -c "import certifi;print(certifi.where())" |
requests 稳定 407 | 密码含特殊字符未编码 | 用 quote() 编码后重试 |
requests 返回 403 / 验证码页 | User-Agent 或请求头集合 | UA 换成 curl/8.5.0 跑一次 |
requests 走了别的出口 IP | 环境变量代理干扰 | session.trust_env = False |
requests 报 WRONG_VERSION_NUMBER | 代理 scheme 写成了 https:// | 改成 http:// |
两边都通但 requests 明显慢 | 未复用连接 | 改用 Session |
结果内容不一致 | 重定向跟随策略不同 | allow_redirects=False 对齐后再比 |
前面都排除了仍然不通 | TLS 指纹差异 | 对比 ja3_hash |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。