Python json.loads 报 JSONDecodeError?8 种成因对照表与一行定位脚本
JSONDecodeError:8 种成因与一行定位脚本
json.loads() 报错最坑的地方不是它不告诉你错在哪,而是——
同一个报错,可能对应四种完全不同的成因。
比如这条最常见的:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
它可能是接口返回了空字符串,可能是网关甩给你一个 HTML 502 页面,
可能是你把 abc123 当 JSON,也可能是你传了 Python 的 None。
报错一模一样,修法完全不同。
所以本文的第一条建议不是"记住 8 种成因",而是:先跑一行脚本把原始文本亮出来。
一、一行定位脚本(先看这个)
报错后第一件事,别猜,把真实内容打出来:
import json, sys
raw = open(sys.argv[1], encoding='utf-8', errors='replace').read() # 文件
# 调接口则用:raw = resp.text
print(repr(raw[:200])) # 看前 200 字符的真实形态
print('len =', len(raw))
try:
json.loads(raw)
except json.JSONDecodeError as e:
print(f'{e.msg} | line {e.lineno} column {e.colno} (char {e.pos})')
repr() 是关键:它会把 \ufeff、换行、前导空格这些肉眼看不见的东西显形。
我遇到过好几次"看起来完全正常却解析失败",打出来才发现开头有个 BOM。
调 HTTP 接口时,再加两行看响应元信息:
print(resp.status_code, resp.headers.get('Content-Type'))
Content-Type 是 text/html 而你在等 JSON——答案就已经出来了。
二、8 种成因对照表(本机实测)
以下均为 Python 3.13.14 本机实跑的原始报错文本:
| # | 成因 | 输入(repr) | 报错信息 |
|---|---|---|---|
| 1 | 空响应体 | '' |
Expecting value: line 1 column 1 (char 0) |
| 2 | HTML 错误页(502/504/登录跳转) | '<html><body>502</body></html>' |
Expecting value: line 1 column 1 (char 0) |
| 3 | 裸字符串没加引号 | 'abc123' |
Expecting value: line 1 column 1 (char 0) |
| 4 | Python None 当 JSON(应为 null) |
'None' |
Expecting value: line 1 column 1 (char 0) |
| 5 | UTF-8 BOM 头 | '\ufeff{"ok": true}' |
Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0) |
| 6 | 单引号字典(Python 字面量) | "{'a': 1}" |
Expecting property name enclosed in double quotes: line 1 column 2 (char 1) |
| 7 | 多余数据(两段 JSON / 日志拼接) | '{} {}' |
Extra data: line 1 column 4 (char 3) |
| 8 | 尾随逗号 | '{"a": 1,}' |
Illegal trailing comma before end of object: line 1 column 8 (char 7) |
注意 1~4 的报错完全一致——这就是"必须看原始文本"的原因。
而 5~8 的报错各不相同,看到Unexpected UTF-8 BOM或Extra data时反而更好定位。
各种成因怎么修
| # | 修法 |
|---|---|
| 1 | 先判空:if not text.strip(): raise ValueError("空响应") |
| 2 | 先 resp.raise_for_status() 确认 200,再看 Content-Type |
| 3 | JSON 字符串必须双引号:"abc123" |
| 4 | 用 json.dumps(None) 生成(得到 null),别手写 "None" |
| 5 | open(f, encoding='utf-8-sig') 读文件 |
| 6 | 用 json.dumps() 生成,别复制 Python 字典的字面量 |
| 7 | 确认是否该用 JSON Lines:逐行 json.loads(line) |
| 8 | 去掉尾随逗号,或用 json5 / YAML 等宽松格式 |
实测:修复验证
BOM 用 utf-8-sig 解码后 : {'ok': True}
JSON Lines 逐行解析 : [{'a': 1}, {'b': 2}]
三、按场景排查(三个最常见的现场)
场景 A:读本地 JSON 文件
with open('config.json', encoding='utf-8-sig') as fh: # utf-8-sig 一并解决 BOM
data = json.load(fh)
Windows 记事本保存的 JSON 默认带 BOM,用 utf-8 读就会撞上成因 5。
写文件也建议显式指定 encoding='utf-8',避免换平台再出问题。
场景 B:调 HTTP 接口
import json, requests
resp = requests.get(url, timeout=5)
resp.raise_for_status() # ① 先确认不是 4xx/5xx 错误页
if 'json' not in resp.headers.get('Content-Type', ''):
print('非 JSON 响应:', resp.text[:200]) # ② 看 Content-Type 与开头
data = resp.json() # ③ 再解析(requests 内置,等价 json.loads)
顺序不能反:先状态码 → 再类型 → 最后解析。大部分"解析失败"其实是请求就没成功。
场景 C:解析大模型(LLM)的返回
这是这几年新增的高频坑:模型返回的 JSON 常被代码围栏包着:
```json
{"title": "示例", "score": 8}
```
直接 json.loads() 会撞上成因 3(开头是反引号)。先剥围栏再解析:
import json, re
def parse_llm_json(text: str):
text = text.strip()
m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S) # 去掉 ```json ... ```
if m:
text = m.group(1).strip()
return json.loads(text)
更稳的做法是在提示词里要求"只输出 JSON、不要任何额外文字",
但解析侧留一手剥离逻辑更保险——模型偶尔会自作主张加说明。
四、防御性写法(可直接抄)
import json
def safe_loads(text: str, *, context: str = ""):
"""带上下文的 JSON 解析:失败时把原始文本一起报出来。"""
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(
f"JSON 解析失败({context}):{e.msg} @ line {e.lineno} col {e.colno}\n"
f"原始文本前 200 字符:{text[:200]!r}"
) from e
核心是失败时把原始文本一起记录——线上出问题时,你拿到的往往只有一行报错,
没有原始响应,那就只能重启复现。
五、30 秒自检清单
print(repr(text[:200]))—— 看真实形态(空?<html?\ufeff?反引号?)- 调接口:看
status_code与Content-Type - 读文件:改用
encoding='utf-8-sig' - 粘贴来的文本:确认是双引号、无尾随逗号
- LLM 返回:先剥 ``` 围栏
- 还是不行 → 整段是不是 JSON Lines?逐行解析试试
小结
Expecting value: line 1 column 1 (char 0)不等于"空响应"——它可能是 4 种成因里的任何一种;- 定位的第一步永远是
repr()把原文亮出来,而不是对着报错猜; - 防御性写法要把原始文本一起记录,否则线上无从排查;
- 读文件用
utf-8-sig,调接口先看状态码与 Content-Type,LLM 返回先剥围栏。
本文环境:Python 3.13.14(CPython)/ Windows 11,仅用标准库
json;
文中 8 条报错信息与修复验证均为本机实跑输出。
涉及requests的段落为接口场景示例(行为以 requests 官方文档为准)。
系列阅读
- Django 返回 JSON 的三种方式与 4 个真实坑——服务端那一侧的坑:中文转义、
safe=False、日期不可序列化、DRF 内容协商。