在日常开发中,从日志、配置或第三方接口拿到的JSON数据往往不符合标准规范。Python自带的json.loads()遇到单引号、末尾逗号、无引号键名或注释会直接抛出异常。本文整理5种常见非标准场景,并提供从ast.literal_eval到json5的完整解决方案,帮你快速清洗数据。
场景一:单引号+末尾逗号
手写配置或API日志中经常出现这种格式:
- data = "{'name': 'Tom', 'age': 18,}"
复制代码
方案1:ast.literal_eval(推荐轻量场景)- import ast
- result = ast.literal_eval(data)
- # {'name': 'Tom', 'age': 18}
复制代码 ast.literal_eval能安全解析Python字面量,包括单引号、末尾逗号、True/False/None。但它不能解析真正的JSON扩展(如null、true)。
方案2:正则替换+json.loads- import json, re
- def fix_json(s):
- s = re.sub(r"(?<!\\)'", '"', s)
- s = re.sub(r',\s*([\]\}])', r'\1', s)
- return json.loads(s)
- result = fix_json(data)
复制代码 这种方法灵活但正则容易漏掉字符串内部的引号,复杂场景建议慎用。
场景二:键名未加引号- data = '{name: "Tom", age: 18}'
复制代码 方案:使用demjson3或json5- pip install demjson3
- import demjson3
- result = demjson3.decode(data)
复制代码 或使用更标准的json5:- pip install json5
- import json5
- result = json5.loads(data)
复制代码 json5是JSON5格式的Python实现,支持无引号键名、单引号、注释、尾随逗号等所有非标准特性,且维护活跃。
场景三:包含注释的JSON
某些配置文件会夹杂//或/* */注释:- {
- "name": "Tom", // 用户名
- "age": 18
- }
复制代码 方案1:正则清除注释- import re
- def strip_comments(s):
- s = re.sub(r'//.*', '', s)
- s = re.sub(r'/\*.*?\*/', '', s, flags=re.DOTALL)
- return s
复制代码 清除后再用json.loads解析。
方案2:直接用json5一步到位- import json5
- with open('config.json5', 'r') as f:
- data = json5.load(f)
复制代码
场景四:JavaScript风格的特殊值
数据中包含undefined、NaN、Infinity:- {"value": undefined, "num": NaN}
复制代码 方案:demjson3能自动转换(undefined→None,NaN→nan,Infinity→inf)。json5同样支持。
场景五:十六进制数字(如0xFF)
标准JSON不支持,但json5可以解析。
方案对比
- json.loads:仅标准JSON,快但严格。
- ast.literal_eval:Python字面量,安全无依赖,不能识别null/true/false。
- demjson3:功能较全,但社区维护少,新版Python可能不兼容。
- json5:语法标准、功能全面、持续维护,推荐作为主力方案。
- 正则替换:适合简单一次性清洗,容易出错。
实践建议
1. 如果数据来源固定且简单(如手写配置),优先用ast.literal_eval。
2. 数据格式未知且复杂,直接上json5,开箱即用。
3. 生产环境对性能敏感:先用json5清洗并保存为标准JSON,后续用json.loads。
4. 永远不要信任外部JSON,尽量要求对方输出规范格式。
总结
非标准JSON的本质是生产方与消费方对格式定义不一致。Python生态提供了从轻量ast.literal_eval到全面json5的多种方案,选哪个取决于数据有多“脏”。json5是目前最推荐的折中选择——既有广泛的标准支持,又能覆盖绝大多数异常情况。 |