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

 更新时间:2026年09月17日 15:01:45   作者:老李校长  
还在用字符串拼接处理JSON?Python的json模块才是正道,本文详细讲解字典和列表如何转成JSON字符串,JSON字符串如何解析回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}

注意几个转换细节:

  • 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类型说明
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 有几个属性非常有用:

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Python基于百度API识别并提取图片中文字

    Python基于百度API识别并提取图片中文字

    本文主要实现了利用百度 AI 开发平台的 OCR 文字识别 API 识别并提取图片中的文字。具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • 使用Pandas的ExcelWriter操作excel的方法

    使用Pandas的ExcelWriter操作excel的方法

    这篇文章主要介绍了使用Pandas的ExcelWriter操作excel的方法,ExcelWriter这个插件有个坑,就是已经设置好的格式是无法更改的,因此,由pandas转成excel的时候,必须将格式清除,尤其是表头的格式需要大家多多注意,本文结合示例代码讲解的非常详细,需要的朋友参考下吧
    2023-11-11
  • 详解Python相关文件常见的后缀名

    详解Python相关文件常见的后缀名

    常见的 Python 文件后缀有:py、pyc 、pyo、 pyi、pyw、 pyd、 pyx 等。本文给大家介绍Python相关文件常见的后缀名,感兴趣的朋友跟随小编一起看看吧
    2021-05-05
  • 微软开源最强Python自动化神器Playwright(不用写一行代码)

    微软开源最强Python自动化神器Playwright(不用写一行代码)

    这篇文章主要介绍了微软开源最强Python自动化神器Playwright(不用写一行代码),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • 解决tensorflow训练时内存持续增加并占满的问题

    解决tensorflow训练时内存持续增加并占满的问题

    今天小编就为大家分享一篇解决tensorflow训练时内存持续增加并占满的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-01-01
  • Python中的字典遍历备忘

    Python中的字典遍历备忘

    这篇文章主要介绍了Python中的字典遍历备忘,本文列举了多种字典遍历方法,适合初学者查看,并简单讲解了interitems和iterms区别,需要的朋友可以参考下
    2015-01-01
  • 使用Python获取并处理IP的类型及格式方法

    使用Python获取并处理IP的类型及格式方法

    今天小编就为大家分享一篇使用Python获取并处理IP的类型及格式方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-11-11
  • flask框架单元测试原理与用法实例分析

    flask框架单元测试原理与用法实例分析

    这篇文章主要介绍了flask框架单元测试原理与用法,结合实例形式较为详细的分析了单元测试的概念、原理及基本用法,需要的朋友可以参考下
    2019-07-07
  • 基于Python 函数和方法的区别说明

    基于Python 函数和方法的区别说明

    这篇文章主要介绍了基于Python 函数和方法的区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • 用Python编写生成树状结构的文件目录的脚本的教程

    用Python编写生成树状结构的文件目录的脚本的教程

    这篇文章主要介绍了用Python编写生成树状结构的文件目录的脚本的教程,是一个利用os模块下各函数的简单实现,需要的朋友可以参考下
    2015-05-05

最新评论