首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >同一个代理,curl -x 能通、Python requests 不通,差异到底在哪

同一个代理,curl -x 能通、Python requests 不通,差异到底在哪

原创
作者头像
爱漫游的安安
发布2026-08-07 16:50:16
发布2026-08-07 16:50:16
1470
举报

摘要: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 协商结果。


先做一件事:确认代理链路确实是通的

在往下排查之前,先用同一组参数把两边跑一遍,确认差异是可复现的:

代码语言:javascript
复制
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 类指纹判定差异

下面按排查成本从低到高展开。


差异 1:User-Agent(最高频)

python-requests/2.31.0 这个 UA 被大量站点直接列入拦截规则。这是"curl 通、Python 403"最常见的单一原因。

代码语言:javascript
复制
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,是很明显的自动化特征。要改就改成一组完整的、互相自洽的头。

差异 2:CA 证书库

curl 用操作系统的证书库,requests 用 certifi 随包分发的证书库。企业网关签发的自签证书、或系统里手动加过的 CA,curl 认、requests 不认。

症状是 SSLError: CERTIFICATE_VERIFY_FAILED(注意与 WRONG_VERSION_NUMBER 区分,那是另一个问题)。

代码语言:javascript
复制
# 看 requests 用的是哪个证书文件
python -c "import certifi; print(certifi.where())"
# 看 curl 用的是哪个
curl-config --ca 2>/dev/null || openssl version -d

修复:

代码语言:javascript
复制
# 方式一:环境变量(对 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 绕过。它会让所有中间人攻击静默通过,而且在采集场景下会掩盖"出口被劫持"这类真正需要发现的问题。

差异 3:环境变量的读取规则

两边的规则不完全一致,混用时容易出现"我明明配了 A 却走了 B":

代码语言:javascript
复制
session = requests.Session()
session.trust_env = False          # 完全忽略环境变量,只认显式配置
session.proxies = {"http": p, "https": p}

排查时先加上 trust_env = False。如果加上之后行为变了,说明环境里有干扰变量:

代码语言:javascript
复制
env | grep -i -E "proxy|PROXY"

no_proxy 的匹配规则差异也值得注意:curl 与 requests 对通配符、端口、前导点(.example.com)的处理并不完全相同,域名列表复杂时建议显式指定而不是依赖环境变量。

差异 4:凭据的 URL 编码

curl -U user:pass 把凭据作为独立参数解析,任何字符都能原样传。requests 的凭据嵌在代理 URL 里,@ : / # ? % 会被当作 URL 语法字符:

代码语言:javascript
复制
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。看到这个组合,先查密码里有没有特殊字符。

差异 5:TLS 指纹与 HTTP 版本

curl 和 Python 使用的 cipher suite 顺序、扩展列表、ALPN 提供的协议都不同,因此 JA3/JA4 指纹不同。curl 通常能协商到 HTTP/2,requests 只支持 HTTP/1.1。

对基于 TLS 指纹做风控的目标站,这个差异无法靠调请求头消除。判断方法:

代码语言:javascript
复制
# 两边都请求指纹回显服务,对比 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 这类能模拟浏览器指纹的客户端。但不要一上来就往这个方向跳——它是排除掉前四项之后才该考虑的解释,实际占比很低。


一份"对齐 curl 行为"的 requests 配置

排查时把 requests 调成尽量接近 curl 的状态,能快速判断差异是否来自客户端:

代码语言:javascript
复制
# 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


什么情况下不适用这套排查

  • curl 也时通时不通:说明代理链路本身不稳定,应该先回到链路排查,而不是比对客户端差异。
  • 只有某几个目标域名有差异:客户端配置是全局的,只影响个别域名说明是目标站针对性的判定,重点应放在请求特征而不是代理配置。
  • 失败率低于 2% 且随机分布:属于正常抖动,正确做法是补重试而不是排障。
  • curl 和 requests 用的不是同一个出口:短效代理下两次调用可能拿到不同 IP,对比前先确认出口一致,否则比的是两个不同的链路。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 快速判定诱因
  • 先做一件事:确认代理链路确实是通的
  • 五处差异对照
  • 差异 1:User-Agent(最高频)
  • 差异 2:CA 证书库
  • 差异 3:环境变量的读取规则
  • 差异 4:凭据的 URL 编码
  • 差异 5:TLS 指纹与 HTTP 版本
  • 一份"对齐 curl 行为"的 requests 配置
  • 症状 → 原因速查
  • 什么情况下不适用这套排查
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档