Python json模块怎么用?字典列表转JSON字符串详解
开头先讲个真实场景。上个月我在调试一个爬虫项目,从某个数据接口拿到的返回结果是一长串带嵌套结构的文本,当时年轻,想着直接拿字符串切片去匹配数据,结果被里面层层嵌套的括号和转义字符折腾到怀疑人生。后来老老实实用 json 模块去解析,三行代码解决问题。这件事给我的教训是: 在Python里处理数据交换,json模块就是那个最不该绕开的工具。
这篇文章就围绕 json 模块的核心玩法展开,重点解决三类问题:字典/列表怎么变成JSON字符串,JSON字符串怎么变回字典/列表,以及实际项目中文件读写、异常处理、自定义对象序列化这些绕不开的坑。适合刚入门Python、或者已经写了一阵子但一直靠 eval 和字符串切片硬扛的读者。看完你可以直接把文中的写法抄进自己的项目里。
1. 为什么程序离不开JSON转换
1.1 JSON是程序之间的“普通话”
先理清一个概念。JSON全称是JavaScript Object Notation,但今天它早就超出了JavaScript的范畴,成了后端接口、配置文件、日志存储、数据交换的事实标准。你在网上看天气接口、查快递单号、刷微博时间线,底层返回的数据十有八九是JSON格式。
Python里的字典和列表是内存中的对象,一个程序用完后进程结束,数据就没了。要让数据存活下来、或者传给另一个语言写的服务,必须把它变成一串 纯文本 。这个“把内存对象变成文本”的动作叫 序列化 ,反过来“把文本还原成内存对象”叫 反序列化 。 json 模块干的就是这件事。
1.2 不转换直接用字符串拼接行不行
很多人刚开始会想:我不就是拼个字符串嘛,用f-string不就行了?比如构造一个用户信息:
name = "张三"
age = 25
# 不推荐:手拼JSON字符串
payload = '{"name": "' + name + '", "age": ' + str(age) + '}'
这个写法一旦遇到name里有双引号、换行符,或者age变成None,字符串直接崩给你看。就算你小心翼翼转义了所有特殊字符,下一个维护你代码的人内心也是崩溃的。
用 json 模块只需要:
import json
payload = json.dumps({"name": "张三", "age": 25})
少了转义、少了类型转换、少了拼接逻辑,而且保证输出的字符串 一定是合法JSON 。这不是省几行代码的问题,是把一类错误直接消灭掉。
2. 字典/列表转JSON字符串:json.dumps的完整姿势
2.1 最基础的一行代码
dumps 方法负责把Python对象变成JSON字符串。
“dump”加“s”的“s”代表string,记住这个规律就不会和后面要讲的 dump (写文件)搞混。
import json
data = {
"name": "张三",
"age": 25,
"tags": ["python", "json"],
"is_active": True,
"score": None
}
json_str = json.dumps(data)
print(json_str)
# {"name": "\u5f20\u4e09", "age": 25, "tags": ["python", "json"], "is_active": true, "score": null}
注意几个转换细节:
- Python的
True变成小写的true,False变成false - Python的
None变成小写的null - 元组会被转成JSON数组
- 字符串里的中文默认被转成了
\uXXXX形式的Unicode转义序列
这最后一点经常把新手吓一跳,其实是 ensure_ascii 参数在起作用,下面单独说。
2.2 中文乱码问题与ensure_ascii=False
默认情况下, dumps 把所有非ASCII字符都转成 \uXXXX 。这样设计是为了保证生成的JSON字符串在任何编码环境下都不会乱码,但可读性实在太差。
如果你这个JSON是给人看的——比如写配置文件、导出数据报表——加上 ensure_ascii=False :
json_str = json.dumps(data, ensure_ascii=False)
print(json_str)
# {"name": "张三", "age": 25, "tags": ["python", "json"], "is_active": true, "score": null}
这里要提醒一个细节: ensure_ascii=False 只影响输出,不影响JSON的合法性。无论转不转义,解析方拿到的都是同一个字符串。
真正要注意的是文件读写时的编码要配套,写文件用 encoding="utf-8" ,读文件同样用 encoding="utf-8" ,否则一边是UTF-8一边是GBK,照样乱码。
2.3 indent、sort_keys、separators的参数细节
格式化输出:indent
调试时或者写配置文件,希望输出有缩进、可读性强,用 indent 参数:
json_str = json.dumps(data, ensure_ascii=False, indent=2) print(json_str)
输出效果:
{
"name": "张三",
"age": 25,
"tags": [
"python",
"json"
],
"is_active": true,
"score": null
}
indent 的单位是空格数,常用的有2和4。
indent=0 会输出换行但不缩进, indent=None (默认)输出的是最紧凑的单行格式。
固定键顺序:sort_keys
字典在Python 3.7+是保持插入顺序的,但JSON本身不保证键的顺序。
如果你希望输出的JSON键按字母排序,方便对比或测试,用 sort_keys=True :
json_str = json.dumps(data, ensure_ascii=False, sort_keys=True)
# {"age": 25, "is_active": true, "name": "张三", "score": null, "tags": ["python", "json"]}
这个参数在做配置对比、生成固定签名的场景下特别有用。
前后两次生成的JSON字符串完全一致,方便做哈希或比对。
压缩存储:separators
反过来,如果你要把JSON存到数据库字段、缓存或日志里,希望体积尽可能小,用 separators 参数去掉多余空格:
json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
print(json_str)
# {"name":"张三","age":25,"tags":["python","json"],"is_active":true,"score":null}
separators 接收一个二元组,第一个是元素之间的分隔符,第二个是键值之间的分隔符。默认是 (', ', ': ') ,改成 (',', ':') 后每个键值对之间少一个空格,数据量大的时候压缩效果可观。
我之前处理过几万条记录导出成JSON,光这一项就省了大约15%的存储空间。
2.4 Python类型与JSON类型的映射表
搞清楚类型对应关系,是避免序列化报错的关键。
这个表建议刻进脑子里:
| Python类型 | JSON类型 | 说明 |
|---|---|---|
| dict | object | 键会被转成字符串 |
| list, tuple | array | 元组也会变成数组 |
| str | string | 默认转义非ASCII字符 |
| int, float | number | 支持整型和浮点型 |
| True / False | true / false | 首字母变小写 |
| None | null | 对应JSON的null |
| bytes | 不支持 | 默认会抛TypeError |
| set | 不支持 | 默认会抛TypeError |
| datetime | 不支持 | 需要自定义序列化逻辑 |
bytes 、 set 、 datetime 这些类型直接 dumps 会报 TypeError: Object of type XXX is not JSON serializable ,这是最常遇到的异常之一。解决办法后面第5章专门讲。
2.5 skipkeys参数:键不是字符串时怎么办
JSON规定键必须是字符串,如果Python字典里的键是别的类型, dumps 默认会报 TypeError 。但有些场景下你确实有非字符串键,比如元组键:
data = {(1, 2): "a", (3, 4): "b"}
json.dumps(data) # TypeError: keys must be str, int, float, bool or None, not tuple
加上 skipkeys=True 会直接跳过这些键,而不是报错:
json.dumps(data, skipkeys=True) # {}
这个参数要慎用。跳过键等于静默丢数据,很容易留下隐患。
我通常建议:先检查数据源,把键规范成字符串,而不是用 skipkeys 掩盖问题。
3. JSON字符串转回字典/列表:json.loads与异常处理
3.1 基础用法
loads 和 dumps 正好相反,把JSON字符串转回Python对象:
import json
json_str = '{"name": "张三", "age": 25, "tags": ["python", "json"], "is_active": true, "score": null}'
data = json.loads(json_str)
print(data)
# {'name': '张三', 'age': 25, 'tags': ['python', 'json'], 'is_active': True, 'score': None}
注意转换规则是逆向的:JSON的 true 变回Python的 True , null 变回 None ,数组变回列表。
如果JSON字符串最外层是数组,加载回来就是列表:
json_str = '[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]'
data = json.loads(json_str)
print(data[0]["name"]) # a
3.2 JSONDecodeError异常处理
loads 最常见的失败是字符串格式不合法。
比如你从接口拿到的数据被截断了,或者手写的JSON少了一个大括号:
bad_str = '{"name": "张三", "age": 25,'
try:
data = json.loads(bad_str)
except json.JSONDecodeError as e:
print(f"解析失败:{e}")
print(f"出错位置:第 {e.lineno} 行,第 {e.colno} 列")
JSONDecodeError 有几个属性非常有用:
doc:原始字符串pos:出错位置在字符串中的索引lineno:出错行号colno:出错列号
实际项目里我会把解析封装成一个函数,统一处理异常并记录日志:
def safe_loads(json_str, default=None):
try:
return json.loads(json_str)
except (json.JSONDecodeError, TypeError):
return default
这样接口返回异常数据时程序不会直接崩溃,而是返回一个默认值,后续逻辑自行决定怎么处理。
3.3 为什么不要用eval代替loads
很多人刚学的时候会问:JSON字符串看起来就是一个Python字面量,直接 eval 不就行了吗?
data = eval('{"name": "张三"}') # 能跑,但极度危险
eval 会执行任意Python表达式。如果JSON字符串里混入了恶意代码——比如 {"name": __import__("os").system("rm -rf /")} ——你的程序就把系统命令执行了。
json.loads 只解析JSON语法,不执行任何代码,这是根本区别。 凡是JSON解析一律用json模块,不用eval,这句话值得写进团队规范。
3.4 parse_int与parse_float:解析数字时的定制钩子
loads 还支持两个不太常用但很好用的参数: parse_int 和 parse_float 。它们负责把JSON字符串里的数字转成Python对象。
比如接口返回的数字是字符串形式的"100",你想在解析阶段自动转成 Decimal避免浮点精度问题:
from decimal import Decimal
data = json.loads('{"price": 19.99}', parse_float=Decimal)
print(data["price"]) # 19.99,类型是Decimal
这个场景在处理金额、精度敏感的数据时非常重要。
默认的 parse_float=float 会有二进制浮点误差, 0.1 + 0.2 不等于 0.3 的问题在JSON解析中同样存在。用 Decimal 可以规避。
4. 实际项目里更常用的json.dump与json.load(文件读写)
4.1 为什么推荐dump而不是先dumps再写文件
dumps 是生成字符串, dump 是直接写入文件对象。
看名字很像,使用场景完全不同:
import json
data = {"name": "张三", "age": 25}
# 方式一:dumps + 手动写文件(不推荐)
with open("data.json", "w", encoding="utf-8") as f:
f.write(json.dumps(data, ensure_ascii=False, indent=2))
# 方式二:dump直接写文件(推荐)
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
推荐 dump 的原因很实在:你不需要自己管理字符串的写入生命周期, dump 内部处理好了。读文件时对应 load :
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
4.2 配置文件读写的实操模板
我常用的JSON配置文件读写模板长这样:
import json
from pathlib import Path
CONFIG_PATH = Path("config.json")
def load_config(path: Path = CONFIG_PATH) -> dict:
if not path.exists():
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_config(data: dict, path: Path = CONFIG_PATH) -> None:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True)
几个细节说明:
Path对象可以直接传给open,不用先转成字符串encoding="utf-8"必须显式指定,避免Windows下默认GBK编码导致中文乱码indent=2让配置文件有可读性,sort_keys=True让键顺序固定,方便git diff对比
4.3 大JSON文件的读取问题
如果你的JSON文件很大(几GB级别),直接用 json.load 会把整个文件载入内存,很容易内存爆炸。这种情况有两个处理思路:
思路一是 流式读取 ,但标准 json 模块不支持增量解析,需要逐段处理或用 ijson 这类第三方库。
思路二是 按行存储 ,设计数据格式时每行一个JSON对象,读取时逐行 loads :
def load_jsonl(path):
"""逐行读取JSON Lines格式的文件"""
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 record in load_jsonl("huge_data.jsonl"):
print(record["id"])
JSON Lines格式在日志分析和数据管道中非常流行,每行一个独立JSON对象,天然支持流式处理、断点续读和并行分块。
5. 我踩过的坑和进阶处理技巧
5.1 datetime和自定义对象无法序列化
这是 json.dumps 报错频率最高的场景。模型里有 datetime 字段,一 dumps 就抛 TypeError 。
解决方案是用 default 参数指定自定义转换函数:
from datetime import datetime
def json_default(obj):
if isinstance(obj, datetime):
return obj.strftime("%Y-%m-%d %H:%M:%S")
if hasattr(obj, "__dict__"):
return obj.__dict__
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
data = {"name": "张三", "created_at": datetime.now()}
json_str = json.dumps(data, ensure_ascii=False, default=json_default)
更优雅的方式是继承 json.JSONEncoder :
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
json_str = json.dumps(data, ensure_ascii=False, cls=CustomEncoder)
两种方式效果类似, cls 参数适合在多个地方复用同一套编码逻辑,模块化更好。
我在项目里通常维护一个 utils/json_encoders.py ,集中放所有自定义类型的转换规则,所有接口统一引用。
5.2 浮点数精度与Decimal
前面提到过 parse_float 。这里再补充一个实际案例:有一次我从支付平台回调里解析金额,用默认的 float 解析,结果 0.29 被存成了 0.29000000000000004 ,对账怎么都对不上。后来改成:
from decimal import Decimal
def parse_decimal(s):
return Decimal(s)
data = json.loads(callback_body, parse_float=parse_decimal)
金额字段从此稳定精确。处理金额、汇率、坐标这类对精度敏感的数据时,强烈建议用 Decimal 替代 float 。
5.3 非字符串键被静默转换
JSON的键必须是字符串,但Python字典的键可以是整数。 dumps 时整数键会 自动转成字符串 ,这个过程是静默的,很多时候你没意识到数据已经变了:
data = {1: "a", 2: "b"}
json_str = json.dumps(data)
print(json_str) # {"1": "a", "2": "b"}
转回来时:
recovered = json.loads(json_str)
print(recovered) # {'1': 'a', '2': 'b'}
注意,键从 1 变成了 '1' 。如果你后续用 recovered[1] 取值,会直接KeyError。这是个非常隐蔽的坑。
解决方案是在解析后统一处理键类型,或者设计数据时就避免非字符串键。把字典的键统一规范为字符串,省掉后面一堆麻烦。
5.4 超大整数精度丢失
如果你处理的JSON字符串里包含超过JavaScript安全整数范围的数字(比如雪花算法生成的ID),直接 json.loads 解析成Python的 int 没问题。但如果这个JSON字符串是给前端JS用的,JS的 Number 类型会精度丢失。
比如 9223372036854775807 在JS里会被解析成 9223372036854776000 。解决办法是把大整数序列化为字符串:
def json_default(obj):
if isinstance(obj, int) and obj > 2**53:
return str(obj)
return super().default(obj) if hasattr(super(), 'default') else str(obj)
这个细节在和前端联调时非常关键。经验是: 所有可能超过2^53的整数,传输时一律转成字符串 。
5.5 object_hook:解析时定制对象结构
loads 支持 object_hook 参数,可以在解析JSON对象时对结果做二次加工。比如你想把嵌套字典自动转成某个类的实例:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def user_hook(d):
if "name" in d and "age" in d:
return User(d["name"], d["age"])
return d
data = json.loads('{"user": {"name": "张三", "age": 25}}', object_hook=user_hook)
print(data["user"].name) # 张三
object_hook 对JSON里 每个对象 都会调用一次,所以函数内部要判断当前对象是否包含目标字段,不匹配的原样返回。这个机制在处理嵌套配置、复杂数据模型时能省掉很多手动转换的代码。
5.6 HTTP接口中的json参数与常见错误
用 requests 库请求接口时,很多人分不清 json 参数和 data 参数的区别。 json= 会自动帮你做序列化并设置 Content-Type: application/json :
import requests
payload = {"name": "张三", "age": 25}
resp = requests.post("https://api.example.com/users", json=payload)
如果手动用 data=json.dumps(payload) ,还要自己设置Header:
headers = {"Content-Type": "application/json"}
resp = requests.post("https://api.example.com/users", data=json.dumps(payload), headers=headers)
两种方式等价,但 json= 更省心。接收响应时, resp.json() 内部其实就是调用的 json.loads(resp.text) ,不需要自己再解析一遍。
这里要特别提醒一个高频报错:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
这个错误通常表示响应体里 根本就不是JSON——可能是空字符串、可能是HTML错误页、可能是网关返回的纯文本。
出现这个错误时,先打印 resp.text 看看实际内容,别急着怀疑json模块。
6. 三个月实操后的经验总结
最后分享几个我实际项目中沉淀下来的习惯,都是反复踩坑后总结的:
统一封装JSON读写工具函数。 不要在每个模块里直接散落 json.dumps 和 json.loads ,封装统一的工具函数,把 ensure_ascii=False 、 indent=2 、 encoding="utf-8" 这些参数写死在工具层,团队里所有人调同一个入口。
所有涉及金额的字段用Decimal。 从接口解析到序列化返回,全程走 parse_float=Decimal 和自定义 default ,把精度问题挡在入口和出口。
日志里打印JSON片段时压缩存储。 大JSON打日志会刷屏,用 separators=(',', ':') 生成紧凑格式,既保留信息又不占太多空间。
接口返回前做一次合法性校验。 用 json.dumps 序列化响应体时,如果某个字段类型不支持,会直接在接口层报500。在开发阶段我会写一个递归校验函数,提前发现不可序列化的字段。
测试用例里固定sort_keys=True。 断言接口返回时,开启 sort_keys 让JSON字符串有确定顺序,测试用例更稳定,不会因为字典遍历顺序不同而误报。
json模块的核心玩法就这些: dumps / loads 管字符串, dump / load 管文件, ensure_ascii 管中文, indent 管格式, sort_keys 管顺序, default 管自定义类型, object_hook 管解析加工。把这几个参数用熟,绝大多数日常场景都能覆盖。剩下那些极少碰到的边界情况,去翻官方文档时你也会发现,万变不离其宗。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
使用Pandas的ExcelWriter操作excel的方法
这篇文章主要介绍了使用Pandas的ExcelWriter操作excel的方法,ExcelWriter这个插件有个坑,就是已经设置好的格式是无法更改的,因此,由pandas转成excel的时候,必须将格式清除,尤其是表头的格式需要大家多多注意,本文结合示例代码讲解的非常详细,需要的朋友参考下吧2023-11-11
微软开源最强Python自动化神器Playwright(不用写一行代码)
这篇文章主要介绍了微软开源最强Python自动化神器Playwright(不用写一行代码),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2021-01-01


最新评论