Python JSONDecodeError 排查清单:从 NaN 到编码 BOM

json.loadsJSONDecodeError 时,异常信息里有行号、列号、字节偏移量——先把这三个印出来,问题基本就定位了:

import json

try:
    data = json.loads(content)
except json.JSONDecodeError as e:
    print(e)
    print("line:", e.lineno, "col:", e.colno, "pos:", e.pos)
    print(content[max(0, e.pos - 40): e.pos + 40])

最后一行把出错位置前后 40 个字符打出来,肉眼就能看出问题。以下是几种常见踩坑。

1. 根本不是合法 JSON(最常见)

抓了一段”对象序列”贴进来:

{
    "id": 1
},
{
    "id": 2
}

这不是合法 JSON,缺个数组外壳。要么改成:

[
    { "id": 1 },
    { "id": 2 }
]

要么按字段包一层:

{
    "objects": [...]
}

2. NaN / Infinity 没引号

标准 JSON 不支持 NaNInfinity。这样合法:

{"z": "NaN"}     ← 字符串

这样非法:

{"z": NaN}       ← Python json.loads 会崩

服务端如果是 Python json.dumps(..., allow_nan=True) 输出的,可能就是裸 NaN。解决办法:

import json
data = json.loads(content, parse_constant=lambda x: None)
# 或
data = content.replace("NaN", "null")
data = json.loads(data)

3. 结尾多逗号

也是非标准 JSON:

{
    "a": 1,
    "b": 2,      这个逗号 Python 不接受
}

json5 或者预处理正则去掉最后一个逗号:

import re
content = re.sub(r",\s*([}\]])", r"\1", content)

4. UTF-8 BOM

从 Windows 记事本另存的 JSON 常带 BOM,第一个字符是 

json.loads(content)
# JSONDecodeError: Expecting value: line 1 column 1 (char 0)

读文件时用 utf-8-sig

with open("data.json", encoding="utf-8-sig") as f:
    data = json.load(f)

5. 数字全被当字符串了

不会抛异常,但会让下游算错:

{"x": "1228.86"}

type(obj["x"])str,要计算得先转:

x = float(obj["x"])

批量转的话用一个小函数:

def to_float_fields(d, fields):
    for f in fields:
        if f in d:
            d[f] = float(d[f])
    return d

6. 单引号不是 JSON

从 Python print(dict) 复制的字符串是单引号:

{'id': 1, 'name': 'Tom'}

这不是 JSON,是 Python repr。要么请对方 json.dumps 输出,要么本地 ast.literal_eval

import ast
data = ast.literal_eval("{'id': 1, 'name': 'Tom'}")

literal_eval 只解析字面量,比 eval 安全得多。

一句话总结

JSONDecodeError 第一步永远是把 e.pos 附近的原文打出来。九成情况能在 30 秒内定位到具体字符是啥。