W weiserv
← 返回博客

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-Typetext/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 BOMExtra 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 秒自检清单

  1. print(repr(text[:200])) —— 看真实形态(空?<html\ufeff?反引号?)
  2. 调接口:看 status_codeContent-Type
  3. 读文件:改用 encoding='utf-8-sig'
  4. 粘贴来的文本:确认是双引号、无尾随逗号
  5. LLM 返回:先剥 ``` 围栏
  6. 还是不行 → 整段是不是 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 官方文档为准)。


系列阅读

广告位占位 · post-inline