it编程 > 前端脚本 > Python

Python json模块怎么用?字典列表转JSON字符串详解

4人参与 2026-09-18 Python

开头先讲个真实场景。上个月我在调试一个爬虫项目,从某个数据接口拿到的返回结果是一长串带嵌套结构的文本,当时年轻,想着直接拿字符串切片去匹配数据,结果被里面层层嵌套的括号和转义字符折腾到怀疑人生。后来老老实实用 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}

注意几个转换细节:

这最后一点经常把新手吓一跳,其实是 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类型说明
dictobject键会被转成字符串
list, tuplearray元组也会变成数组
strstring默认转义非ascii字符
int, floatnumber支持整型和浮点型
true / falsetrue / false首字母变小写
nonenull对应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 有几个属性非常有用:

实际项目里我会把解析封装成一个函数,统一处理异常并记录日志:

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)

几个细节说明:

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 管解析加工。把这几个参数用熟,绝大多数日常场景都能覆盖。剩下那些极少碰到的边界情况,去翻官方文档时你也会发现,万变不离其宗。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

您想发表意见!!点此发布评论

推荐阅读

使用Python给Word文档添加文本和图片水印的完整指南

09-18

Python使用json模块来处理JSON数据实现方式

09-18

Python如何处理UTF-8 BOM编码标记

09-18

Python批量自动化实现Excel行列转换全指南

09-18

Python对HTML进行预处理的全流程

09-18

Python中数据类型转换与格式化输出实战指南

09-18

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论