Skip to content

让模型产出数据,而不是导入脚本 ​

需要给测试环境准备一批员工数据,让模型直接写一个导入脚本,跑完显示「成功 4 条,失败 4 条」,没报错,就算完成了。过了几天有人发现,备注字段都被截断在第一个逗号处,还有一条记录的状态是一个不存在的值 P。

用模型准备数据时,常见的做法是让它连数据带导入代码一起生成。每次需要新数据,都得到一段新的、未经审查的导入代码。更稳妥的分工是:模型只产出数据,格式由 Schema 约束;导入由一个固定的、审查过的程序完成。数据每次都可能不同,导入程序只写一次,它的正确性可以被测试一次、复用多次。

本文用一份模拟模型生成的 CSV,对比一段常见的生成脚本与一个固定导入程序,看错误分别在哪里被发现。实验见文末。

每次生成新的导入脚本模型只产出数据line.split(",")引号里的逗号、换行被拆开逐行提交失败后库里留一半except: errors += 1退出码 0,流水线以为成功结果备注截断、状态 P 进了库csv.DictReader + utf-8-sigBOM、引号、换行都正确Schema 校验枚举、引用、唯一,问题带行号单事务导入失败整体回滚,原因完整报告人审阅问题清单修正后重新生成,或只导入合格行模型产出数据(每次都校验),Schema 定义规则(有版本),导入程序写一次、有测试、复用按 utf-8 读带 BOM 的文件:第一列名变成 \ufeffid,row['id'] 取到 None
图 1 · 实测(Python 3.14.8、SQLite):逗号切分、逐行提交、吞异常的生成脚本报告「成功 4、失败 4」后正常退出,库里两条备注被截断、非法状态进了库;固定程序用标准 csv 解析与 utf-8-sig 解码,写入前按 Schema 列出全部问题,整批在一个事务里,失败就整体回滚。

一、生成脚本的四个常见缺陷 ​

python
for line in text.splitlines():
    cols = line.split(",")
    try:
        db.execute("insert into emp values(?,?,?,?,?)", (int(cols[0]), cols[1], cols[2], cols[3], cols[4]))
        ok += 1
    except Exception:
        errors += 1
text
逗号切分 + 逐行提交 + 吞异常:报告成功 4 行、失败 4 行,脚本没有报错退出
库里实际的行:1|Ada|active|"负责 A;2|Bo|active|"第一行;3|Cy|on_leave|;4|Di|P|把 P 写成了状态
  1. 按逗号切分:CSV 的字段可以用引号包住,里面可以有逗号和换行。"负责 A,B 两个模块" 被切成两段,备注只剩 "负责 A;带换行的字段被拆成两行,第二行无法解析。
  2. 不跳过表头:表头行进入了导入,靠 int() 失败被吞掉,才没有出事。
  3. 逐行提交:前面的行已经写入,后面的行失败了,库里是一份「部分成功」的数据,而且没有办法区分哪些是这次导入的。
  4. 吞掉异常:失败只变成了一个计数,原因没有记录,退出码是 0。调用它的流水线会认为导入成功。

非法状态 P 则说明了另一件事:数据库约束只覆盖了一部分规则(这张表没有为状态加检查约束),能写进去不代表数据是对的。

二、标准解析器与编码 ​

CSV 不是「逗号分隔的文本」,它有引号、转义和换行规则,应当用标准库的解析器:

text
标准 csv 解析:共 6 条记录;第 1 条 note='负责 A,B 两个模块',第 2 条 note='第一行\n第二行'

编码也要显式指定。模型或表格软件导出的 UTF-8 文件常带 BOM:

text
按 utf-8 解码后用 DictReader:第一列的列名是 'id',取 row['id'] 得到 None
按 utf-8-sig 解码:列名 ['id', 'name', 'dept', 'status', 'note']

BOM 被当成了第一列名字的一部分,row['id'] 取不到值,后面的代码会以为 ID 缺失。用 utf-8-sig 解码可以兼容有无 BOM 两种情况。

三、Schema 校验:在写入之前列出所有问题 ​

固定程序在写入数据库之前先做校验,把问题连同行号一次列出来:

text
Schema 校验:3 条合格,3 个问题:第 5 行 status='P' 不在 ['active', 'left', 'on_leave'];第 6 行 id=3 重复;第 7 行 dept='sales' 不存在

校验的内容来自领域规则:枚举的取值范围、引用的部门是否存在、ID 是否唯一、必填字段。这份 Schema 同时也是给模型的输入:告诉它允许哪些值,比在提示里用自然语言描述更精确,也更容易验证。

校验结果先给人看,再决定怎么处理:修正数据重新生成,还是只导入合格的部分。

四、单事务导入:要么全部成功,要么什么都没有 ​

text
不过滤、整批单事务导入:回滚(IntegrityError: UNIQUE constraint failed: emp.id),库里 0 行
只导入合格记录(导入前展示问题清单):已提交,库里 3 行

整批数据放在一个事务里,任何一行失败就整体回滚,库里不会留下一半的数据。失败的原因被完整地报告出来,而不是变成一个计数。对于很大的数据量,可以分批提交,但每一批都要记录进度,失败后能从断点继续,并且能识别和清理某一批导入的数据。

五、分工 ​

谁做什么怎样验证
模型按 Schema 生成数据每次都校验,不信任
Schema定义取值范围、引用、唯一、必填和领域规则一起维护、有版本
导入程序解析、校验、单事务写入、报告写一次,有测试,复用
人审阅问题清单,决定修正或部分导入—

这和模型写的 SQL 离放它去查库还差几道检查是同一个思路:模型的产出是需要校验的输入,执行副作用的代码由人审查并固定下来。生成的数据进入测试时,还要注意数据归属和清理,见测试数据归谁。

六、检查清单 ​

  • 模型产出数据,导入由固定程序完成;不每次生成新的导入代码。
  • 用标准库解析 CSV,显式指定编码(utf-8-sig)。
  • 写入前按 Schema 校验,问题带行号一次列出。
  • 整批在一个事务里,失败整体回滚,原因完整报告。
  • 不吞异常;失败时非零退出。

配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权