Philipp Acsany 2025-08-20
介绍 JSON
JSON 是 JavaScript Object Notation(JavaScript 对象表示法)的缩写。顾名思义,JSON 起源于 JavaScript。然而,JSON 已经超越了其起源,成为一种与编程语言无关的数据交换标准。
JSON 的流行主要归功于 JavaScript 语言对其原生支持,这使得在 Web 浏览器中解析 JSON 具有出色的性能。此外,JSON 的语法非常简洁,人类和计算机都能轻松地读写 JSON 数据。
为了对 JSON 有个初步印象,请看下面这个示例代码:
hello_world.json
{
"greeting": "Hello, world!"
}
稍后你将更深入地学习 JSON 语法。现在,请注意 JSON 格式是基于文本的。换句话说,你可以使用任意代码编辑器创建 JSON 文件。一旦将文件扩展名设置为 .json,大多数代码编辑器都会自动对 JSON 数据进行语法高亮显示:
![编辑器截图:VS Code 使用 Bearded 配色方案显示 JSON 文件的语法高亮效果]
上图展示了 VS Code 如何使用 Bearded 配色主题显示 JSON 数据。接下来,我们将更仔细地查看 JSON 格式的语法!
检查 JSON 语法
在上一节中,你已经对 JSON 数据的外观有了初步印象。作为一名 Python 开发者,JSON 结构很可能让你联想到常见的 Python 数据结构,比如包含字符串键和值的字典。如果你理解 Python 字典的语法,那么你已经掌握了 JSON 对象的一般语法。
注意:稍后在本教程中,你将了解到 JSON 文档的顶层也可以使用列表和其他数据类型。
Python 字典与 JSON 对象之间的相似性并非巧合。建立 JSON 作为首选数据交换格式的理念之一,就是让开发者无论使用哪种编程语言,都能尽可能方便地处理 JSON:
[键值对集合和数组] 是通用的数据结构。几乎所有现代编程语言都以某种形式支持它们。因此,一种能与编程语言互操作的数据格式也基于这些结构是有道理的。
为了进一步探索 JSON 语法,请创建一个名为 hello_frieda.json 的新文件,并添加以下更复杂的 JSON 结构作为文件内容:
hello_frieda.json
{
"name": "Frieda",
"isDog": true,
"hobbies": ["eating", "sleeping", "barking"],
"age": 8,
"address": {
"work": null,
"home": ["Berlin", "Germany"]
},
"friends": [
{
"name": "Philipp",
"hobbies": ["eating", "sleeping", "reading"]
},
{
"name": "Mitch",
"hobbies": ["running", "snacking"]
}
]
}
上面的代码展示了关于一只名叫 Frieda 的狗的数据,采用 JSON 格式表示。顶层值是一个 JSON 对象。与 Python 字典一样,JSON 对象用花括号 ({}) 包裹。
第 1 行以左花括号 ({) 开始 JSON 对象,然后在第 20 行末尾用右花括号 (}) 关闭该对象。
注意:尽管 JSON 中的空白字符无关紧要,但通常习惯使用两个或四个空格进行缩进格式化 JSON 文档。如果 JSON 文档的文件大小很重要,那么你可以考虑通过删除空白字符来压缩(minify)JSON 文件。稍后你将学习更多关于压缩 JSON 数据的内容。
在 JSON 对象内部,你可以定义零个、一个或多个键值对。如果添加多个键值对,则必须用逗号 (,) 分隔它们。
JSON 对象中的键值对用冒号 (:) 分隔。冒号左侧是键,必须用双引号 (") 包裹的字符串。与 Python 不同,JSON 字符串不支持单引号 (')。
JSON 文档中的值仅限于以下数据类型:
| JSON 数据类型 | 描述 |
|---|---|
| object | 花括号 ({}) 内的键值对集合 |
| array | 方括号 ([]) 包裹的值列表 |
| string | 双引号 ("") 包裹的文本 |
| number | 整数或浮点数 |
| boolean | true 或 false(无引号) |
| null | 表示空值,写作 null |
与字典和列表一样,你可以在 JSON 对象和数组中嵌套数据。例如,你可以将一个对象作为另一个对象的值。同样,你也可以在 JSON 数组中使用任何其他允许的值作为项。
作为一名 Python 开发者,你可能需要特别注意布尔值。你必须使用小写的 JavaScript 风格布尔值 true 或 false,而不是 Python 中的大写 True 或 False。
遗憾的是,JSON 语法中还有一些其他细节可能会让开发者感到困惑。接下来我们将看看这些陷阱。
探索 JSON 语法陷阱
JSON 标准不允许注释、尾随逗号或字符串使用单引号。这对于习惯 Python 字典或 JavaScript 对象的开发者来说可能会感到困惑。
以下是之前 JSON 文件的一个较小版本,但包含无效语法:
❌ 无效的 JSON
{
"name": 'Frieda',
"address": {
"work": null, // Doesn't pay rent either
"home": "Berlin",
},
"friends": [
{
"name": "Philipp",
"hobbies": ["eating", "sleeping", "reading",]
}
]
}
高亮显示的行包含无效的 JSON 语法:
- 第 2 行:字符串使用单引号包裹
- 第 4 行:使用了行内注释
- 第 5 行:最后一个键值对后面有尾随逗号
- 第 10 行:数组中有尾随逗号
作为 Python 开发者,使用双引号是可以适应的。注释有助于解释代码,尾随逗号可以让移动代码行变得更灵活。这就是为什么一些开发者喜欢使用 Human JSON (Hjson) 或带注释的 JSON (JSONC)。
Hjson 让你可以自由使用注释、省略属性之间的逗号,或者创建无引号的字符串。除了花括号 ({}) 外,Hjson 语法看起来像是 YAML 和 JSON 的混合体。
JSONC 比 Hjson 更严格一些。与普通 JSON 相比,JSONC 允许使用注释和尾随逗号。你可能在编辑 VS Code 的 settings.json 文件时遇到过 JSONC。在其配置文件中,VS Code 以 JSONC 模式工作。但对于普通 JSON 文件,VS Code 会更严格,并指出 JSON 语法错误。
如果你想确保编写有效的 JSON,你的代码编辑器可以提供很大帮助。上面的无效 JSON 文档包含了每个不正确 JSON 语法出现位置的标记:
![编辑器显示 JSON 语法错误标记的截图]
当你不想依赖代码编辑器时,也可以使用在线工具验证你编写的 JSON 语法是否正确。流行的 JSON 验证在线工具有 JSON Lint 和 JSON Formatter。
稍后在本教程中,你将学习如何从终端舒适地验证 JSON 文档。但在那之前,是时候了解如何在 Python 中处理 JSON 数据了。
使用 Python 编写 JSON
Python 通过内置的 json 模块支持 JSON 格式。json 模块专门用于读写格式化为 JSON 的字符串。这意味着你可以方便地将 Python 数据类型转换为 JSON 数据,反之亦然。
将数据转换为 JSON 格式的过程称为序列化。这个过程涉及将数据转换为一系列字节,以便存储或通过网络传输。相反的过程称为反序列化,涉及将 JSON 格式的数据解码回 Python 中的可用形式。
你将首先使用 json 模块将 Python 代码序列化为 JSON 数据。
将 Python 字典转换为 JSON
在 Python 中处理 JSON 时最常见的操作之一是将 Python 字典转换为 JSON 对象。为了了解这是如何工作的,请打开你的 Python REPL 并跟随下面的代码:
>>> import json
>>> food_ratings = {"organic dog food": 2, "human food": 10}
>>> json.dumps(food_ratings)
'{"organic dog food": 2, "human food": 10}'
导入 json 模块后,你可以使用 .dumps() 将 Python 字典转换为 JSON 格式的字符串,该字符串表示一个 JSON 对象。
重要的是要理解,当你使用 .dumps() 时,返回的是一个 Python 字符串。换句话说,你并没有创建任何类型的 JSON 数据类型。结果类似于使用 Python 内置的 str() 函数:
>>> str(food_ratings)
"{'organic dog food': 2, 'human food': 10}"
当你的 Python 字典不包含字符串作为键,或者值不能直接转换为 JSON 格式时,使用 json.dumps() 会变得更有趣:
>>> numbers_present = {1: True, 2: True, 3: False}
>>> json.dumps(numbers_present)
'{"1": true, "2": true, "3": false}'
在 numbers_present 字典中,键 1、2 和 3 是数字。一旦你使用 .dumps(),字典键就会在 JSON 格式的字符串中变成字符串。
注意:当你将字典转换为 JSON 时,字典键在 JSON 中始终是字符串。
字典中的 Python 布尔值会变成 JSON 布尔值。如前所述,JSON 布尔值与 Python 布尔值之间微小但重要的区别在于 JSON 布尔值是小写的。
Python 的 json 模块的巧妙之处在于它会自动处理转换。当你使用变量作为字典键时,这会非常有用:
>>> dog_id = 1
>>> dog_name = "Frieda"
>>> dog_registry = {dog_id: {"name": dog_name}}
>>> json.dumps(dog_registry)
'{"1": {"name": "Frieda"}}'
当将 Python 数据类型转换为 JSON 时,json 模块接收的是已求值的值。在此过程中,json 严格遵循 JSON 标准。例如,当将整数键如 1 转换为字符串 "1" 时。
将其他 Python 数据类型序列化为 JSON
json 模块允许你将常见的 Python 数据类型转换为 JSON。以下是你可以转换为 JSON 值的所有 Python 数据类型和值的概览:
| Python | JSON |
|---|---|
| dict | object |
| list | array |
| tuple | array |
| str | string |
| int | number |
| float | number |
| True | true |
| False | false |
| None | null |
请注意,不同的 Python 数据类型(如列表和元组)都会序列化为相同的 JSON 数组数据类型。当你将 JSON 数据转换回 Python 时,这可能会导致问题,因为数据类型可能与之前不同。稍后在学习如何读取 JSON 时,你将探索这个陷阱。
字典可能是你在 JSON 中最常使用的顶层 Python 数据类型。但你也可以像处理字典一样顺利地转换上面列出的数据类型,使用 json.dumps()。例如,取一个布尔值或列表:
>>> json.dumps(True)
'true'
>>> json.dumps(["eating", "sleeping", "barking"])
'["eating", "sleeping", "barking"]'
JSON 文档的顶层可能只包含一个标量值,比如一个数字。这仍然是有效的 JSON。但大多数情况下,你希望处理键值对的集合。就像并非所有数据类型都可以在 Python 中用作字典键一样,并非所有键都可以转换为 JSON 键字符串:
| Python 数据类型 | 是否允许作为 JSON 键 |
|---|---|
| dict | ❌ |
| list | ❌ |
| tuple | ❌ |
| str | ✅ |
| int | ✅ |
| float | ✅ |
| bool | ✅ |
| None | ✅ |
你不能使用字典、列表或元组作为 JSON 键。对于字典和列表,这条规则是有道理的,因为它们是不可哈希的。但即使元组是可哈希的并且在字典中允许作为键,当你尝试使用元组作为 JSON 键时,也会得到 TypeError:
>>> available_nums = {(1, 2): True, 3: False}
>>> json.dumps(available_nums)
Traceback (most recent call last):
...
TypeError: keys must be str, int, float, bool or None, not tuple
通过提供 skipkeys 参数,你可以在创建包含不受支持的 Python 键的 JSON 数据时避免 TypeError:
>>> json.dumps(available_nums, skipkeys=True)
'{"3": false}'
当你将 json.dumps() 中的 skipkeys 设置为 True 时,Python 会跳过那些不受支持的键,否则会引发 TypeError。结果是一个只包含输入字典子集的 JSON 格式字符串。在实践中,你通常希望你的 JSON 数据尽可能接近输入对象。因此,你必须谨慎使用 skipkeys,以免在调用 json.dumps() 时丢失信息。
注意:如果你遇到需要将不受支持的对象转换为 JSON 的情况,可以考虑创建
JSONEncoder的子类并实现.default()方法。
当你使用 json.dumps() 时,可以使用额外的参数来控制生成的 JSON 格式字符串的外观。例如,你可以通过将 sort_keys 参数设置为 True 来对字典键进行排序:
>>> toy_conditions = {"chew bone": 7, "ball": 3, "sock": -1}
>>> json.dumps(toy_conditions, sort_keys=True)
'{"ball": 3, "chew bone": 7, "sock": -1}'
当你将 sort_keys 设置为 True 时,Python 在序列化字典时会按字母顺序对键进行排序。当你以前的字典键代表数据库的列名,并且想要以有序的方式向用户显示它们时,对 JSON 对象的键进行排序会很有用。
json.dumps() 的另一个值得注意的参数是 indent,这可能是你在序列化 JSON 数据时最常用的参数。稍后在本教程的美化 JSON 部分,你将探索 indent。
当你将 Python 数据类型转换为 JSON 格式时,你通常有一个目标。最常见的是,你会使用 JSON 来持久化和交换数据。为此,你需要将 JSON 数据保存在 Python 程序之外。接下来,你将探索如何将 JSON 数据保存到文件中。
使用 Python 编写 JSON 文件
当你想在 Python 程序之外保存数据时,JSON 格式会很有用。与其启动一个数据库,你可能会决定使用 JSON 文件来为你的工作流存储数据。同样,Python 为你提供了支持。
要将 Python 数据写入外部 JSON 文件,你使用 json.dump()。这是一个与之前看到的函数类似的函数,但名称末尾没有 s:
hello_frieda.py
import json
dog_data = {
"name": "Frieda",
"is_dog": True,
"hobbies": ["eating", "sleeping", "barking",],
"age": 8,
"address": {
"work": None,
"home": ("Berlin", "Germany",),
},
"friends": [
{
"name": "Philipp",
"hobbies": ["eating", "sleeping", "reading",],
},
{
"name": "Mitch",
"hobbies": ["running", "snacking",],
},
],
}
with open("hello_frieda.json", mode="w", encoding="utf-8") as write_file:
json.dump(dog_data, write_file)
在第 3 到 22 行,你定义了一个 dog_data 字典,在第 25 行使用上下文管理器将其写入 JSON 文件。为了正确表明文件包含 JSON 数据,你将文件扩展名设置为 .json。
当你使用 open() 时,最好定义编码。对于 JSON,通常希望在读写文件时使用 "utf-8" 编码:
RFC 要求 JSON 必须使用 UTF-8、UTF-16 或 UTF-32 表示,其中 UTF-8 是推荐的默认编码,以实现最大的互操作性。
json.dump() 函数有两个必需的参数:
- 你要写入的对象
- 你要写入的文件
除此之外,还有许多可选参数用于 json.dump()。json.dump() 的可选参数与 json.dumps() 相同。稍后在本教程中美化和压缩 JSON 文件时,你将研究其中一些参数。
使用 Python 读取 JSON
在前面的部分中,你学习了如何将 Python 数据序列化为 JSON 格式的字符串和 JSON 文件。现在,你将看到当你将 JSON 数据加载回 Python 程序时会发生什么。
与 json.dumps() 和 json.dump() 并行,json 库提供了两个函数来将 JSON 数据反序列化为 Python 对象:
json.loads():用于反序列化字符串、字节或字节数组实例json.load():用于反序列化文本文件或二进制文件
作为经验法则,当你的数据已经在 Python 程序中时,你使用 json.loads()。当你使用保存在磁盘上的外部文件时,你使用 json.load()。
从 JSON 数据类型和值到 Python 的转换遵循与之前将 Python 对象转换为 JSON 格式时类似的映射:
| JSON | Python |
|---|---|
| object | dict |
| array | list |
| string | str |
| number | int |
| number | float |
| true | True |
| false | False |
| null | None |
当你将此表与前一节中的表进行比较时,你可能会发现 Python 为所有 JSON 类型都提供了匹配的数据类型。这非常方便,因为这样你就可以确保在将 JSON 数据反序列化为 Python 时不会丢失任何信息。
注意:反序列化并不是序列化过程的完全逆过程。原因是 JSON 键始终是字符串,并非所有 Python 数据类型都可以转换为 JSON 数据类型。这种差异意味着某些 Python 对象在序列化然后再反序列化后可能无法保留其原始类型。
为了更好地感受数据类型的转换,你将首先将 Python 对象序列化为 JSON,然后将 JSON 数据转换回 Python。这样,你就可以发现序列化的 Python 对象与反序列化 JSON 数据后最终得到的 Python 对象之间的差异。
将 JSON 对象转换为 Python 字典
为了研究如何从 JSON 对象加载 Python 字典,请回顾之前的示例。首先创建一个 dog_registry 字典,然后使用 json.dumps() 将 Python 字典序列化为 JSON 字符串:
>>> import json
>>> dog_registry = {1: {"name": "Frieda"}}
>>> dog_json = json.dumps(dog_registry)
>>> dog_json
'{"1": {"name": "Frieda"}}'
通过将 dog_registry 传递给 json.dumps(),你创建了一个包含 JSON 对象的字符串,并将其保存在 dog_json 中。如果你想将 dog_json 转换回 Python 字典,可以使用 json.loads():
>>> new_dog_registry = json.loads(dog_json)
通过使用 json.loads(),你可以将 JSON 数据转换回 Python 对象。根据你迄今为止获得的 JSON 知识,你可能已经怀疑 new_dog_registry 字典的内容与 dog_registry 的内容并不完全相同:
>>> new_dog_registry == dog_registry
False
>>> new_dog_registry
{'1': {'name': 'Frieda'}}
>>> dog_registry
{1: {'name': 'Frieda'}}
new_dog_registry 和 dog_registry 之间的差异很微妙,但在你的 Python 程序中可能会产生影响。在 JSON 中,键必须始终是字符串。当你使用 json.dumps() 将 dog_registry 转换为 dog_json 时,整数键 1 变成了字符串 "1"。当你使用 json.loads() 时,Python 无法知道该字符串键应该再次成为整数。这就是为什么你的字典键在反序列化后仍然是字符串的原因。
你将通过使用其他 Python 数据类型进行另一次转换往返来调查类似的行为!
反序列化 JSON 数据类型
为了探索不同数据类型在从 Python 到 JSON 再回到 Python 的往返过程中如何表现,请取之前部分中的 dog_data 字典的一部分。注意字典如何包含不同数据类型的值:
>>> dog_data = {
... "name": "Frieda",
... "is_dog": True,
... "hobbies": ["eating", "sleeping", "barking",],
... "age": 8,
... "address": {
... "work": None,
... "home": ("Berlin", "Germany",),
... },
... }
dog_data 字典包含了许多常见的 Python 数据类型作为值。例如,第 2 行的字符串、第 3 行的布尔值、第 7 行的 NoneType 和第 8 行的元组,仅举几例。
接下来,将 dog_data 转换为 JSON 格式的字符串,然后再转换回 Python。之后,查看新创建的字典:
>>> dog_data_json = json.dumps(dog_data)
>>> dog_data_json
'{"name": "Frieda", "is_dog": true, "hobbies": ["eating", "sleeping", "barking"],
"age": 8, "address": {"work": null, "home": ["Berlin", "Germany"]}}'
>>> new_dog_data = json.loads(dog_data_json)
>>> new_dog_data
{'name': 'Frieda', 'is_dog': True, 'hobbies': ['eating', 'sleeping', 'barking'],
'age': 8, 'address': {'work': None, 'home': ['Berlin', 'Germany']}}
你可以将每个 JSON 数据类型完美地转换为匹配的 Python 数据类型。JSON 布尔值 true 反序列化为 True,null 转换回 None,对象和数组变成字典和列表。不过,仍然有一个例外你可能会在往返过程中遇到:
>>> type(dog_data["address"]["home"])
<class 'tuple'>
>>> type(new_dog_data["address"]["home"])
<class 'list'>
当你序列化 Python 元组时,它会变成 JSON 数组。当你加载 JSON 时,JSON 数组正确地反序列化为列表,因为 Python 无法知道你希望该数组是元组。
当你进行数据往返时,可能会遇到这样的问题。当往返发生在同一个程序中时,你可能更清楚预期的数据类型。当你处理来自另一个程序的外部 JSON 文件时,数据类型转换可能会更加模糊。接下来你将调查这种情况!
使用 Python 打开外部 JSON 文件
在前面的部分中,你创建了一个 hello_frieda.py 文件,该文件保存了 hello_frieda.json 文件。如果你需要回顾一下,可以展开下面的可折叠部分查看代码:
[代码展示区域]
当你想要将内容写入 JSON 文件时,你使用 json.dump()。json.dump() 的对应函数是 json.load()。顾名思义,你可以使用 json.load() 将 JSON 文件加载到你的 Python 程序中。
跳回到 Python REPL 并加载之前的 hello_frieda.json JSON 文件:
>>> import json
>>> with open("hello_frieda.json", mode="r", encoding="utf-8") as read_file:
... frie_data = json.load(read_file)
...
>>> type(frie_data)
<class 'dict'>
>>> frie_data["name"]
'Frieda'
就像写入文件时一样,在 Python 中读取文件时使用上下文管理器是个好主意。这样,你就不必担心再次关闭文件。当你想要读取 JSON 文件时,在 with 语句块内使用 json.load()。
load() 函数的参数必须是文本文件或二进制文件。从 json.load() 获得的 Python 对象取决于 JSON 文件的顶层数据类型。在这种情况下,JSON 文件在顶层包含一个对象,该对象反序列化为字典。
当你将 JSON 文件反序列化为 Python 对象时,你可以原生地与它交互——例如,通过方括号表示法 ([]) 访问 "name" 键的值。不过,这里有一个警告。导入之前的原始 dog_data 字典并将其与 frie_data 进行比较:
>>> from hello_frieda import dog_data
>>> frie_data == dog_data
False
>>> type(frie_data["address"]["home"])
<class 'list'>
>>> type(dog_data["address"]["home"])
<class 'tuple'>
当你将 JSON 文件作为 Python 对象加载时,任何 JSON 数据类型都会愉快地反序列化为 Python。这是因为 Python 了解 JSON 格式支持的所有数据类型。不幸的是,反过来则不是这样。
如前所述,有一些 Python 数据类型(如元组)可以转换为 JSON,但你会在 JSON 文件中得到数组数据类型。一旦你将 JSON 数据转换回 Python,数组就会反序列化为 Python 列表数据类型。
一般来说,对数据类型转换保持谨慎应该是编写 JSON 的 Python 程序的关注点。凭借你对 JSON 文件的了解,只要 JSON 文件有效,你总是可以预测最终会得到哪些 Python 数据类型。
如果你使用 json.load(),那么你加载的文件内容必须包含有效的 JSON 语法。否则,你会收到 JSONDecodeError。幸运的是,Python 为你提供了更多工具来与 JSON 交互。例如,它允许你从终端的便利性检查 JSON 文件的有效性。
与 JSON 交互
到目前为止,你已经探索了 JSON 语法,并且已经发现了一些常见的 JSON 陷阱,比如尾随逗号和字符串使用单引号。在编写 JSON 时,你可能还发现了一些烦人的细节。例如,整齐缩进的 Python 字典最终变成了 JSON 数据块。
在本教程的最后一部分,你将尝试一些技术,让你在 Python 中处理 JSON 数据时生活更轻松。首先,你将给你的 JSON 对象一个应得的美化升级。
使用 Python 美化 JSON
JSON 格式的一大优势是 JSON 数据是人类可读的。更重要的是,JSON 数据是人类可写的。这意味着你可以在你喜欢的文本编辑器中打开 JSON 文件并根据自己的喜好更改内容。至少,这是想法!
当你的 JSON 数据在文本编辑器中看起来像这样时,手动编辑 JSON 数据并不是特别容易:
![没有缩进的 JSON 代码]
即使启用了自动换行和语法高亮,当 JSON 数据是一行代码时也很难阅读。而且作为一名 Python 开发者,你可能还会想念一些空白字符。但不用担心,Python 为你提供了支持!
当你调用 json.dumps() 或 json.dump() 来序列化 Python 对象时,你可以提供 indent 参数。首先尝试使用不同的缩进级别调用 json.dumps():
>>> import json
>>> dog_friend = {
... "name": "Mitch",
... "age": 6.5,
... }
>>> print(json.dumps(dog_friend))
{"name": "Mitch", "age": 6.5}
>>> print(json.dumps(dog_friend, indent=0))
{
"name": "Mitch",
"age": 6.5
}
>>> print(json.dumps(dog_friend, indent=-2))
{
"name": "Mitch",
"age": 6.5
}
>>> print(json.dumps(dog_friend, indent=""))
{
"name": "Mitch",
"age": 6.5
}
>>> print(json.dumps(dog_friend, indent=" ⮑ "))
{
⮑ "name": "Mitch",
⮑ "age": 6.5
}
indent 的默认值是 None。当你调用 json.dumps() 时不使用 indent 或者值为 None 时,你会得到一行紧凑的 JSON 格式字符串。
如果你想要在 JSON 字符串中换行,可以将 indent 设置为 0 或提供一个空字符串。虽然可能不太有用,但你甚至可以提供负数作为缩进或任何其他字符串。
更常见的是,你会为 indent 提供 2 或 4 这样的值:
>>> print(json.dumps(dog_friend, indent=2))
{
"name": "Mitch",
"age": 6.5
}
>>> print(json.dumps(dog_friend, indent=4))
{
"name": "Mitch",
"age": 6.5
}
当你在调用 json.dumps() 时使用正整数作为 indent 的值时,你会用给定的缩进计数作为空格来缩进 JSON 对象的每一级。同时,你会为每个键值对添加换行符。
注意:要在 REPL 中实际看到空白字符,你可以将
json.dumps()调用包装在print()函数调用中。
indent 参数对 json.dump() 的作用与对 json.dumps() 完全相同。继续将 dog_friend 字典以 4 个空格的缩进写入 JSON 文件:
>>> with open("dog_friend.json", mode="w", encoding="utf-8") as write_file:
... json.dump(dog_friend, write_file, indent=4)
...
当你在序列化 JSON 数据时设置缩进级别时,你会得到美化的 JSON 数据。看看你的编辑器中 dog_friend.json 文件的样子:
![格式化的 JSON 代码]
Python 可以处理任何缩进方式的 JSON 文件。作为人类,你可能更喜欢包含换行符并整齐缩进的 JSON 文件。这样的 JSON 文件编辑起来要方便得多。
在终端中验证 JSON
能够编辑编辑器中的 JSON 数据的便利性伴随着风险。当你移动键值对或添加只有一个引号而不是两个引号的字符串时,最终会得到无效的 JSON。
要快速检查 JSON 文件是否有效,你可以利用 Python 的 json.tool。你可以使用 -m 开关在终端中将 json.tool 模块作为可执行文件运行。要查看 json.tool 的实际效果,还可以提供 dog_friend.json 作为 infile 位置参数:
$ python -m json.tool dog_friend.json
{
"name": "Mitch",
"age": 6.5
}
当你只使用 infile 选项运行 json.tool 时,Python 会验证 JSON 文件,如果 JSON 有效,则在终端中输出 JSON 文件的内容。上面的例子运行意味着 dog_friend.json 包含有效的 JSON 语法。
注意:
json.tool默认以 4 个空格的缩进打印 JSON 数据。你将在下一节中探索此行为。
要让 json.tool 报错,你需要使你的 JSON 文档无效。你可以通过删除键值对之间的逗号 (,) 来使 dog_friend.json 的 JSON 数据无效:
dog_friend.json
{
"name": "Mitch"
"age": 6.5
}
保存 dog_friend.json 后,再次运行 json.tool 来验证文件:
$ python -m json.tool dog_friend.json
Expecting ',' delimiter: line 3 column 5 (char 26)
json.tool 模块成功地发现了 dog_friend.json 中缺少的逗号。Python 注意到一旦第 3 行位置 5 开始出现用双引号包裹的 "age" 属性名时,就缺少了分隔符。
继续尝试再次修复 JSON 文件。你也可以创造性地使 dog_friend.json 无效,并检查 json.tool 如何报告你的错误。但请记住,json.tool 只报告第一个错误。因此,你可能需要在修复 JSON 文件和运行 json.tool 之间来回切换。
一旦 dog_friend.json 有效,你可能会注意到输出总是看起来一样。当然,像任何制作精良的命令行界面一样,json.tool 为你提供了一些选项来控制程序。
在终端中漂亮地打印 JSON
在上一节中,你使用 json.tool 验证了 JSON 文件。当 JSON 语法有效时,json.tool 以换行符和四个空格的缩进显示内容。要控制 json.tool 如何打印 JSON,你可以设置 --indent 选项。
如果你一直跟着教程进行,那么你有一个 hello_frieda.json 文件,该文件不包含换行符或缩进。或者,你可以点击下面的链接下载 hello_frieda.json:
当你将 hello_frieda.json 传递给 json.tool 时,你可以在终端中漂亮地打印 JSON 文件的内容。当你设置 --indent 时,你可以控制 json.tool 用于显示代码的缩进级别:
$ python -m json.tool hello_frieda.json --indent 2
{
"name": "Frieda",
"is_dog": true,
"hobbies": [
"eating",
"sleeping",
"barking"
],
"age": 8,
"address": {
"work": null,
"home": [
"Berlin",
"Germany"
]
},
"friends": [
{
"name": "Philipp",
"hobbies": [
"eating",
"sleeping",
"reading"
]
},
{
"name": "Mitch",
"hobbies": [
"running",
"snacking"
]
}
]
}
在终端中看到美化的 JSON 数据很酷。但你甚至可以通过向 json.tool 运行提供另一个选项来进一步提升你的技能!
默认情况下,json.tool 将输出写入 sys.stdout,就像你调用 print() 函数时通常做的那样。但你也可以通过提供位置参数 outfile 将 json.tool 的输出重定向到文件中:
$ python -m json.tool hello_frieda.json pretty_frieda.json
通过将 pretty_frieda.json 作为 outfile 选项的值,你将输出写入 JSON 文件而不是在终端中显示内容。如果文件尚不存在,Python 会在过程中创建该文件。如果目标文件已存在,则会用新内容覆盖该文件。
注意:你可以通过使用相同的文件作为
infile和outfile参数来就地美化 JSON 文件。
你可以通过运行 ls 终端命令来验证 pretty_frieda.json 文件是否存在:
$ ls -al
drwxr-xr-x@ 8 realpython staff 256 Jul 3 19:53 .
drwxr-xr-x@ 12 realpython staff 384 Jul 3 18:29 ..
-rw-r--r--@ 1 realpython staff 44 Jul 3 19:25 dog_friend.json
-rw-r--r--@ 1 realpython staff 286 Jul 3 17:27 hello_frieda.json
-rw-r--r--@ 1 realpython staff 484 Jul 3 16:53 hello_frieda.py
-rw-r--r--@ 1 realpython staff 34 Jul 2 19:38 hello_world.json
-rw-r--r--@ 1 realpython staff 594 Jul 3 19:45 pretty_frieda.json
你添加到 pretty_frieda.json 的空白字符是有代价的。与原始的、未缩进的 hello_frieda.json 文件相比,pretty_frieda.json 文件的大小现在大约是原来的两倍。这里的 308 字节增加可能并不显著。但当你处理大型 JSON 数据时,好看的 JSON 文件会占用相当多的空间。
当通过网络提供数据时,拥有较小的数据占用空间尤其有用。由于 JSON 格式是网络数据交换的事实标准,因此值得尽可能减小文件大小。再次,Python 的 json.tool 为你提供了支持!
使用 Python 压缩 JSON
正如你现在所知,Python 在处理 JSON 时是一个很好的帮手。你可以通过两种方式使用 Python 压缩 JSON 数据:
- 在终端中利用 Python 的
json.tool模块 - 在你的 Python 代码中使用
json模块
之前,你使用 json.tool 与 --indent 选项来添加空白字符。在这里,你可以提供 --compact 来做相反的事情,删除 JSON 中键值对之间的任何空白字符:
$ python -m json.tool pretty_frieda.json mini_frieda.json --compact
调用 json.tool 模块后,你提供一个 JSON 文件作为 infile,另一个 JSON 文件作为 outfile。如果目标 JSON 文件存在,则会覆盖其内容。否则,你会创建一个具有你提供的文件名的新文件。
就像使用 --indent 一样,你可以提供相同的文件作为源文件和目标文件来就地压缩文件。在上面的例子中,你将 pretty_frieda.json 压缩为 mini_frieda.json。运行 ls 命令看看你从原始 JSON 文件中挤出了多少字节:
$ ls -al
drwxr-xr-x@ 9 realpython staff 288 Jul 3 20:12 .
drwxr-xr-x@ 12 realpython staff 384 Jul 3 18:29 ..
-rw-r--r--@ 1 realpython staff 44 Jul 3 19:25 dog_friend.json
-rw-r--r--@ 1 realpython staff 286 Jul 3 17:27 hello_frieda.json
-rw-r--r--@ 1 realpython staff 484 Jul 3 16:53 hello_frieda.py
-rw-r--r--@ 1 realpython staff 34 Jul 2 19:38 hello_world.json
-rw-r--r--@ 1 realpython staff 257 Jul 3 20:12 mini_frieda.json
-rw-r--r--@ 1 realpython staff 594 Jul 3 19:45 pretty_frieda.json
与 pretty_frieda.json 相比,mini_frieda.json 的文件大小减少了 337 字节。这甚至比原始的 hello_frieda.json 文件(不包含任何缩进)还要少 29 字节。
为了调查 Python 甚至从原始 JSON 中删除了更多空白字符的位置,请再次打开 Python REPL 并使用 Python 的 json 模块压缩原始 hello_frieda.json 文件的内容:
>>> import json
>>> with open("hello_frieda.json", mode="r", encoding="utf-8") as input_file:
... original_json = input_file.read()
...
>>> json_data = json.loads(original_json)
>>> mini_json = json.dumps(json_data, indent=None, separators=(",", ":"))
>>> with open("mini_frieda.json", mode="w", encoding="utf-8") as output_file:
... output_file.write(mini_json)
...
在上面的代码中,你使用 Python 的 .read() 获取 hello_frieda.json 的内容作为文本。然后,你使用 json.loads() 将 original_json 反序列化为 json_data,这是一个 Python 字典。你可以使用 json.load() 直接获得 Python 字典,但你需要先将 JSON 数据作为字符串获取才能正确比较。
这就是为什么你使用 json.dumps() 创建 mini_json,然后使用 .write() 而不是直接利用 json.dump() 将压缩后的 JSON 数据保存在 mini_frieda.json 中。
如前所述,json.dumps 需要 JSON 数据作为第一个参数,然后接受缩进值。indent 的默认值是 None,所以你可以像上面那样跳过显式设置参数。但是通过 indent=None,你清楚地表明你不想要任何缩进,这对以后阅读你代码的其他人来说是个好事。
json.dumps() 的 separators 参数允许你定义一个包含两个值的元组:
- 键值对或列表项之间的分隔符。默认情况下,此分隔符是逗号后跟空格 (
", ")。 - 键和值之间的分隔符。默认情况下,此分隔符是冒号后跟空格 (
": ")。
通过将 separators 设置为 (",", ":"),你继续使用有效的 JSON 分隔符。但你告诉 Python 不要在逗号 (",") 和冒号 (":") 后面添加任何空格。这意味着你的 JSON 数据中剩下的唯一空白字符可能是键名和值中出现的空白字符。这相当紧凑!
有了 original_json 和 mini_json 都包含你的 JSON 字符串,是时候比较它们了:
>>> original_json
'{"name": "Frieda", "is_dog": true, "hobbies": ["eating", "sleeping", "barking"],
"age": 8, "address": {"work": null, "home": ["Berlin", "Germany"]},
"friends": [{"name": "Philipp", "hobbies": ["eating", "sleeping", "reading"]},
{"name": "Mitch", "hobbies": ["running", "snacking"]}]}'
>>> mini_json
'{"name":"Frieda","is_dog":true,"hobbies":["eating","sleeping","barking"],
"age":8,"address":{"work":null,"home":["Berlin","Germany"]},
"friends":[{"name":"Philipp","hobbies":["eating","sleeping","reading"]},
{"name":"Mitch","hobbies":["running","snacking"]}]}'
>>> len(original_json)
284
>>> len(mini_json)
256
当你查看输出时,你已经可以发现 original_json 和 mini_json 之间的差异。然后,你使用 len() 函数验证 mini_json 的大小确实更小。如果你好奇为什么 JSON 字符串的长度几乎完全匹配写入文件的文件大小,那么深入了解 Python 中的 Unicode 和字符编码是个好主意。
无论是 json 还是 json.tool,都是在你想让 JSON 数据看起来更漂亮,或者想压缩 JSON 数据以节省一些字节时的优秀助手。使用 json 模块,你可以方便地在 Python 程序中与 JSON 数据交互。当你需要更多控制权来与 JSON 交互时,这很棒。当你想直接在终端中处理 JSON 数据时,json.tool 模块非常方便。
结论
无论你是想通过 API 传输数据还是在文档数据库中存储信息,你都可能会遇到 JSON。Python 提供了强大的工具来促进这一过程,并帮助你高效地管理 JSON 数据。在 Python 和 JSON 之间进行数据往返时需要小心一些,因为它们并不共享相同的数据类型集。尽管如此,JSON 格式仍然是保存和交换数据的好方法。
在本教程中,你学习了如何:
- 理解 JSON 语法
- 将 Python 数据转换为 JSON
- 将 JSON 反序列化为 Python
- 编写和读取 JSON 文件
- 验证 JSON 语法
此外,你还学习了如何在终端中美化 JSON 数据以及如何压缩 JSON 数据以减小文件大小。现在,你有足够的知识开始在项目中使用 JSON 了。如果你想重温本教程中编写的代码或测试你对 JSON 的了解,请点击链接下载材料或参加下面的测验。玩得开心!