在Python开发中,处理接口返回的JSON字符串是再常见不过的任务。标准库json提供了loads和dumps两个核心函数,但很多人在实际解析复杂数据时,还是会碰到中文乱码、类型丢失、嵌套取值困难等情况。理解json模块的一些进阶用法,能让我们少写很多防御性代码。

一、中文乱码与ensure_ascii参数
使用json.dumps把字典转成字符串时,默认参数ensure_ascii=True会把所有非ASCII字符转义成uXXXX的形式。这在调试时非常不友好,也增加了日志体积。其实只要把这个参数设为False,就能直接输出中文。
下面是一段对比代码,展示了两种输出方式的差异:
import json
data = {'name': '张三', 'age': 28}
# 默认转义中文
s1 = json.dumps(data)
print(s1) # {"name": "u5f20u4e09", "age": 28}
# 保持中文
s2 = json.dumps(data, ensure_ascii=False)
print(s2) # {"name": "张三", "age": 28}
需要注意的是,当ensure_ascii=False时,如果后续要把字符串写入文件,文件打开应指定encoding='utf-8',否则在部分系统上仍可能出现编码错误。这个细节在跨平台部署时尤其重要。
另外,loads函数本身不存在中文乱码问题,因为它把JSON文本解析为Python对象,字符串在内存中就是正常的unicode。乱码通常出现在dumps之后又用错误编码去打印或存储的环节。
二、安全提取嵌套字段
JSON数据常常是多层嵌套的结构,如果直接用data['a']['b']['c']去取值,一旦某一层key不存在就会抛出KeyError,导致程序中断。更稳妥的做法是使用字典的get方法,它允许我们提供默认值。
对于不确定的深层结构,可以写一个递归的 safe_get 函数,根据路径列表逐步取值:
import json
def safe_get(obj, keys, default=None):
cur = obj
for k in keys:
if isinstance(cur, dict) and k in cur:
cur = cur[k]
else:
return default
return cur
text = '{"user": {"profile": {"nickname": "小李"}}}'
data = json.loads(text)
name = safe_get(data, ['user', 'profile', 'nickname'])
print(name) # 小李
age = safe_get(data, ['user', 'profile', 'age'], 0)
print(age) # 0
这种写法把可能出错的路径遍历封装起来,业务代码里只需要传入key列表和默认值。相比try except包裹整段逻辑,它更轻量,也更容易复用。
如果项目中频繁处理JSONPath风格的取值,也可以引入第三方库如jsonpath-ng,但标准库方案在简单场景下已经足够,而且没有额外依赖。
三、用object_hook转换特殊类型
JSON本身只支持对象、数组、字符串、数字、布尔和null,像Python的datetime、Decimal这类类型无法直接序列化。反过来,有时我们希望把JSON里的某些对象自动变成自定义类的实例,这时候就要用到loads的object_hook参数。
object_hook会在解析出每个字典时被调用,我们可以在里面判断并转换:
import json
from datetime import datetime
def hook(d):
if 'created_at' in d:
d['created_at'] = datetime.fromisoformat(d['created_at'])
return d
text = '{"title": "文章", "created_at": "2023-05-01T10:00:00"}'
obj = json.loads(text, object_hook=hook)
print(type(obj['created_at'])) # <class 'datetime.datetime'>
这样解析完之后,created_at字段就已经是datetime对象,后续做时间计算就不用再手动解析一次。与之对应的是dumps时的default参数,用来把未知类型变成可序列化结构。
使用object_hook时要注意,它会对每一层字典生效,如果数据结构很深,会带来一定性能开销。只在确实需要类型还原的接口中使用即可。
四、流式解析大文件
当JSON文件非常大,一次性loads进内存可能导致内存占用过高。标准库里更底层的json.JSONDecoder虽然灵活,但写起来麻烦。对于数组包裹的大列表,可以用ijson这类第三方库做迭代解析,不过如果环境受限只能用标准库,也可以把数据按行切分或自己用token方式处理。
下面是一个利用标准库做分块读取思路的示例,假设每行是一个独立JSON对象:
import json
def read_json_lines(path):
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
yield json.loads(line)
# 使用方式
# for item in read_json_lines('data.txt'):
# process(item)
这种方式把内存压力从“一次性加载整个文件”降到了“每次一行”,适合日志类JSON流。如果必须是单个巨型JSON数组,标准库本身不提供惰性解析,这时评估是否引入ijson会更合理。
在解析外部输入时,无论用哪种方式,都应当假设数据可能不合法,做好异常捕获。json.JSONDecodeError能告诉我们出错的大致位置和原因,方便记录日志并返回友好提示。
五、小结与编码习惯
掌握json模块的ensure_ascii、get取值、object_hook和分块读取等技巧后,解析常见接口数据会变得轻松很多。建议在团队代码规范里明确:dumps输出中文必须设ensure_ascii=False;访问外部JSON字段优先用安全取值函数;时间等类型通过hook统一还原。
这些习惯看似细小,却能在联调阶段减少大量乱码和崩溃问题,也让代码可读性更高。当你再面对一份复杂的JSON报文时,就能更从容地把它变成干净的Python对象。