news 2026/10/31 8:58:22

requests 安装与测试避坑:依赖树、超时重试与排错链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
requests 安装与测试避坑:依赖树、超时重试与排错链路

"装个 requests 而已,能有多大事?"——这话我带新人时听过太多遍,也亲眼见过太多翻车现场。有人pip install requests敲下去满屏红色报错,有人装完import成功但第一个请求就卡在原地两分钟,还有人本机跑得好好的,换一台机器全部超时,最后发现是某个依赖被别的包悄悄升了级。requests 这个库的特点就在这里:它的 API 友好到让人误以为它没有坑,可它背后挂着的链条其实很长——Python 解释器、pip 包管理器、虚拟环境、依赖树、证书库、字符编码、连接池、超时策略,任何一环没对齐,报错信息都会指向一个看起来跟它毫无关系的地方。这篇内容我想把 requests 的安装和测试一次性走完:先讲清楚它到底替你做了什么、为什么要装那一串你从没主动用过的依赖,再讲安装环节几种走法的取舍,然后是装完之后怎么分层验证——不是import不报错就算完,而是从包注册、版本自检、真实请求、字段核对一路验到边界行为,最后把测试怎么写、坑怎么排讲透。适合刚上手的朋友照着抄,也适合已经用了一两年但对内部机制还模糊的同学。

1. requests 装进来的不只是"一个库"

很多人对安装的认知停留在"装一个包",所以一看到pip list里多出来 urllib3、certifi、idna、charset_normalizer 这几个名字就犯迷糊,以为是装错了。其实这正是理解 requests 的第一道门槛:它不是一个自给自足的单体,而是一层把底层能力重新组织的封装。搞明白这层关系,后面百分之七八十的报错你都能一眼看出根源在哪。

1.1 从 urllib 到 requests:省掉的是哪几十行

用 Python 标准库发一个带超时、带请求头、带异常处理的 GET 请求,代码大概是这样:

import urllib.request import urllib.error req = urllib.request.Request( "https://example.com/data", headers={"User-Agent": "demo/1.0"}, ) try: with urllib.request.urlopen(req, timeout=5) as resp: status = resp.status body = resp.read() except urllib.error.HTTPError as e: status = e.code body = e.read() except urllib.error.URLError as e: status = None body = str(e.reason).encode()

这段代码里塞了请求构造、头部设置、上下文管理、两类异常分支,还是没处理重定向细节、没处理连接复用、没处理编码判断。requests 把这些全部收进了一行requests.get(url, timeout=5),然后给你一个Response对象。省掉的不是几十行代码,是"每次发请求都要重新想一遍异常怎么分类"的认知负担。

这背后连接的是 urllib3:真正管理 TCP 连接、连接池、重试、分块传输的是它,requests 做的是 API 设计与行为决策。所以当你在 requests 里看到ConnectionError、ReadTimeout、ChunkedEncodingError这些名字,它们其实都是从 urllib3 那一层透传上来的。

1.2 Response 对象里最该先记住的六个字段

新手拿到响应往往只看r.text,然后一旦出现乱码或者解析失败就抓瞎。我的建议是装完的第一天就把这六个字段养成习惯:

字段含义什么时候必须看
status_codeHTTP 状态码判断是否进入业务逻辑前
headers响应头字典(大小写不敏感)排查内容类型、长度、缓存
content原始字节串下载文件、图片、二进制
text按某种编码解码后的字符串文本处理,但要注意编码来源
encodingrequests 推断/指定的解码方式出现乱码时的第一嫌疑
elapsed从发出到响应读完的耗时判断慢在哪一环

特别注意elapsed用的是timedelta,要拿秒数得写r.elapsed.total_seconds()。很多人第一次用会直接拿它跟数字比较,结果报TypeError,白白浪费半小时。

1.3 依赖树:装 requests 实际上是装了一棵树

执行pip install requests之后,装进来的东西大致是这几类:

  • urllib3:连接池、重试、TLS 会话的底层实现。
  • certifi:一份打包好的根证书集合,requests 默认用它做证书校验。这是为什么你在某些机器上会遇到"证书过期"类报错——certifi 版本太老,里面的根证书没跟上。
  • idna:处理国际化域名(IDN)的编解码。
  • charset_normalizer:负责在没有明确声明字符集时猜测编码,新版本 requests 已经用它替换掉了原来的 chardet。

理解这棵树的实际意义在于:你遇到的绝大多数"装不上""跑不通",根因都不在 requests 自己身上,而在它的邻居身上。比如公司内网机器装了某个老版本的 urllib3,新 requests 要求更高版本,pip 就会在解析依赖时反复回溯甚至直接冲突退出;比如 certifi 被别的东西锁死在很旧的版本,你这边所有 HTTPS 请求都会报证书验证失败,而错误信息里根本不会提 certifi 三个字。

2. 安装这一步:三种走法与它们各自的前提

安装方式本身没什么神秘,但选错场景会很麻烦。我把它分成三类:直接装、隔离装、离线装。下面逐个说清楚适用条件和代价。

2.1 直接全局安装:快,但只建议在一次性环境里用

最省事的写法:

python -m pip install requests

注意我用的是python -m pip而不是裸pip。这不是洁癖,是有实际原因的:一台机器上经常同时存在多个 Python(系统自带的、包管理器装的、独立安装的),裸pip到底指向哪个解释器,取决于 PATH 的顺序,很容易出现"我明明装了,怎么 import 不到"。用python -m pip等于强制"用当前这个解释器自己的包管理器去装",把歧义消掉了。

装完立刻回读一次:

python -m pip show requests python -m pip show urllib3 certifi idna charset-normalizer

pip show会打印版本号、安装位置、依赖列表。养成这个动作的价值在于:以后出问题时你能马上判断"是不是装到了另一个解释器下面"。

如果遇到的是版本问题,直接指定:

python -m pip install "requests==2.31.0"

给版本号加引号是给 shell 看的,><==在某些 shell 里有特殊含义,不加引号容易被截断,这个坑在 Windows 的 cmd 和 PowerShell 上表现还不一样。

2.2 虚拟环境:不是最佳实践口号,是排错成本的计算

虚拟环境这件事被说得太"规范"了,反而让人麻木。我换个角度讲:假设你手上三个项目,A 需要 urllib3 1.x,B 需要 urllib3 2.x,C 是老代码只能用 requests 2.25。全局装的话,你每切一次项目就要重装一次依赖,而每次重装都可能顺手把别的项目弄坏,最后你会在某个深夜对着"昨天还好好的"抓狂。虚拟环境解决的就是这个:每个项目一份独立的 site-packages,互不干扰。

标准流程:

# 创建 python -m venv .venv # 激活:Linux / macOS source .venv/bin/activate # 激活:Windows PowerShell .venv\Scripts\Activate.ps1 # 激活确认,看输出前缀是否带 (.venv) python -c "import sys; print(sys.prefix)"

Windows 上有一个高频卡点:PowerShell 默认禁止执行脚本,激活时会报"无法加载文件,因为在此系统上禁止运行脚本"。这不代表虚拟环境坏了,只是执行策略限制。可以在当前用户的范围内放开:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

至于用 venv 还是 conda,我的判断标准很简单:如果项目里只有 Python 依赖,venv 足够且更轻;如果还要装非 Python 的二进制依赖(比如某些科学计算库需要的本地编译产物),conda 省心。没必要为此争论,选一个团队里大多数人在用的就行。

2.3 指定源与离线安装:两种非常现实的场景

网络受限的环境下直连公共包索引经常很慢甚至超时。这时候换一个可达的镜像地址:

python -m pip install requests -i https://<你的镜像地址>/simple

具体地址各家的不一样,选一个你所在网络能访问到的即可。如果镜像使用自签证书,可能还需要加--trusted-host <镜像主机名>,但这一步要谨慎,它等于对这个主机放弃证书校验,只应在你确认该主机可信时使用。

另一种场景是完全没有外网。做法是在一台能联网的机器上把包和依赖下载成文件:

python -m pip download requests -d ./wheels

然后把整个wheels目录拷到目标机器:

python -m pip install --no-index --find-links=./wheels requests

--no-index表示"别去网上找",--find-links表示"就在这个目录里翻"。这两个参数配合使用,才算真正断网安装,只写--find-links的话 pip 还是会尝试联网,慢得让人怀疑人生。

提示:pip download默认只下载匹配当前平台的 wheel。如果目标机器是另一个平台或另一个 Python 小版本,需要加--platform、--python-version、--only-binary=:all:等参数,否则拷过去会提示"找不到匹配的发行版"。

3. 装完之后的分层验证:从"没报错"到"真的能用"

import requests不报错,只证明包能被找到,不证明它能在你的环境下完成一次请求。我把验证拆成四层,逐层加深,任何一层失败都能把问题范围缩小一大截。

3.1 第一层:包注册与版本落点

python -m pip show requests python -m pip check

pip check会检查已安装包之间的依赖声明是否互相满足。它跑得快、输出直白,是排查"玄学问题"的第一招。常见输出形如requests 2.31.0 has requirement urllib3<3,>=1.21.1, but you have urllib3 3.0.0.——这种情况在手动升级过某个子依赖之后非常容易出现。

3.2 第二层:导入、版本与关键子模块自检

写一个单独的脚本,别偷懒用一行命令,因为一行命令在 Windows 的 cmd 里引号转义很容易出错:

import sys import ssl import requests import urllib3 import certifi print("python :", sys.version.split()[0]) print("requests :", requests.__version__) print("urllib3 :", urllib3.__version__) print("certifi :", certifi.__version__) print("certifi path:", certifi.where()) print("openssl :", ssl.OPENSSL_VERSION) print("default enc :", requests.utils.default_encoding)

这一段输出信息量很大。certifi.where()告诉你证书集合的实际文件路径,如果这个路径指向了一个不存在的文件,那后面所有 HTTPS 请求必然失败;ssl.OPENSSL_VERSION能帮你判断 TLS 能力,某些老系统上的 OpenSSL 版本过旧,连不上只支持新协议的站点,而报错会伪装成"连接被重置"。把这些信息先存一份,以后出问题可以直接对比"好的时候是什么样"。

3.3 第三层:一次真实请求,逐字段核对

挑一个确定可达的地址发一次请求,并且把关键字段全打出来:

import requests resp = requests.get( "https://example.com", timeout=(3, 5), headers={"User-Agent": "verify/1.0"}, ) print("status :", resp.status_code) print("encoding :", resp.encoding) print("apparent :", resp.apparent_encoding) print("bytes :", len(resp.content)) print("chars :", len(resp.text)) print("ctype :", resp.headers.get("Content-Type")) print("elapsed :", round(resp.elapsed.total_seconds(), 3)) print("url :", resp.url) print("history :", [h.status_code for h in resp.history])

timeout=(3, 5)是元组形式,分别代表连接超时和读取超时。这个细节值得专门记一下:单个数字的timeout=5在语义上是"连接和读取各自最多 5 秒",但实际使用中,一次慢响应可能由连接慢和传输慢两部分叠加,分开设置更容易定位问题。而且timeout是必须显式给的,requests 的默认值是"不超时",意味着对方不返回,你的进程就一直挂着。

len(resp.content)和len(resp.text)的差值也很说明问题:如果 content 是 12000 字节而 text 只有 4000 字符,说明内容是 UTF-8 中文,正常;如果 text 长度明显偏大且出现大量Ã、å这种字符,就是编码猜错了。

3.4 第四层:边界与异常行为验证

这一层最容易被跳过,但它恰恰决定了代码上线后的表现。至少要验这几件事:

  • 404 不会抛异常:requests 对 4xx/5xx 不抛异常,只看status_code。想要抛就调resp.raise_for_status()。
  • 连接失败抛什么:把地址改成http://127.0.0.1:1/(一个几乎不可能开着的端口),确认拿到的是requests.exceptions.ConnectionError。
  • 超时抛什么:requests.exceptions.ConnectTimeout和ReadTimeout都继承自Timeout,捕获时建议捕Timeout而不是捕Exception。
  • JSON 解析失败抛什么:resp.json()在内容不是合法 JSON 时抛requests.exceptions.JSONDecodeError(新版)或标准库的ValueError,捕获时要留意版本差异。

写成一个小脚本一次性跑完,输出一张对照表,以后换环境时重跑一遍就知道有没有退化。我在几个项目里都保留着这样一个check_env.py,它比任何文档都可靠,因为它测的是"现在这一台"。

4. 测试怎么写:别让单元测试真的去敲外网

给 requests 相关代码写测试,最大的误区是"测试里也发真请求"。这么干的直接后果是:网络一抖测试就红,第三方接口一改字段测试就红,CI 上跑一次要几分钟。测试的价值在于快速给出确定的答案,而真实网络恰恰是最不确定的东西。

4.1 打桩的基本思路:替换的是传输层,不是你的代码

好在 requests 的设计对打桩很友好,因为它所有请求最终都会经过Session.send或适配器层,第三方库只要在这里拦一刀就行。最常用的是responses,用装饰器或者上下文管理器把指定的 URL 注册成固定响应:

import responses import requests @responses.activate def test_fetch_user_ok(): responses.add( responses.GET, "https://api.example.test/users/1", json={"id": 1, "name": "A同学"}, status=200, ) resp = requests.get("https://api.example.test/users/1", timeout=2) assert resp.status_code == 200 assert resp.json()["name"] == "A同学" assert len(responses.calls) == 1 assert responses.calls[0].request.headers["User-Agent"].startswith("python-requests")

注意这里我用的域名是api.example.test,.test是保留后缀,永远不会解析到真实主机。这个习惯值得养成:测试里的地址一律用保留域名或127.0.0.1,既能防止误请求线上服务,也避免有人把测试代码里的地址当成真实接口去调用。

responses.activate的行为是:把所有没注册过的请求直接判失败。这个默认策略我强烈建议保留,因为它能抓到"你以为打桩了其实没打上"的情况——比如代码里拼出来的 URL 多了一个斜杠、或者查询参数顺序不同导致匹配不上。

4.2 需要测"真实连接行为"时,起一个本地服务

有些东西打桩测不了:连接超时、读取超时、分块传输、重定向次数。这些必须有一个真实的 socket 才能验证。最简单的方式是用标准库起一个本地 HTTP 服务:

import json import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/slow": time.sleep(2) if self.path == "/redirect": self.send_response(302) self.send_header("Location", "/ok") self.end_headers() return body = json.dumps({"ok": True, "path": self.path}).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *args): pass def start_server(): server = HTTPServer(("127.0.0.1", 0), Handler) port = server.server_address[1] t = threading.Thread(target=server.serve_forever, daemon=True) t.start() return server, port

绑定的端口写0表示让系统分配一个空闲端口,避免测试之间抢端口。log_message覆写成空函数,否则每次请求都会往 stderr 打一行日志,测试输出会变得很难看。

有了这个服务,超时就能真测了:

import pytest import requests def test_read_timeout(): server, port = start_server() try: with pytest.raises(requests.exceptions.ReadTimeout): requests.get(f"http://127.0.0.1:{port}/slow", timeout=(1, 1)) finally: server.shutdown()

这里超时设成 1 秒,服务端睡 2 秒,必然触发读取超时。要注意finally里必须调用shutdown(),否则serve_forever的线程会一直占着,测试进程退不出去。

4.3 超时与重试:这部分测试最容易被写成"测了个寂寞"

重试逻辑如果用HTTPAdapter+Retry实现,测试的关键是让服务端在前几次返回 5xx,然后验证客户端最终拿到 200,并且总请求次数符合预期。

from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): session = requests.Session() retry = Retry( total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504], allowed_methods=["GET"], raise_on_status=False, ) adapter = HTTPAdapter(max_retries=retry, pool_connections=10, pool_maxsize=10) session.mount("http://", adapter) session.mount("https://", adapter) return session

几个参数值得解释一下,因为写错任何一个,测试就会变成"看起来在测重试,其实一次都没重试":

  • total=3限制的是重试次数,不含首次请求,所以最多发出 4 次。
  • backoff_factor=0.2会让退避时间按 0.2、0.4、0.8 递增(含随机抖动),别设成 0,否则失败瞬间连打三次,等于给对方添堵。
  • allowed_methods一定要显式给。默认只对幂等方法重试是相对安全的,但历史上某些版本的默认行为变过,显式声明最保险。POST 默认不重试是有道理的——重发可能造成重复下单之类的后果。
  • raise_on_status=False让最终失败时把响应原样返回,由你的业务代码决定怎么处理,而不是直接抛异常。

还有一个常被忽略的点:Retry只作用于连接层重试和状态码重试,它不管你业务上的"接口返回 200 但 body 里code != 0"。后者得自己在业务层写循环,两者不要混为一谈。

5. 装完才暴露的五类问题与它们的真正成因

这一节我按"症状—根因—动作"的顺序写,都是我在实际项目里反复遇到的。你会发现几乎每一条的第一反应都是错的。

5.1 SSL 证书报错:先别急着关校验

典型报错是SSLCertVerificationError: certificate verify failed: unable to get local issuer certificate。很多人的第一反应是加verify=False,然后就地把警告压掉。这非常危险:它等于放弃了身份确认,而且很多人还会顺手写进生产代码。

正确的排查顺序是:

  1. 跑一遍certifi.where(),确认文件存在且可读。
  2. 检查 certifi 版本,太旧的根证书集合可能已经不包含目标站点使用的根。
  3. 如果是企业内网,很可能是流量在网关处被解密重签,此时应该把网关的根证书加入信任链,而不是关校验——REQUESTS_CA_BUNDLE环境变量或verify="/path/to/bundle.pem"都行。
  4. 检查系统时间。这个听起来离谱但确实常见:容器里时间漂移几分钟,证书的有效期判断就会失败。

注意:verify=False在测试代码里用来隔离环境问题尚可,但必须配上urllib3.disable_warnings()之外的显式注释说明原因,并且绝不允许进入生产配置。

5.2 乱码:三种来源,三种解法

乱码的根因永远只有一个——"编码"和"解码"用了不同的字符集。但来源分三种:

  • 响应头没声明 charset。这时 requests 会退回到猜。稳妥做法是显式指定:resp.encoding = resp.apparent_encoding,或者干脆resp.content.decode("utf-8")。
  • 响应头声明的 charset 是错的。某些老旧服务端不管内容是什么都声明ISO-8859-1,此时apparent_encoding会给出更靠谱的猜测。
  • 请求体编码不对。用data=传 dict 时 requests 会做表单编码,用json=传 dict 时会序列化成 JSON 并带上charset相关的头。如果你手动传了一个已经编码过的bytes,又没设Content-Type,服务端可能按别的字符集解析,这就是"我发出去的是中文,对方收到的是问号"的由来。

5.3 连接池默认值:并发一上来就体现

Session内部维护连接池,默认每个 host 最多保留 10 个连接。单线程脚本完全够用,但一旦上多线程,就会出现"明明只发了 20 个请求,怎么有一半在等"的现象——因为池满了,多出来的线程在排队。表现是整体耗时被拉长,而不是报错,所以很难第一眼看出来。

adapter = HTTPAdapter(pool_connections=20, pool_maxsize=50, pool_block=False)

pool_connections是保留的 host 池数量,pool_maxsize是单个池的最大连接数。pool_block=False表示池满时不阻塞,而是新建连接(用完丢弃),代价是连接复用率下降。这两个值调大要配合服务端的承受能力,盲目设成几百只会把人家的限流触发得更快。

5.4 依赖冲突:pip check 是唯一靠谱的裁判

"昨天还能跑"这类问题的头号原因就是某个包在装别的东西时把共享依赖升了级。典型链条:你装了某个库,它依赖 urllib3>=2,pip 顺手把 urllib3 升了,而你的老代码里用了 urllib3 1.x 才有的接口,于是运行时报AttributeError: module 'urllib3' has no attribute ...。

处理办法是固化成文件:

python -m pip freeze > requirements.txt

以后重建环境一律:

python -m pip install -r requirements.txt

freeze会把当前所有包的精确版本都写进去(包括间接依赖),代价是文件很长、升级时要手动改。另一个做法是只锁顶层依赖的版本区间,让 pip 自己解析间接依赖,代价是可复现性差一些。我的习惯是:应用项目用freeze全锁,库项目用区间约束。无论哪种,pip check都应该进 CI。

5.5 重定向与 TooManyRedirects

requests 默认跟随重定向,最多 30 次,超过就抛TooManyRedirects。这个异常在正常业务里很少见,但有两种情况会撞上:一是登录态失效后站点在几个页面间反复跳,形成环;二是某些接口把 302 用得不当,跳转到自己。排查时把allow_redirects=False打开,看resp.status_code和resp.headers["Location"],一跳一跳手动跟,很快就能找到环在哪。

还有一个容易忽略的点:跨域重定向时,requests 默认会把Authorization头去掉,这是安全设计,但如果你依赖这个头做鉴权,重定向之后就会收到 401,而错误信息不会告诉你"头被删了"。

6. 一条可复现的排查链路:从一句报错走到根因

前面讲的是分类,这一节讲流程。因为真实场景里你拿到的往往只有一行报错,需要自己一步步收窄。

6.1 四步定位法:先分域,再细分

我习惯按这个顺序走,每一步都能把范围砍掉一半:

第一步,分清是"装的问题"还是"跑的问题"。判据很简单:新建一个空目录,建全新虚拟环境,只装 requests,跑一个最小脚本。如果这一步就失败,问题在环境;如果成功,问题在你的项目依赖。

第二步,分清是"连不上"还是"连上了但结果不对"。前者抛ConnectionError/Timeout,后者能拿到status_code。这两类问题的排查方向完全不同,前者看网络与 DNS,后者看请求构造与响应解析。

第三步,分清是"本机特有问题"还是"普遍问题"。换一台机器、换一个网络跑同样的脚本。如果只有某台机器出问题,重点看那台机器的证书库、时间、环境变量;如果都出问题,重点看代码。

第四步,把不确定的东西全打出来。请求前打url、headers、params,请求后打status_code、headers、前 200 字节的content。很多人凭想象猜服务端返回了什么,一打印发现根本不是那回事。

6.2 三个现场还原

现场一:ModuleNotFoundError: No module named 'requests',但pip list里明明有。根因几乎总是解释器不一致。编辑器里配的解释器是 A,终端里pip装到了 B。动作:在报错的那个进程里打印sys.executable,然后对这个路径执行-m pip install。

现场二:脚本在本机秒回,在服务器上卡 60 秒后ConnectTimeout。大概率是出网策略限制。动作:先用curl -v或ping验证目标可达性,注意 ICMP 通不代表 TCP 端口通;再看requests抛的是连接超时还是读取超时——连接超时说明握手就没成,读取超时说明连上了但对方不回。

现场三:json.decoder.JSONDecodeError,但抓包看响应体明明是 JSON。常见于响应被中间层加了 BOM 或者前后多了空白,还有一种情况是服务端返回了压缩内容而中间某处已经解压过一次。动作:先打印resp.content[:200]看原始字节,再打印resp.headers.get("Content-Encoding")确认压缩声明,最后用resp.json()之前先resp.content.decode("utf-8-sig")试试。

6.3 最小可复现脚本模板

排查到最后,一定要能产出一个"别人拿去就能跑出同样结果"的脚本。我平时用的模板长这样:

import json import sys import traceback import requests def probe(url: str, **kwargs) -> None: print("=" * 60) print("python :", sys.version.split()[0]) print("requests:", requests.__version__) print("url :", url) try: resp = requests.get(url, timeout=(3, 10), **kwargs) print("status :", resp.status_code) print("elapsed :", round(resp.elapsed.total_seconds(), 3)) print("history :", [h.status_code for h in resp.history]) print("headers :", json.dumps(dict(resp.headers), ensure_ascii=False)) print("body[:200]:", repr(resp.content[:200])) except Exception: traceback.print_exc() if __name__ == "__main__": probe("https://example.com/")

这个模板的价值不在代码本身,在于它把所有可能影响结果的因素都固化成了可打印的输出:解释器版本、库版本、请求地址、耗时、重定向链、响应头、原始字节。把它贴进工单,对方不用问"你环境是什么"就能复现。

7. 几个我踩过之后才记住的细节

timeout不写不是"用默认值",而是"永不超时"。这个默认值坑过太多人,尤其在批处理脚本里,一个卡住的请求能让整个任务停到天亮。我现在写任何请求代码,第一个敲的参数就是timeout。

用Session而不是裸requests.get。裸调用每次都新建连接,一次请求一次 TCP 握手加 TLS 握手,批量请求时开销非常可观。改成with requests.Session() as s:之后,同一 host 的连接会被复用,实测在几十次请求的量级上能省掉一半以上的时间。而且Session可以统一挂 headers、挂适配器,配置集中在一处,比在每个调用点重复传参可靠得多。

resp.json()不要裸调。它内部会先判断内容是否为空,空内容会抛异常而不是返回None。线上接口返回 204 或者网关返回空 body 时,这一行就会炸。稳妥写法是先判if resp.content:再解析,或者包一层 try。

测试里永远不要相信"这个接口不会变"。我见过太多测试用例直接对着真实地址发请求,第一次跑通之后再也没跑过,等 CI 上重新启用时全线飘红。花二十分钟用responses打桩,能省掉后面几十次的误报排查。

最后分享一个小习惯:在项目根目录放一个scripts/check_requests.py,内容就是第 3.2 节和 3.3 节那两个自检脚本的合并版,只做打印不做断言。每次换机器、换虚拟环境、升级依赖之后跑一遍,把输出跟上次的存一份对比。它不会告诉你哪里错了,但它能让你在十分钟内知道"变了什么"——而绝大多数排查的时间,其实都花在确认"到底哪里不一样"上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 8:56:59

保险理赔大文件分片上传技术实践与优化

1. 保险理赔视频上传的技术挑战与解决方案在保险理赔业务场景中&#xff0c;视频证据的上传处理面临着几个关键痛点。理赔视频通常具有以下特征&#xff1a;单文件体积大&#xff08;普遍在500MB-2GB之间&#xff09;、上传环境复杂&#xff08;客户可能使用各类浏览器和设备&a…

作者头像 李华
网站建设 2026/10/31 8:54:24

LangGraph与AutoGen生产落地避坑指南:框架选型、工具链与国产化适配

1. 这份附录不是“速查表”&#xff0c;而是你落地AI工程的导航图如果你最近在看LangGraph、AutoGen相关的教程&#xff0c;或者正为选型发愁——比如该用LangGraph还是AutoGen搭Agent系统&#xff1f;要不要引入FastAPI做服务封装&#xff1f;本地调试该配什么终端工具&#x…

作者头像 李华
网站建设 2026/10/31 8:54:24

电商搜索三阶架构:ES+混合召回+ReRank实战

1. 项目概述&#xff1a;为什么“一条腿走路会摔”是搜索系统最真实的写照你有没有试过在电商App里搜“苹果手机”&#xff0c;结果首页蹦出来一堆红富士、青香蕉&#xff0c;甚至还有卖水果刀的&#xff1f;或者在招聘平台输入“Java工程师”&#xff0c;系统却优先展示“Java…

作者头像 李华
网站建设 2026/10/31 8:51:13

AIGC入门指南:从工具选择到创作变现全流程

1. AIGC时代&#xff1a;普通人如何系统掌握生成式AI 生成式人工智能&#xff08;AIGC&#xff09;正在重塑内容创作的方式。从文字到图像&#xff0c;从音乐到视频&#xff0c;AI工具已经渗透到创作的各个环节。但面对琳琅满目的AI工具&#xff0c;很多初学者常常感到无从下手…

作者头像 李华
网站建设 2026/10/31 8:51:13

机器人算法十年演进:从PID到深度学习的跨越

1. 机器人算法十年演进概述 2008年我刚入行机器人行业时&#xff0c;主流算法还停留在PID控制和简单的路径规划阶段。十年间&#xff0c;我亲眼见证了从传统控制方法到深度学习算法的跨越式发展。如今的机器人系统已经能够处理复杂环境下的动态避障、多模态感知和自主决策等高级…

作者头像 李华
网站建设 2026/10/31 8:50:24

淮扬菜核心技法拆解:刀工火候吊汤本味与家常落地

淮扬菜这三个字&#xff0c;很多人第一反应是“国宴菜”“文人菜”&#xff0c;觉得离日常灶台很远。我在后厨摸爬滚打这些年&#xff0c;接触过不少菜系&#xff0c;淮扬菜是最让我又爱又恨的一支——爱它清鲜平和、刀工精细&#xff0c;恨它对火候和原料的苛刻几乎到了不讲道…

作者头像 李华