Python常用标准库:做一个测试结果分析工具

先把一条判断摆出来:能在标准库里找到的,就别装包。

理由不是「装包不好」,而是标准库没有版本冲突、没有依赖树、没有一层额外的学习成本。今天做的工具,六样东西全是自带的:json、csv、pathlib、argparse、logging、collections。

顺便说三个本机实测撞出来的坑,都不是抄来的经验。

一、with 不是可选的写法习惯

python 复制代码
with target.open("w", encoding="utf-8") as stream:
    json.dump(payments, stream, ensure_ascii=False, indent=2)

with 保证用完自动关闭------哪怕中间抛异常,文件也会被关。

用 open() 加手写 close(),一旦某条路径上漏掉,数据可能只写了一半,而半截 JSON 是最难查的一类 bug:能读一半、报一个语法错误、看不出是哪行写的。

写完立刻读回来核对,这是唯一可靠的验证方式:

python 复制代码
again = json.loads(target.read_text(encoding="utf-8"))
print(again == payments)          # True
print(isinstance(again, list))   # True

第二条比第一条更重要------确认读回来的还是列表,而不是变成了字典 。Day19 踩过一次,len(document) 数的是外层字段,不是订单数。

二、CSV 交给模块,因为规则比想象中麻烦

手拼在数据简单时能跑:

python 复制代码
manual = "name,note\nA,键盘\n"

问题出在值里有逗号:

text 复制代码
name,note
A,"键盘,鼠标"

这一行几列?CSV 没有类型,只认分隔符。值里有逗号要用双引号包住,双引号本身还要写成两个:

text 复制代码
B,"他说""你好"""

手工处理这套转义,漏一次就出一份错数据。而:

python 复制代码
writer = csv.DictWriter(stream, fieldnames=["name", "note"])
writer.writeheader()
writer.writerows(rows)

读写两边的规则都替你想好了。实跑确认读回来的结构和 JSON 完全一致:back == payments 为 True。

newline="" 不能省

Windows 上默认把 \n 写成 \r\n,csv 模块读的时候对不上就会多出空行。

读写两边都写 newline=""。 这不是风格问题,是 csv 模块文档明确要求的。

三、路径:读之前先检查,后缀记得 .lower()

python 复制代码
p = Path("data/payments.json")
p.name          # payments.json
p.parent.name   # data
p.suffix        # .json
p.stem          # payments
p.exists()      # True

拼接用 / ,别自己加斜杠。folder / "x.json" 在三个系统上都对,str(folder) + "\\" + "x.json" 只在 Windows 上对。

一个在 Windows 上测不出来的坑

分派文件类型时写的是:

python 复制代码
suffix = path.suffix.lower()

那个 .lower() 为什么不能省?

因为 Windows 的文件名不区分大小写。 本机实测 Path("PAYMENTS.JSON").exists() 返回 True------它和 payments.json 就是同一个文件 。而 Path.suffix 原样保留大写,返回 .JSON。

在 Linux 上这两个是两个不同的文件。不写 .lower(),同一份代码在两个系统上行为不同,而你在 Windows 上永远测不出来。

这个问题是我跑断言时发现的:一开始以为某条断言写错了,查下去才发现是操作系统帮了倒忙。

教训:跨平台代码里的任何大小写比较,都要先想一遍「这个测试环境能不能发现它」。 本机测过不等于没问题。

四、Counter:省下的不只是三行

python 复制代码
counts = {}
for p in payments:
    counts[p["status"]] = counts.get(p["status"], 0) + 1

c = Counter(p["status"] for p in payments)

四行变一行,但真正的收益在别处:

python 复制代码
c["REFUNDED"]          # 0  ------ 没出现过的键直接给 0
c.most_common(2)       # [('SUCCESS', 3), ('FAILED', 2)]
dict(c1 + c2)          # 两个 Counter 相加,当分桶用

第一条最实用:手写字典时 counts["REFUNDED"] 会抛 KeyError,必须写 .get(..., 0) 兜着。用 Counter 就没有这个分支了------少一个分支,就少一个能写错的地方。

五、argparse:type=int 是白拿的检查

python 复制代码
parser.add_argument("--limit", type=int, default=2)

有人传 --limit abc,实跑输出:

text 复制代码
usage: report [-h] [--status STATUS] [--limit LIMIT] source
report: error: argument --limit: invalid int value: 'abc'

退出码 2。这不是我手写的判断,是 argparse 自己拦的,还顺手打印了完整用法。

漏掉必填参数也是退出码 2,和「后缀不认识」(退出码 1)区分开:

退出码 含义
0 正常完成
1 工具启动了,但没完成(文件不存在、格式不认识)
2 命令行就用错了(类型错、缺参数)

这个区分对写脚本很关键:CI 里看到 2,知道是人用错了;看到 1,知道是数据或环境的问题。全部返回 0 或全部抛异常,这两种信息都丢了。

六、日志:两个坑,第二个尤其阴

print 给人看,logging 留痕迹。加 FileHandler 存文件时格式要带时间戳,否则没法按时间排查:

python 复制代码
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))

坑一:日志文件开着的时候删不掉

text 复制代码
PermissionError: 另一个程序正在使用此文件

FileHandler 一直占着它。

坑二:边遍历边删,后面的被跳过

我第一次写关处理器是这样:

python 复制代码
for handler in logger.handlers:     # 错
    handler.close()
    logger.removeHandler(handler)

结果文件还是删不掉。 removeHandler 在循环里修改了正在遍历的列表,后面的元素被跳过------FileHandler 根本没被关掉。

正确写法:

python 复制代码
for handler in list(logger.handlers):   # 对
    handler.flush()
    handler.close()
    logger.removeHandler(handler)

改完实跑确认删除成功。

凡是「遍历一个列表、同时要改这个列表」,先 list() 复制一份。

这个错误的迷惑性在于:症状是「我明明关掉了,怎么还不行」,很容易怀疑是操作系统的问题、是不是文件权限、是不是有别的进程------实际上是遍历逻辑。

七、读回结构:验证要验两层

python 复制代码
again = json.loads(target.read_text(encoding="utf-8"))
print(again == payments)          # True
print(isinstance(again, list))   # True

第二条比第一条更重要。

again == payments 为 True 只保证「内容对」------但如果 payments 本来就是个列表,那它也保证了类型对。问题是 Day19 踩过一次坑:orders.json 的顶层是 {"orders": [...]},len(document) 数到的是外层有几个字段,不是几笔订单。

数值相等和结构相等是两件事。 写回读回的验证应该同时验两层,否则你会漏掉「写成了字典的列表但按字典读」这类问题。

顺带一条实用经验:转换之后立刻验原样,不要先做点别的。 比如 read_rows 里:

python 复制代码
return document["payments"] if isinstance(document, dict) else document

这个分支是因为 JSON 顶层可能是 {"payments": [...]} 也可能直接是 [...]。如果哪天上游改了结构,这里不会报错,只会返回空列表------所以这一行必须配一个「读到 0 条就记 ERROR」的检查,而不是让它静悄悄地传下去。

八、拼起来的工具

read_rows 有三个设计点,每个都是为了把决定权留给上层:

python 复制代码
def read_rows(path):
    if not path.exists():
        logger.error("文件不存在:%s", path)
        return []
    suffix = path.suffix.lower()
    if suffix == ".json":
        ...
    if suffix == ".csv":
        ...
    logger.error("不认识的后缀:%s", path.suffix)
    return []

一、先检查再读 ,而不是让 FileNotFoundError 冒出去------对使用者来说,一句 文件不存在:xxx 比一串堆栈有用得多。

二、按后缀分派,这是最自然的依据。

三、返回空列表而不是直接退出。 「读到 0 条」是正常情况还是错误?工具不决定,main 决定:

python 复制代码
if not rows:
    logger.error("没有读到数据,退出")
    return 1

把「发现问题」和「决定怎么办」分开 ,同一个 read_rows 才能被复用到别的工具里。

输出的交叉核对

text 复制代码
条数 5、SUCCESS 3、FAILED 2、金额合计 659.3、通过率 0.6

659.3 和 Day22 里「不过滤状态」的数字一致。 通过率 0.6 的分母是成功加失败,口径从 Day21 保持到今天没变过------这比任何单元测试都更能说明代码没走样。

最后

能在标准库里找到的,就别装包。

今天没有学新语法,学的是「工具箱里已经有什么」,以及三个具体到能复现的坑:csv 的 newline="" 不能省 、后缀比较前要 .lower() 且这个坑 Windows 上测不出来 、遍历列表同时改它要先 list()。

下一篇是 Day24《Python第三方库实战:requests、Pydantic与接口数据校验》 ------今天 read_rows 只按后缀分派、不管字段缺不缺 ,下期接一个本地 HTTP 服务,用 requests 真正发请求,再用 Pydantic 回答「状态码 200 到底算不算成功」。

相关推荐
程序员cxuan1 小时前
离谱,AI 大佬顺手搓了个 YouTube ,而我顺手给他搓了个中文插件
人工智能·后端·程序员
爱勇宝11 小时前
Claude Fable 5.2 发布后,开发者该关注什么
程序员·ai编程
挖掘狂人11 小时前
AI 把代码写得飞快,然后呢?聊聊我验收生成代码的一套流程
人工智能·程序员·ai编程
codigger11 小时前
AI 把代码写得飞快,然后呢?聊聊我验收生成代码的一套流程
ai·程序员·编程·#人工智能
SimonKing12 小时前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
非优秀程序员1 天前
题记|《互联网产品恩仇录》
后端·程序员·创业
易师傅1 天前
技术面试已死?AI 时代,真正拉开程序员差距的是什么?
前端·后端·程序员
Hilaku1 天前
你们公司有 35 岁以上的前端吗?
前端·javascript·程序员
AI前端工程狮1 天前
服务端已经死了,客户端却报"超时"
人工智能·程序员
楠风测开1 天前
两个任务同时跑,B 的登录态为什么被踢回登录页
程序员