在 AI 开发者空间实现 JSONL 校验器:原始行号、退出码与 19 项测试

举报
yd_225105208 发表于 2026/11/06 18:12:09 2026/11/06
【摘要】 在华为云 AI 开发者空间免费体验环境中,用 Python 标准库实现 JSONL 校验器,保留原始行号、检查必需字段并区分退出码。记录流式读取修正、19 项测试与实际命令输出,并说明工具的适用边界。

处理日志、问答样本或批量导出数据时,JSONL 很方便:一行放一个 JSON 值,可以追加,也容易按行处理。但文件能打开,并不意味着每一行都适合交给下游程序。少一个右括号、把对象写成数组、遗漏一个字段,都可能让导入任务在中途失败。

这次我在华为云 AI 开发者空间的免费体验环境中,用 AI Shell 完成了一个 Python 标准库小工具。目标很具体:检查每个非空行是否为 JSON 对象、是否包含指定顶层字段,报告原始行号,并用退出码区分数据错误和输入问题。开发过程经历了一次源码检查和修正,最终实际运行了 19 项 unittest 测试。

本文记录的是 2026 年 11 月 6 日这次环境中的实现和输出。示例都是专门创建的合成数据,没有扫描个人文件,也没有部署额外云服务。

1. 先把校验规则写清楚

项目只有一个命令行入口:

python3 jsonl_check.py examples/valid.jsonl --required id,text

规则分为三层。第一层检查 JSON 能否解析,同时拒绝 NaN、Infinity、-Infinity 这些非标准常量;第二层要求解析结果必须是对象;第三层检查命令行指定的顶层字段是否存在。

这里的“存在”按键判断。{"id": 1, "text": null} 包含 text 字段,因此通过;{"id": 1} 没有 text,才会报缺字段。工具不要求 text 一定是字符串,也不递归检查嵌套对象。如果业务要求非空字符串、数值范围或对象之间的关系,需要另加规则。

空白行允许跳过,但仍计入物理行数。错误定位必须对应原文件,不能先删除空行再重新编号。空文件则返回零条记录、零条错误,退出码为 0;“文件必须至少有一条记录”属于另外一项业务约束。

2. 在体验环境中完成一个可验证的小项目

我从 AI Agent 开发实践中心进入 DevSpace,选择免费体验版,在 AI Shell 新建会话,工作目录使用 /root/workspace。随后要求它在独立目录中创建源码、测试、说明和两个样例,并实际执行测试与命令行示例。

最终目录如下,运行需要 Python 3.10 或更新版本:

jsonl-validator-demo/
├── jsonl_check.py
├── test_jsonl_check.py
├── README.md
└── examples/
    ├── valid.jsonl
    └── mixed_errors.jsonl

没有引入第三方库。json 负责解析,argparse 负责参数,unittest 负责测试,测试产生的文件放进 TemporaryDirectory,结束后自动清理。体验环境的数据不长期保留,因此完成后还需要把交付文件和原始输出下载保存。

3. 一行是否合法,拆成一个小函数

Python 的 json.loads 默认允许解析 NaN 等常量。因此只写一个 try/except,并不能满足这里的严格常量规则。实现中通过 parse_constant 指定回调,遇到这些常量立即抛出错误。

下面是实际源码中的单行校验函数:

def _reject_constant(const: str):
    raise ValueError(f"Non-standard JSON constant: {const}")


def validate_line(line: str, required_fields: list) -> str | None:
    try:
        obj = json.loads(line, parse_constant=_reject_constant)
    except json.JSONDecodeError as exc:
        return f"JSON syntax error: {exc.msg}"
    except ValueError as exc:
        return str(exc)

    if not isinstance(obj, dict):
        return f"Not a JSON object (got {type(obj).__name__})"

    for field in required_fields:
        if field not in obj:
            return f"Missing required field: {field}"

    return None

返回值只有两类:通过返回 None,失败返回原因字符串。每条错误记录只报告当前发现的第一项问题;如果一行缺少多个字段,不会一次列出所有缺失字段。这种行为简单,也需要在使用时知道它的边界。

4. 测试通过以后,仍然要读源码

第一版的 17 项测试都通过了,实际示例的输出也正确。但检查 run() 时,我发现它先调用了 fh.readlines(),再遍历列表。这样可以按行校验,却会先把整个文件读进内存,与我提出的逐行读取要求不一致。

修正后直接遍历文件对象,把读取和校验放在同一个循环中。以下是实际实现的核心部分,total、blank、valid、err_count 初始值为 0,details 初始为空列表:

try:
    with open(path, "r", encoding="utf-8") as fh:
        for lineno, raw in enumerate(fh, start=1):
            total += 1
            stripped = raw.strip()
            if not stripped:
                blank += 1
                continue
            reason = validate_line(stripped, required)
            if reason is None:
                valid += 1
            else:
                err_count += 1
                details.append({"line": lineno, "reason": reason})
except UnicodeDecodeError as exc:
    return {"error": f"Bad UTF-8 encoding: {exc}"}, 2
except OSError as exc:
    return {"error": f"Cannot read file: {exc}"}, 2

enumerate(fh, start=1) 在遇到空白行之前就确定了物理行号,所以空行不会改变后面错误的位置。try 同时包住打开文件和迭代过程,读取过程中出现编码或 I/O 错误也能返回退出码 2。

逐行读取并不意味着内存恒定。当前行的文本、解析后的对象和错误详情都需要内存;错误列表还会随错误数量增长。特别长的一行仍然需要一次解析。这个版本没有加入行长限制、错误数量上限或分批输出,也没有做大文件性能测试。

5. 用一个七行样例验证错误定位

mixed_errors.jsonl 的实际内容如下,第二行为空行:

{"id": 1, "text": "valid record"}

{"id": 2, "text": "missing closing brace"
[1, 2, 3]
{"id": 5}
{"id": 6, "text": NaN}
{"id": 7, "text": null}

运行 python3 jsonl_check.py examples/mixed_errors.jsonl --required id,text,在体验环境中得到的结果为:

{
  "total_lines": 7,
  "blank_lines": 1,
  "valid_records": 2,
  "error_records": 4,
  "errors": [
    {"line": 3, "reason": "JSON syntax error: Expecting ',' delimiter"},
    {"line": 4, "reason": "Not a JSON object (got list)"},
    {"line": 5, "reason": "Missing required field: text"},
    {"line": 6, "reason": "Non-standard JSON constant: NaN"}
  ]
}

对应退出码为 1。第七行的 null 被接受,因此有效记录是第 1、7 行;错误仍然落在原文件的第 3、4、5、6 行。把这些数字对照起来,比只看到“校验失败”更容易发现计数和行号方面的问题。

另一个 valid.jsonl 样例共有四行,包含中文文本和 null,实际输出为 total_lines=4、valid_records=4、error_records=0,退出码为 0。给不存在的路径 /no/such/file.jsonl 时,输出 {"error": "File not found: /no/such/file.jsonl"},退出码为 2。

6. 退出码要能被下一个程序使用

这个工具的三种退出码含义如下:

退出码 含义 输出方式
0 所有非空行通过校验 stdout 输出 JSON 汇总
1 至少一条记录不符合规则 stdout 输出带错误详情的 JSON 汇总
2 文件、编码或命令行参数有问题 文件与编码错误输出 JSON;参数错误由 argparse 输出 usage 到 stderr

文件不存在和数据不合格需要分开处理:前者可能是调用脚本传错了路径,后者才需要修复记录内容。还有一个容易写错的文档细节:不能声称所有错误都输出 JSON。遗漏文件路径这类命令行错误会沿用 argparse 的标准行为,在 stderr 显示用法,stdout 为空。

7. 最终 19 项测试实际通过

在修正读取方式后,我补了两个测试:一个用 mock 让 open 抛出 PermissionError,检查友好错误和退出码;另一个遗漏 CLI 必填位置参数,检查退出码、stderr 用法说明和空 stdout。

这里选 mock 是因为体验环境以 root 身份运行,仅靠 chmod 改权限不能稳定制造“读不了”的场景。它验证的是程序处理异常的分支,不代表已经覆盖所有操作系统和文件系统的权限组合。

最终运行命令与实际输出摘要如下:

python3 -m unittest -v

test_missing_cli_arg ... ok
test_permission_error_mocked ... ok
(其余 17 项也均为 ok)

Ran 19 tests in 0.014s

OK

测试覆盖了中文 UTF-8、空行后的真实行号、JSON 语法错误、非对象值、缺字段与 null、NaN 与 Infinity、空文件、不存在文件、坏编码、多个错误的位置以及三种退出码。这里的 0.014 秒只是这一次小规模测试的运行输出,不能用来推断大文件吞吐量。

8. 使用边界与这次实践的收获

这个版本适合在小型 JSONL 导入前做基础检查,但没有完整 JSON Schema 的类型和结构约束,也不检查重复键。Python 解析重复键时保留后面的值;超深嵌套、超长单行等资源消耗边界也没有专门加固。读取过程不会主动改写输入文件,但不承诺文件系统访问时间等元数据完全不变。

更精确地说,这是一套“允许空白行、对每条记录做对象与字段检查”的约定,不是对所有 JSON Lines 细则的完整认证。源码还会先 strip 行首尾空白,这种宽容处理同样属于本工具的选择。

这次 AI Shell 快速完成了第一版代码和测试,而源码检查找出了测试未覆盖的整文件读取问题。把需求写成可核对的行为,再把真实输出、实现细节和文档对齐,才让这个小工具具备了可复用的基础。下一步若要处理大量错误,可以优先增加错误详情上限或分批输出;在需求出现之前,这次实践先保持现有范围。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。