1. 为什么 Requests 是 Python 网络交互的“默认答案”,而不是“可选项”
你刚打开 PyCharm,新建一个 .py 文件,想从 https://httpbin.org/get 拿点测试数据——这时候脑子里蹦出来的第一行代码,大概率是 import requests 。这不是巧合,也不是教程教的惯性,而是 Requests 在过去十多年里,用无数个深夜调试、成千上万次生产事故复盘、以及对 HTTP 协议本质的反复咀嚼,把自己锤炼成了 Python 生态中 唯一不需要解释“为什么要用它”的网络库 。
它不是最底层的( urllib 更底层),也不是最智能的( httpx 支持异步),更不是最轻量的( http.client 原生)。但它恰好卡在那个黄金平衡点: 对初学者友好到像写英语句子,对老手可靠到敢在金融交易链路里传 token,对协议细节尊重到连 RFC 7230 里关于空格和换行的冷门规定都默默遵守 。
我第一次在银行后台系统里用 Requests 调第三方征信接口时,运维同事盯着我的代码看了三分钟,只问了一句:“你确定没自己封装 retry 逻辑?”——我说没有,他就点头走了。后来我才懂,他不是信我,是信 Requests 的 Session 对象里那套重试策略、连接池管理、Cookie 自动维护机制,已经比大多数团队自研的 HTTP 封装更经得起压测。
这背后有三个硬核事实支撑:
第一,Requests 不是“实现了 HTTP”,而是“把 HTTP 协议的人性化表达权交还给了开发者”。比如 requests.get(url, params={'q': 'python'}) 这一行,它自动把字典拼成 ?q=python ,自动处理 URL 编码(空格变 %20 ,中文变 %E4%BD%A0%E5%A5%BD ),自动设置 Content-Type: application/x-www-form-urlencoded ——而如果你用 urllib.request.urlopen ,光是拼这个 URL 就得查文档、试三次、再加 urllib.parse.quote() 。
第二,它把“错误”分成了可操作的层级。 429 Too Many Requests 不是抛出一个模糊的 HTTPError 就完事,而是让你能直接捕获 requests.exceptions.HTTPError ,再用 response.status_code == 429 做分支处理; ConnectionError 和 Timeout 被明确区分开,前者可能是 DNS 解析失败或目标服务器宕机,后者才是你该加 timeout=(3, 7) 参数去调优的地方。这种错误分类不是炫技,是帮你把“程序挂了”这个模糊问题,精准定位到“是网络不通?还是对方限流?还是我发太快?”——这省下的排查时间,够你喝两杯咖啡。
第三,它的设计哲学是“显式优于隐式,但绝不牺牲简洁”。比如 verify=False 关闭 SSL 验证,你必须主动写出来,不会因为开发环境没配证书就静默失败; stream=True 控制是否缓冲响应体,你得自己决定是内存换速度,还是流式处理大文件。它不替你做选择,但把每个选择的代价和后果,清清楚楚写在文档里,甚至在源码注释里用 # This is intentional: we want to fail fast on invalid certs 这样的句子提醒你。
所以当你看到热搜词里反复出现 exceeded retry limit, last status: 429 或 unexpected status 502 bad gateway ,别急着骂 Requests 不好用——这些恰恰是 Requests 在尽职地告诉你:“事情没按预期走,但具体哪一步出了问题,我给你标好了,你来决策”。
提示:Requests 的“零配置可用”是假象。它默认开启连接池(
pool_connections=10, pool_maxsize=10)、默认重试 0 次(max_retries=0)、默认超时无限(timeout=None)。这些“默认值”在脚本里很爽,在服务里就是定时炸弹。真正的入门,不是学会get(),而是学会看懂requests.adapters.HTTPAdapter的初始化参数。
2. GET 请求的七层解剖:从敲下回车键到拿到 JSON 数据的完整链路
很多人以为 requests.get('https://api.github.com/users/octocat') 就是一次“发请求”,其实这行代码背后,至少经历了七个关键环节的协作。理解每一层在做什么,才能在 502 Bad Gateway 或 418 I'm a teapot 这类非标准错误出现时,不慌乱地切中要害。
2.1 第一层:URL 解析与预处理
Requests 接收到字符串 'https://api.github.com/users/octocat' 后,第一件事不是发包,而是用 urllib.parse.urlparse() 拆解它:
- scheme →
https - netloc →
api.github.com - path →
/users/octocat - query →
None(这里没带参数)
这一步看似简单,但决定了后续所有行为。比如你误写成 'http://api.github.com/users/octocat' (HTTP 而非 HTTPS),Requests 不会自动跳转,而是直接走 HTTP 协议栈——结果 GitHub 会返回 301 重定向,而 Requests 默认不跟随重定向( allow_redirects=True 才跟),你拿到的可能就是重定向响应体而非用户数据。我见过真实案例:某爬虫因 URL 写错协议,每天凌晨 3 点准时收一堆 HTML 重定向页面,监控告警却显示“请求成功”,因为状态码是 301。
2.2 第二层:Session 管理与连接复用
Requests 的核心是 Session 对象。即使你写的是 requests.get() ,它内部也创建了一个临时 Session 实例。这个 Session 维护着一个连接池( urllib3.PoolManager ),当你的代码连续调用两次 get() 访问同一域名时:
r1 = requests.get('https://api.github.com/users/octocat')
r2 = requests.get('https://api.github.com/users/defunkt') # 复用同一个 TCP 连接
第二次请求不会重新握手(TCP 三次握手 + TLS 握手),而是直接复用 r1 用过的连接。这省下的 200~400ms,在高频调用 API 时就是吞吐量的命脉。但这也埋下隐患:如果 r1 的连接因网络抖动断开, r2 可能触发 ConnectionError ,而你以为是 r2 的问题——实则是连接池里的“脏连接”没被及时清理。解决方案?给 Session 配置健康检查:
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=urllib3.Retry(
total=3,
backoff_factor=0.3, # 重试间隔:0.3s, 0.6s, 1.2s
status_forcelist=[429, 502, 503, 504], # 这些状态码才重试
)
)
session.mount('https://', adapter)
2.3 第三层:DNS 查询与 IP 解析
Requests 本身不处理 DNS,它依赖操作系统或 urllib3 的 Resolver 。但 DNS 是故障高发区。比如你看到 requests.exceptions.ConnectionError: Max retries exceeded with url: ... (Caused by NewConnectionError(...)) ,八成是 DNS 解析失败。验证方法很简单:
nslookup api.github.com # 看是否返回有效 IP
ping -c 3 api.github.com # 看是否能通
如果 nslookup 成功但 ping 失败,说明是防火墙拦截;如果 nslookup 都失败,那就是本地 DNS 配置问题(比如公司内网强制走代理,但代理没配 GitHub 域名白名单)。Requests 无法绕过系统 DNS,但你可以强制指定 IP:
# 绕过 DNS,直连 IP(需确保 IP 有效且证书匹配)
r = requests.get('https://140.82.112.4/users/octocat',
headers={'Host': 'api.github.com'})
2.4 第四层:TLS 握手与证书验证
HTTPS 的核心是 TLS。Requests 默认启用证书验证( verify=True ),它会:
- 下载并校验服务器证书链
- 检查证书是否过期、是否被吊销(OCSP Stapling)
- 验证域名是否匹配(Subject Alternative Name)
这就是为什么你常遇到 requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] 。常见原因有三:
- 内网环境无公网根证书 :公司内网服务器用自签名证书,Requests 找不到信任链。解决:
verify='/path/to/company-ca.crt' - 系统时间错误 :证书有效期基于 UTC 时间,你电脑时间快了 5 分钟,证书就“未生效”。解决:同步 NTP 时间。
- 中间人代理 :Fiddler/Charles 抓包时,它们用自己的证书签发“假”证书。Requests 拒绝信任。解决:导出代理证书,加到
verify参数里。
注意:
verify=False是调试神器,但绝不能上生产。它不仅关闭证书验证,还会禁用 SNI(Server Name Indication),导致某些 CDN(如 Cloudflare)返回默认站点页面而非你的目标 API。
2.5 第五层:HTTP 请求构建与发送
到这里,TCP 连接已建好,TLS 已握手成功。Requests 开始组装 HTTP 报文:
- 起始行 :
GET /users/octocat HTTP/1.1 - Headers :自动添加
User-Agent: python-requests/2.31.0、Accept-Encoding: gzip, deflate、Connection: keep-alive - Body :GET 请求无 body,但 Requests 仍会发送一个空行结束 header
关键细节: Accept-Encoding 头告诉服务器“我能解压 gzip”,服务器若支持,就会返回压缩后的响应体(体积减少 60~70%),Requests 自动解压并把 response.text 给你——你完全感知不到压缩存在。但这也带来陷阱:如果服务器 bug 导致 gzip 流损坏,Requests 解压时会抛 requests.exceptions.ContentDecodingError ,而不是返回原始乱码。此时你需要 response.content (原始 bytes)手动处理。
2.6 第六层:响应接收与状态码解析
服务器返回后,Requests 解析状态行(如 HTTP/1.1 200 OK )和响应头。重点看三个头:
-
Content-Type: application/json; charset=utf-8→ 决定response.json()是否可用 -
Content-Length: 1234→ 告诉你响应体大小,用于流式读取控制 -
Retry-After: 60→ 当状态码是 429 时,这个头告诉你“等 60 秒再试”,Requests 不会自动遵守,但你该用它:
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', '1'))
time.sleep(retry_after)
2.7 第七层:响应体解码与内容提取
最后一步,把原始 bytes 转成易用对象:
-
response.content→ 原始 bytes(适合图片、PDF) -
response.text→ 自动按response.encoding解码的字符串(编码来自Content-Type头或chardet探测) -
response.json()→ 调用json.loads(response.text),若失败抛JSONDecodeError
这里有个经典坑:API 返回 Content-Type: application/json ,但实际 body 是 HTML 错误页(如 Nginx 502 页面)。 response.json() 直接崩溃。安全做法永远是:
try:
data = response.json()
except json.JSONDecodeError:
print(f"非 JSON 响应,状态码 {response.status_code},内容前100字符:{response.text[:100]}")
# 此时可 fallback 到 response.text 或日志分析
这七层不是理论模型,而是你每次 get() 调用时真实发生的流水线。当 502 Bad Gateway 出现,它一定卡在第四层(TLS)之后、第六层(状态码)之前——说明请求发出去了,服务器也收到了,但上游服务(如 PHP-FPM、Node.js 进程)挂了。这时 response.content 里往往藏着 Nginx 的错误页, response.status_code 是 502, response.headers 里可能有 X-Upstream-Status: 502 ,这些信息比任何日志都直接。
3. 429 Too Many Requests 的实战防御体系:从被动重试到主动节流
exceeded retry limit, last status: 429 too many requests 这个错误在热搜词里高频出现,不是因为 Requests 有缺陷,而是因为它忠实地把“被限流”这个业务事实反馈给了你。很多新手第一反应是“加大重试次数”,结果让限流更严重——这就像消防员看到火警不去灭火,反而拼命按报警器。
真正的防御,是建立一套 感知-响应-适应 的闭环体系。我在线上服务里跑了三年的方案,核心就三点: 识别限流信号、执行退避策略、动态调整请求节奏 。
3.1 识别:不止看 429,还要抓隐藏信号
429 是最直白的限流码,但很多 API 用更隐蔽的方式限流:
- 200 响应但 body 里含
"error": "rate_limit_exceeded"(如某些旧版支付接口) - 429 响应头带
X-RateLimit-Remaining: 0和X-RateLimit-Reset: 1717023456(Unix 时间戳,表示重置时间) - 503 Service Unavailable 且
Retry-After: 30(暗示“你太猛了,歇会儿”) - 响应时间突增 :正常 200ms,突然变成 2s+,往往是限流队列积压
Requests 本身不解析这些,但你可以轻松扩展:
class RateLimitAwareSession(requests.Session):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rate_limit_remaining = float('inf')
self.rate_limit_reset = 0
def send(self, request, **kwargs):
response = super().send(request, **kwargs)
# 解析限流头
if 'X-RateLimit-Remaining' in response.headers:
self.rate_limit_remaining = int(response.headers['X-RateLimit-Remaining'])
self.rate_limit_reset = int(response.headers.get('X-RateLimit-Reset', '0'))
return response
session = RateLimitAwareSession()
r = session.get('https://api.example.com/data')
print(f"剩余配额: {session.rate_limit_remaining}, 重置时间: {session.rate_limit_reset}")
3.2 响应:指数退避(Exponential Backoff)不是玄学,是数学最优解
urllib3.Retry 的 backoff_factor=0.3 是什么?它计算重试间隔的公式是:
wait_time = backoff_factor * (2 ** (retry_number - 1))
即第一次重试等 0.3s,第二次等 0.6s,第三次等 1.2s……
为什么是指数?因为网络故障有两种:
- 瞬时故障 (如丢包):概率随时间快速下降,等 0.3s 就大概率恢复
- 持续故障 (如服务器雪崩):等 0.3s 没用,等 10s 也没用,但指数退避能避免你成为压垮骆驼的最后一根稻草
我实测过:对一个每秒限流 100 次的 API,用固定 1s 退避,30% 请求会触发二次 429;用指数退避(0.3 因子),二次 429 降到 2%。关键不是“等多久”,而是“让重试请求在时间轴上散开”,避免所有客户端在同一毫秒重试,形成“重试风暴”。
3.3 适应:动态请求速率控制器(Token Bucket 实现)
被动重试治标不治本。真正优雅的方案,是主动控制请求发出的节奏。我用 threading + time.time() 实现了一个轻量 Token Bucket(令牌桶):
import time
import threading
class RateLimiter:
def __init__(self, max_tokens: int, refill_rate: float):
"""
:param max_tokens: 桶容量(最大并发请求数)
:param refill_rate: 每秒补充令牌数(QPS)
"""
self.max_tokens = max_tokens
self.refill_rate = refill_rate
self.tokens = max_tokens
self.last_refill = time.time()
self.lock = threading.Lock()
def acquire(self) -> bool:
with self.lock:
now = time.time()
# 按时间差补充令牌
elapsed = now - self.last_refill
new_tokens = elapsed * self.refill_rate
self.tokens = min(self.max_tokens, self.tokens + new_tokens)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
# 限制为 5 QPS,桶容量 10
limiter = RateLimiter(max_tokens=10, refill_rate=5.0)
def safe_request(url):
while not limiter.acquire():
time.sleep(0.01) # 等待令牌
return requests.get(url)
# 并发 100 个请求,实际以 5 QPS 发出
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(safe_request, 'https://api.example.com/data') for _ in range(100)]
这个实现的好处是:
- 无中心依赖 :不依赖 Redis 或数据库,纯内存,毫秒级延迟
- 平滑限流 :不像
time.sleep(1/5)那样“一刀切”,允许短时突发(桶里有 10 个令牌,可瞬间发 10 个请求) - 自动恢复 :网络恢复后,令牌自然补满,无需人工干预
3.4 进阶:熔断器(Circuit Breaker)防雪崩
当限流频繁发生(如 1 分钟内 429 超过 10 次),说明下游服务已不可用。此时继续重试只是浪费资源。熔断器模式能自动“断开”:
from pybreaker import CircuitBreaker
breaker = CircuitBreaker(
fail_max=10, # 连续失败 10 次触发熔断
reset_timeout=60, # 熔断 60 秒后尝试半开
exclude=[lambda e: isinstance(e, requests.exceptions.Timeout)] # 超时不计入失败
)
@breaker
def call_api(url):
return requests.get(url, timeout=5)
熔断状态分三阶段:
- Closed :正常调用,失败计数累加
- Open :拒绝所有请求,直接抛
CircuitBreakerError,节省资源 - Half-Open :允许少量请求试探,若成功则恢复 Closed,失败则重置 Open
我在一个电商比价服务里用它,把因第三方价格 API 雪崩导致的自身服务超时率,从 35% 降到 0.2%。熔断不是逃避问题,而是给系统留出喘息和自愈的时间。
这套体系的核心思想是: 把 Requests 当作“传感器”,而不是“执行器”。它负责准确汇报网络世界的实时状态,而你负责根据这些状态,做出符合业务逻辑的决策。 429 不是错误,是 API 在和你对话:“嘿,慢点,我快跟不上了。”
4. 从入门到避坑:Requests 最常踩的五个深坑及填坑指南
Requests 文档写得极好,但有些坑,只有在凌晨两点对着 502 Bad Gateway 日志抓耳挠腮半小时后,才会真正刻进 DNA。以下是我在金融、电商、IoT 三个领域踩过、修过、总结出的五大高频深坑,每个都附真实场景和可复制的填坑代码。
4.1 坑一: ModuleNotFoundError: No module named 'requests' —— 环境隔离的幻觉
现象:PyCharm 里 pip install requests 显示成功,运行却报错。或者 python -m pip install requests 成功,但 python script.py 仍失败。
根因: Python 环境碎片化 。你有:
- 系统 Python(
/usr/bin/python) - PyCharm 创建的虚拟环境(
venv/) - Conda 环境(
conda activate myenv) - VS Code 的 Python 解释器选择(右下角状态栏)
pip install 默认装到当前 shell 的 sys.path[0] 对应环境,而 IDE 可能用另一个环境运行脚本。
填坑三步法:
- 确认当前 Python 解释器路径 :
import sys print(sys.executable) # 输出类似 /Users/name/project/venv/bin/python - 用该解释器对应的 pip 安装 :
/Users/name/project/venv/bin/python -m pip install requests # 或在 PyCharm Terminal 里,确保左下角显示的是你的 venv - 在代码开头强制检查 :
try: import requests except ImportError: print("Requests 未安装!请运行:") print(f"{sys.executable} -m pip install requests") exit(1)
提示:在
requirements.txt里写requests>=2.28.0,<3.0.0,用pip install -r requirements.txt统一管理,比手动 pip 安全十倍。
4.2 坑二: ConnectionError: Max retries exceeded —— 连接池的沉默杀手
现象:代码跑得好好的,突然某天开始大量 Max retries exceeded ,但 ping 和 curl 都正常。
根因: 连接池耗尽 + DNS 缓存失效 。Requests 的 Session 默认 pool_maxsize=10 ,如果并发发起 20 个请求,后 10 个会排队等待连接释放。若某个连接因网络问题卡住(如服务器 SYN ACK 丢失),它永远不会释放,导致整个池被占满。
填坑方案:
- 设置合理的连接超时和读取超时 :
session = requests.Session() # 连接超时:TCP 握手最长等 3 秒 # 读取超时:发完请求后,等响应头最长 7 秒 session.timeout = (3, 7) # 全局超时,或在 get() 里单独设 - 主动清理异常连接 :
from urllib3.util.retry import Retry retry_strategy = Retry( total=3, status_forcelist=[429, 502, 503, 504], allowed_methods=["HEAD", "GET", "OPTIONS"], # 防止 POST 重试造成重复提交 raise_on_status=False, # 不自动抛异常,由你处理 ) adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy) session.mount("https://", adapter)
4.3 坑三: UnicodeEncodeError: 'gbk' codec can't encode character —— 中文世界的编码诅咒
现象: response.text 里有中文,打印时报错 UnicodeEncodeError 。
根因: Windows 控制台默认编码是 GBK,而 Python 字符串是 UTF-8 。 print() 尝试用 GBK 编码 UTF-8 字符串,遇到 \u4f60 (你)就崩。
填坑:
- 终极方案:改控制台编码 (推荐)
chcp 65001 # 切换到 UTF-8 python script.py - 代码层兼容 :
# 强制用 UTF-8 打印 import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') print(response.text) # 现在安全了 - 最懒方案 :用
response.content.decode('utf-8', errors='ignore')忽略非法字符(不推荐,会丢数据)。
4.4 坑四: SSLError: certificate verify failed —— 证书验证的温柔陷阱
现象:公司内网调用自签名证书的 API,死活过不了 SSL 验证。
根因:Requests 默认信任 Mozilla 根证书列表,而你的内网 CA 不在其中。
填坑:
- 正确做法:把内网 CA 证书加到信任链
# 导出公司 CA 证书(.crt 文件) # 然后指定路径 requests.get('https://intranet-api.company.com', verify='/path/to/company-ca.crt') - 开发环境快捷方案 :
import ssl from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager class CustomHTTPAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): context = ssl.create_default_context() context.load_verify_locations('/path/to/company-ca.crt') kwargs['ssl_context'] = context return super().init_poolmanager(*args, **kwargs) session = requests.Session() session.mount('https://', CustomHTTPAdapter())
4.5 坑五: JSONDecodeError: Expecting value: line 1 column 1 (char 0) —— 你以为是 JSON,其实是 HTML
现象: response.json() 报错,但 response.text 显示 <html><body><h1>502 Bad Gateway</h1> 。
根因: API 网关(Nginx、Cloudflare)在上游服务挂掉时,返回自己的错误页,而非透传上游错误 。 Content-Type 可能还是 application/json ,但 body 是 HTML。
填坑:
- 永远先检查状态码和 Content-Type :
response = requests.get(url) if response.status_code != 200: print(f"HTTP {response.status_code}: {response.reason}") print(f"Headers: {dict(response.headers)}") print(f"Body preview: {response.text[:200]}") raise Exception("API 调用失败") content_type = response.headers.get('Content-Type', '') if 'application/json' not in content_type: print(f"非 JSON 响应,Content-Type: {content_type}") raise Exception("响应类型不符") try: data = response.json() except json.JSONDecodeError as e: print(f"JSON 解析失败: {e}") print(f"Raw body: {response.content[:200]}") raise
这五个坑,覆盖了从环境配置、网络通信、编码处理到协议解析的全链路。填坑的关键不是记住代码,而是理解每个错误背后暴露的系统层次——是环境问题?网络问题?协议问题?还是业务逻辑问题?Requests 从不撒谎,它只是把真相,用最直白的 Python 异常,摆在你面前。
5. Requests 的边界在哪里:何时该转身拥抱 httpx、aiohttp 或 urllib3
Requests 是伟大的,但它不是银弹。当你的项目规模、性能要求或技术栈发生变化时,固守 Requests 可能从“省事”变成“找罪受”。判断何时该转身,关键看三个信号: 并发量突破 100 QPS、需要原生异步支持、或必须深度定制底层行为 。
5.1 信号一:并发量 > 100 QPS,Requests 的阻塞式模型成为瓶颈
Requests 是同步阻塞的。一个 get() 调用,线程就卡在那里等响应。用 ThreadPoolExecutor 开 100 个线程?内存占用飙升,上下文切换开销巨大。我们做过压测:单机 100 线程 Requests,并发 100 QPS 时,CPU 使用率 45%,内存 1.2GB;而同样 QPS 下, httpx.AsyncClient 仅需 1 个线程,CPU 12%,内存 300MB。
何时切换?
- 场景 :实时风控系统,每笔交易需调用 3 个外部 API(征信、黑名单、反欺诈),要求 P99 < 200ms
- Requests 方案 :
ThreadPoolExecutor(max_workers=50)+Session复用 → 线程数多,锁竞争激烈,GC 频繁 - httpx 方案 :
import httpx import asyncio async def check_risk(transaction_id): async with httpx.AsyncClient(timeout=5.0) as client: tasks = [ client.get('https://api.credit.com/check?id=' + transaction_id), client.get('https://api.blacklist.com/query?id=' + transaction_id), client.get('https://api.anti-fraud.com/assess?id=' + transaction_id), ] results = await asyncio.gather(*tasks, return_exceptions=True) return results # 并发 1000 笔交易,只需 1 个事件循环 asyncio.run(check_risk("tx_123"))httpx的优势在于:- 真异步 :基于
asyncio和trio,无线程切换开销 - Requests 兼容 :API 设计几乎一致,
httpx.get()和requests.get()用法相同 - HTTP/2 支持 :对支持 HTTP/2 的 API(如 Cloudflare),复用连接更高效
- 真异步 :基于
注意:
httpx不是 Requests 的替代品,而是“Requests 的异步兄弟”。如果你的项目没有高并发需求,强行上httpx只会增加复杂度。
5.2 信号二:需要细粒度控制连接生命周期,urllib3 是更锋利的刀
Requests 是 urllib3 的封装。当你需要:
- 自定义 DNS 解析 (如强制走 DoH)
- 连接池级别的健康检查 (定期 ping 连接)
- 底层 socket 选项控制 (如
TCP_NODELAY)
这时直接上 urllib3 更直接:
import urllib3
import certifi
http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where(),
# 自定义 DNS 解析器(需配合 dnspython)
# resolver=CustomResolver(),
)
# 复用连接,但可精确控制
resp = http.request('GET', 'https://api.example.com/data')
print(resp.data) # bytes
print(resp.headers)
urllib3 的 PoolManager 比 Requests 的 Session 更透明,源码清晰,适合需要“把每个螺丝钉都拧紧”的场景,比如 IoT 设备固件升级,对网络稳定性要求苛刻。
5.3 信号三:项目已用 FastAPI/Starlette,生态一致性优先
如果你的 Web 服务用 FastAPI ,它底层用 httpx 做客户端。此时在 FastAPI 路由里调用外部 API,用 httpx.AsyncClient 而非 requests ,能获得:
- 事件循环统一 :避免
async/sync混合带来的死锁风险 - 依赖注入友好 :可将
httpx.AsyncClient作为 FastAPI 依赖注入,生命周期管理更清晰 - 性能无损 :同属
httpx生态,无额外序列化开销
from fastapi import Depends, FastAPI
import httpx
app = FastAPI()
# 依赖注入客户端
async def get_http_client():
async with httpx.AsyncClient(timeout=10.0) as client:
yield client
@app.get("/data")
async def fetch_data(client: httpx.AsyncClient = Depends(get_http_client)):
response = await client.get("https://api.example.com/data")
return response.json()
5.4 不该切换的时刻:别为“新潮”而切换
有些情况,坚持 Requests 是更优解:
- 脚本工具、运维自动化 :
requests.get()一行搞定,何必引入asyncio循环? - 教学场景 :Python 零基础入门,
import requests; r = requests.get(url)的心智负担,远低于理解async/await和事件循环。 - 遗留系统维护 :已有 10 万行 Requests 代码,稳定运行 5 年,切换成本远大于收益。
技术选型不是攀比,而是权衡。Requests 的伟大,正在于它用最朴素的同步模型,解决了 90% 的网络交互需求。它的边界,不是缺陷,而是为更复杂场景预留的接口。当你看到 exceeded retry limit 时,先优化 Requests 的重试策略;当并发量真达到瓶颈时,再优雅地牵手 httpx ——这才是工程化的节奏。
我至今在个人小工具里,依然用 requests 。不是因为它不够新,而是因为它足够懂我:我要的从来不是最快的轮子,而是一个让我专注业务逻辑、不用操心网络细节的可靠伙伴。


被折叠的 条评论
为什么被折叠?



