DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python 读取 JSONL 报 Unexpected UTF-8 BOM

JSONL 第一行报 Unexpected UTF-8 BOM,给文本行加 decode 又报属性错误。确认文件头后用 utf-8-sig 打开,区分逐行 JSON 与整份文档,并保留字符和记录数量。

用 Python 3 逐行读取 JSONL,第一行在 json.loads(line) 处报 Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)。随后给 line 加 .decode("utf-8-sig"),又变成 AttributeError: 'str' object has no attribute 'decode'。

这两条错误对应两个不同环节:文件开头的编码标记进入了 JSON 文本,而文本模式读取的 line 已经是字符串,不能再按字节对象调用 decode()。修改终端显示语言不会移除文件中的标记。

先确认文件头,而不是按文字语言猜编码

UTF-8 BOM 的字节是 EF BB BF。可以仅查看文件头,不必输出正文:

from pathlib import Path

path = Path("example.jsonl")
with path.open("rb") as source:
    print(source.read(3).hex())

把 example.jsonl 换成待检查文件。如果输出是 efbbbf,就找到了 UTF-8 BOM。普通 UTF-8 解码会把它变成开头的 \ufeff 字符,标准库 JSON 解码器收到以这个字符开头的字符串时会拒绝解析。

文件里有中文或阿拉伯文,不等于一定有 BOM。若文件头不是这三个字节,要继续检查实际报错那一行和文件来源,不要无条件丢弃前三个字节。UTF-16、UTF-32 文件也不能按这里的 UTF-8 方案直接处理。

如果文件尚未进入 json.loads(),读取时已经报 UnicodeDecodeError,先核对输入的实际编码;已确认文件由上游按 GBK 导出时,可继续检查 GBK 文件的读取编码。utf-8-sig 不会把 GBK 内容转换成 UTF-8。

在打开文件时处理 BOM

已确认输入是 UTF-8、可能带文件头 BOM 时,在文本读取边界使用 utf-8-sig:

import json

path = "example.jsonl"
with open(path, "r", encoding="utf-8-sig") as source:
    for line_number, line in enumerate(source, start=1):
        try:
            record = json.loads(line)
        except json.JSONDecodeError as exc:
            raise ValueError(
                f"Invalid JSON at line {line_number}"
            ) from exc
        # 在这里处理 record

这个解码器会跳过数据开头可选的 UTF-8 BOM;没有 BOM 的普通 UTF-8 文件也可正常读取。循环拿到的每个 line 都是 str,直接交给 json.loads(),不要再次 .decode()。

这个示例遇到空行或格式错误会停止,并保留出错行号。若数据协议明确允许跳过空行,可另外加入对应判断;不要为了让导入继续运行而把所有解析异常都忽略,否则会出现记录数量减少却没有报告的情况。

JSONL 与单个 JSON 文档要分开读取

JSONL 的每一行是一个独立 JSON 值,应逐行调用 json.loads()。不要为了去 BOM,把整个多行 JSONL 改成一次 json.load();多个顶层值可能导致新的 Extra data 错误。

如果文件实际是一个完整 JSON 文档,例如跨多行排版的数组,则应读取整个文档:

import json

with open("example.json", "r", encoding="utf-8-sig") as source:
    document = json.load(source)

两种方式都在打开文件时处理编码;区别在于逐行解析,还是解析整份文档。

文件中段的 BOM 需要追查拼接方式

utf-8-sig 处理的是流开头的可选标记。如果多个各自带 BOM 的文件直接拼接,后续片段开头的标记可能留在文件中段,第一行修好后仍会在后面的行报错。

此时应核对报错行对应的数据来源和拼接流程。更稳妥的修复是分别按各自编码读取源文件,再按统一的 UTF-8 输出协议重新生成副本。不要对全文执行 replace("\ufeff", ""):JSON 字符串值内部的同一字符可能属于实际数据,批量删除会改变字段内容。

如果上游能控制输出,JSON 文件通常应使用不带 BOM 的 UTF-8。为写入端改成 utf-8-sig 会添加 BOM,方向与这里的修复相反。

验证字符和记录数量

准备带 BOM 与不带 BOM 的 UTF-8 样例,确认两者第一条记录都能解析,并核对非 ASCII 字段内容。随后核对完整文件的记录数量,确保没有通过忽略异常或跳过行来“修好”导入。

只改读取编码不会重写原文件,便于回退。需要转换已有文件时,保留原件并输出到新路径,确认字符、记录数量和结构都一致后再切换;不要直接覆盖唯一的数据副本。也不要使用 errors="ignore" 隐藏不合法字节,它可能静默丢失数据,却没有解决输入格式问题。