Python第三方库实战:requests、Pydantic与接口数据校验

0 阅读7分钟

先说一个在团队里反复出现的场景:

r = requests.get(url, timeout=10)
assert r.status_code == 200

这段代码在 CI 上跑了三个月,绿色的。然后某天业务反馈对不上账——因为那个接口「200 了但业务失败」。

问题不在 assert 写错了。问题在于「成功」被当成了一个布尔值,而它其实有五层。

一、五层「成功」,信号完全不同

层检查什么出问题时的信号谁负责
1连上了吗ConnectionErrorrequests
2HTTP 状态码status_code 是 404 / 500给值,要自己判
3body 是 JSON 吗.json() 抛 JSONDecodeErrorrequests
4字段齐、类型对吗需要校验库Pydantic
5内容对吗没有工具会告诉你自己

前四层可以自动化,第五层不行。

把「成功」写成布尔值,等于把五层压成一层——压掉的那四层,正是出事的时候你最需要看见的那几层。

二、requests 给你的和没给你的

r = requests.get(url, timeout=10)

实跑结果:

/ok            status=200 ok=True
/not-found     status=404 ok=False
/server-error  status=500 ok=False

三个都正常返回,没有一个抛异常。 requests 的契约是「拿到响应就算完成任务」。

r.ok 不够用

r.ok 只是个布尔值。你拿不到「是 404 还是 500」——而这恰恰是重试策略的依据:

  • 404:地址或参数错了,重试一万次还是 404
  • 500:服务端可能只是抖了一下,重试也许能好

所以要写重试,必须自己按 status_code 分支,不能用 r.ok。

timeout 不是可选项

不写 timeout,服务不响应时程序会无限期挂起。自动化脚本里这是最难查的一类问题:没有报错、没有日志,只有一个永远不结束的运行。

超时值怎么定? 定成「比业务能接受的等待时间长一点」。写死的 5 秒在小数据上够用,生产上通常从连接超时和读取超时分开设置。

只有连不上才抛异常

try:
    requests.get("http://127.0.0.1:9/nothing", timeout=2)
except requests.exceptions.ConnectionError as error:
    print(type(error).__name__)   # ConnectTimeout

实际拿到的是 ConnectTimeout,它是 ConnectionError 的子类。注意别写成 except Exception 全部接住——那会把超时、连接失败、以及你自己的逻辑错误混成一类,日志里就看不出到底是哪种了。

Session 什么时候值得开

Session 做两件事:复用 TCP 连接,以及把公共配置集中一处(请求头、认证、默认超时)。

只请求一次不值得开;同一 host 请求很多次时收益才明显。判断标准很简单:你会不会发第二次请求? 会,就用 Session。配置写法见 requests 官方文档。

三、Pydantic 值在哪一步

先看一个不完整但很常见的检查:

if "payments" not in body:
    return "第4关 缺少外层 payments"

它只看了外层有没有 payments 这个键。里面每条记录缺不缺字段、类型对不对,它不知道。

这和 Day23 的 read_rows 是同一种盲区。Pydantic 就是来补这个的:

from pydantic import BaseModel


class Payment(BaseModel):
    payment_no: str
    order_id: str
    status: str
    amount: float


class Envelope(BaseModel):
    payments: list[Payment]

最被低估的一个收益

body = requests.get(f"{base_url}/ok", timeout=5).json()
envelope = Envelope.model_validate(body)
envelope.payments[0].amount     # 199.8  —— float,不是字符串

接口给的是字符串 "199.80",过完 Pydantic 变成数字 199.8。

从 Day21 到 Day23 我们反复写 float(...)。这件事看着琐碎,但它正是 Day21 开头那个字符串排序 bug 的根源——让类型在入口处就归位,比在每个使用点补 float() 可靠得多。

把类型转换放在校验这一层,不要散落到业务代码里。

报错信息是你排查的起点

字段缺失时:

校验失败,一共 3 处:
   ('payments', 0, 'order_id') -> Field required
   ('payments', 0, 'status')    -> Field required
   ('payments', 0, 'amount')    -> Field required

loc 的读法是 (外层键, 第几条, 哪个字段)。一次报出全部问题,而不是修一个跑一次——这在接口一次返回几十条记录时差别很大。

快速失败 vs 挑出坏的

model_validate(整个对象) 的行为是「有一条坏,整份失败」。这在工程上通常是你想要的:一批数据要进系统,宁可全部拒绝,也不要一半进去。

但如果场景是「采集回来的数据,挑能用的留下」:

good, bad = [], []
for raw in rows:
    try:
        good.append(Payment.model_validate(raw))
    except ValidationError:
        bad.append(raw["payment_no"])

先问清楚你要哪种语义,再选写法。 我见过把「快速失败」当成 bug 改掉的,其实那正是最想要的行为。

四、第五层:没有任何工具会帮你

教学服务里有一个路径 /business-fail,返回:

状态码:200
JSON:合法
字段:payment_no / order_id / status / amount 一个不缺
类型:amount 是 float

前四层全过。 requests 顺利返回,Pydantic 顺利放行。

但内容是:

P1001-FAIL  FAILED   199.80
P1001-OK    SUCCESS  199.80

同一笔钱的两次尝试。要算「成功支付总额」,全加会重复,只加成功的才对。

requests 只帮你过前两层,Pydantic 只帮你过第四层。第五层必须你自己写。

def check_business(items):
    problems = []
    for item in items:
        if item.status == "FAILED" and item.amount != 0:
            problems.append((item.payment_no, "失败但金额不是 0"))
    return problems

这段业务代码本身也有三个问题

我把它写进脚本注释里了,因为能说出它哪里不够,比会写它本身更有用:

  1. 它只认死规则。 「失败金额必须是 0」是我编的,真实业务可能根本不是这条规则。业务规则应该来自文档或产品,不是来自示例数据。
  2. expected_count 写死了。 接口一旦分页,这个数就不对——而错误的条数检查会掩盖真实的分页问题。
  3. 出问题只告诉你哪条不对,不告诉你卡在哪一层、该不该重试。

这三点正好是 Day25 用 pytest 要解决的。

五、接口测试最该先写的不是请求

今天花了最多篇幅的不是 requests 怎么用,而是怎么搭那个假服务。

理由很实际:

依赖真实接口依赖本地假服务
可重复性服务变了就挂你控制的
错误路径只能测碰到的想要哪种造哪种
网络会抖、会断不需要网络
别人能不能跑要权限、要环境克隆就能跑

用真实接口写的测试,本质上是在测那个接口还能不能连通,不是在测你的代码对不对。 假服务才能稳定地告诉你「404 时我的代码会怎样」「body 不是 JSON 时会怎样」。

我自己的顺序永远是:先写假服务 → 再写请求 → 最后写断言。 反过来做的话,你会发现自己很难造出那些不常见的错误路径。

六、最后

/ok 那三条加起来是 359.60。

天实现合计
Day21函数359.60
Day22类359.60
Day23命令行工具359.60
Day24HTTP + Pydantic359.60

换了四次实现,数字没变。 这是判断重构有没有搞坏东西的唯一标准。

顺带一句工程价值:这四天的数据来自同一份订单/支付样例,所以每一个实现都能被同一个数字卡住。 一旦某天算出来的不是 359.60,不用讨论「是不是浮点误差」,直接就知道有东西被改坏了。

代价是前面几天的准备工作——要建立能互相校验的数据基线,得从第一天就统一。Day19 用假数据学语法时其实断过一次,是 Day21 换回订单数据才接上的。中间那几天的数字没人能对照,是有代价的。

今天真正值得带走的只有两句:

「成功」有五层,每层的信号不一样。 前四层能自动化,第五层只能你自己写。

下一篇是 Day24 之后的 Day25《Python测试与AI协作:用pytest验证代码而不是盲信答案》,我们把今天手写的死规则变成可运行的断言,并用一段故意写错的代码走完「先失败、再修复、再确认没改坏原规则」。