如何使用 Python json 模块解析和转储 JSON?Python json 模块介绍
前提
阅读本节的前提是已经对 JSON 有所了解,你可以查看编程教程的JSON,JSON 字符串,JSON 数据类型介绍一节来获取相关信息。
Python json 模块
Python 的json
模块包含了一系列与 JSON 相关的函数和类,开发人员可以通过该模块将 JSON 字符串解析(解码)为一个 Python 对象,或将 Python 对象转储(编码)为 JSON 字符串,或者自定义 JSON 的解析过程。
在解析和转储的过程中,JSON 中的数字将对应 Python 中的int
和float
类型,JSON 中的true
和false
将对应 Python 中的True
和False
,JSON 中的null
将对应 Python 中的空值None
,JSON 中使用双引号表示的字符串将对应 Python 中的str
类型,JSON 中使用方括号表示的数组将对应 Python 中的list
类型,JSON 中使用花括号表示的对象将对应 Python 中的dict
类型。
Python json 模块允许 JSON 字符串出现非标准内容
在标准的 JSON 中,允许书写null
,true
,false
,比如{"success":false}
,但不允许书写NaN
,Infinity
和-Infinity
,比如,{"age":NaN}
将被视为一个错误。
不过,Python 对 JSON 字符串的要求更为宽松,解析字符串'{"age":NaN,"size":Infinity}'
或转储对象[float('nan'),float('-inf')]
是可行的,NaN
,Infinity
或-Infinity
将被转换为表示非数字,正无穷和负无穷的 Python 浮点数,他们在 Python 中可显示为nan
,inf
或-inf
。
使用 Python json 模块将 JSON 字符串解析为 Python 对象
要将 JSON 格式的字符串解析为 Python 对象,你可以使用 Python 的json
模块的load
或loads
函数,这两个函数拥有几乎相同的参数。不同的是,loads
函数直接接受 JSON 字符串,load
函数接受间接提供 JSON 字符串的 Python 对象,比如io.StringIO
对象。
load(fp, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kwds)
loads(s, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kwds)
- fp 参数
fp
参数是一个具有read
方法的 Python 对象,该方法将返回需要解析的 JSON 字符串。- s 参数
s
参数是一个 JSON 字符串(str
,bytes
或bytearray
对象)。- cls,object_hook,parse_float,parse_int,parse_constant,object_pairs_hook,kwds 参数
参数
cls
,object_hook
,parse_float
,parse_int
,parse_constant
,object_pairs_hook
,kwds
主要用于自定义 JSON 字符串的解析过程,我们会在后面详细介绍。
对于 JSON 字符串中键相同的键值对,Python json 模块仅接受最后一个
在 JSON 字符串中,如果使用{}
表示的同一个对象存在键相同的键值对,那么最后出现的键值对被视为有效的,比如,使用load
和loads
函数解析'{"name":"Jack","name":"Tom","other":{"name":"Harry","name":"Jerry"}}'
的结果为 Python 对象{'name':'Tom','other':{'name':'Jerry'}}
。
在下面的示例中,我们分别使用load
和loads
函数解析了由参数fp
和s
提供的 JSON 字符串。其中第二个height
是有效的,因此最终结果显示300
而不是200
。
import json
from io import StringIO
# 使用 load 函数解析存储在 StringIO 对象中的 JSON 字符串
s = StringIO('{"name": "Jack", "age": 10}')
print(json.load(s))
# 分别通过 str,bytes 对象提供 JSON 字符串
print(json.loads('{"id": "red", "color": "#ff0000"}'))
# 最后一个 height 才是有效的
print(json.loads(b'{"width": 100, "height": 200, "height": 300}'))
{'name': 'Jack', 'age': 10}
{'id': 'red', 'color': '#ff0000'}
{'width': 100, 'height': 300}
使用 Python json 模块自定义 JSON 字符串的解析过程
在默认情况下,Python 的json
模块通过解码器JSONDecoder
来完成 JSON 字符串的解析,load
或loads
函数会使用相关参数创建JSONDecoder
解码器对象,并由该对象负责将 JSON 字符串转换为一个 Python 对象。如果 JSON 字符串是无效的,那么load
或loads
函数可能会抛出异常JSONDecodeError
。
以下是 Pythonjson
模块的JSONDecoder
类的构造器,其大部分参数的名称和作用与load
或loads
函数相同,使用这些参数可以完成 JSON 解析过程的自定义。
JSONDecoder(*, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True, object_pairs_hook=None)
- object_hook 参数
object_hook
参数是一个 Python 函数(或方法),其参数为从 JSON 字符串中解析得到的 Python 字典(dict
)对象,你可以在函数中对该字典对象进行调整或生成一个自己的字典对象,函数的返回值将用于替换之前解析得到的字典对象(没有返回值相当于返回空值None
)。如果可以从 JSON 字符串中得到多个 Python 字典对象,那么
object_hook
参数对应的函数将被调用多次。- parse_float 参数
parse_float
参数是一个 Python 函数(或方法),其参数是一个 Python 字符串,包含了 JSON 字符串中表示浮点数的文本,函数的返回值将作为上述文本的最终解析结果。如果可以从 JSON 字符串中得到多个 Python 浮点数,那么
parse_float
参数对应的函数将被调用多次。- parse_int 参数
parse_int
参数的作用与parse_float
参数类似,只不过,parse_int
对应的函数针对 JSON 字符串中表示整数的文本。- parse_constant 参数
parse_constant
参数的作用与parse_float
参数类似,只不过,parse_constant
对应的函数针对 JSON 字符串中出现的NaN
,Infinity
和-Infinity
(不针对表示字符串的"NaN"
,"Infinity"
和"-Infinity"
)。- object_pairs_hook 参数
object_pairs_hook
参数与object_hook
参数类似,同样是一个 Python 函数(或方法),不同的是,object_pairs_hook
对应的函数的参数是一个列表,该列表包含了从 JSON 字符串中得到的 Python 字典对象的键值对,每一个键值对对应一个格式为(key,value)
的 Python 元组,其中key
为键值对的键,value
为键值对的值。如果同时指定了参数object_pairs_hook
和object_hook
,则object_hook
将被忽略。- strict 参数
strict
参数用于说明是否不允许在 JSON 字符串中使用控制字符,当该参数被设置为True
且 JSON 字符串包含控制字符时,将引发异常json.decoder.JSONDecodeError: Invalid control character at: …
。
Python 3.1 之前,parse_constant
参数对应的函数还针对 JSON 字符串中出现的null
,true
和false
。
什么控制字符?
上面提到的控制字符,是指编码在0
到31
以内的字符,这包括常见的\t
,\r
和\n
等。
在下面的示例中,我们使用loads
函数解析 JSON 字符串,转换后的 Python 对象将被函数hook
处理,该函数会检查分数中的所有负数。
import json
# 函数 hook,检查分数是否小于 0,如果是,则将其转换为正数
def hook(result):
# 当字典包含 id 时,说明这是分数信息
if 'id' in result and result['value'] < 0:
result['value'] = -result['value']
# 这里一定要返回一个对象,否则最终的解析结果将出现 None
return result
# 使用函数 hook 来处理转换的 Python 对象
o = json.loads(
'{"name": "Jack", "scores": [{"id": "A", "value": -100}, {"id": "B", "value": 200}]}',
object_hook=hook
)
print(f'object_hook {o}')
object_hook {'name': 'Jack', 'scores': [{'id': 'A', 'value': 100}, {'id': 'B', 'value': 200}]}
接下来,我们定义了计算分数之和的函数pairs_hook
,并将其传递给参数object_pairs_hook
。参数pairs
是一个 Python 列表,其包含的元素会被修改,以生成并返回一个新的dict
对象。
# …
# 函数 pairs_hook,计算所有分数之和
sum = 0
def pairs_hook(pairs):
global sum
# 检查所有键值对,计算所有的分数
for i, pair in enumerate(pairs):
key, value = pair
# 键为 value,则累计分数,键为 scores 则写入分数
if key == 'value':
sum += value
elif key == 'scores':
pair = ('scores', sum)
pairs[i] = pair
# 这里使用键值对生成新的字典对象
return dict(pairs)
# 使用函数 pairs_hook 来处理转换的键值对
o = json.loads(
'{"name": "Jack", "scores": [{"id": "A", "value": 100}, {"id": "B", "value": 200}]}',
object_pairs_hook=pairs_hook
)
print(f'object_pairs_hook {o}')
object_pairs_hook {'name': 'Jack', 'scores': 300}
下面的代码中,我们定义了一些函数,用于处理 JSON 中表示数字的文本,其中函数convert_int
可以将 JSON 中的数字转换为 Python 中的字符串,函数convert_constant
可以将 JSON 中与数字相关的NaN
,Infinity
或-Infinity
转换为 Python 空值None
。当然,由于'-Infinity'
表示的是字符串,因此他不会被转换为空值None
。
import json
# 一些处理 JSON 数字的函数
def convert_float(value):
# 将浮点数四舍五入
return round(float(value))
def convert_int(value):
# 将整数转换为 Yes 或 No
i = int(value)
return 'No' if i < 0 else 'Yes'
def convert_constant(value):
# 将 NaN,Infinity,-Infinity 转换为 None
return None
# NaN,Infinity 将被转换为 None
o = json.loads(
'[1.2, 1.5, -1, 1, NaN, Infinity, "-Infinity", null, true, false]',
parse_float=convert_float,
parse_int=convert_int,
parse_constant=convert_constant
)
print(f'parse {o}')
parse [1, 2, 'No', 'Yes', None, None, '-Infinity', None, True, False]
在默认情况下,json
模块可以将 JSON 字符串中的NaN
,Infinity
或-Infinity
转换为 Python 浮点数。
# …
# NaN,Infinity,-Infinity 会转换为 float 类型
o = json.loads('[NaN, Infinity, -Infinity]')
for n in o:
print(f'{type(n)} {n}')
<class 'float'> nan
<class 'float'> inf
<class 'float'> -inf
自定义 Python json 模块的 JSON 解码器
除了使用 Pythonjson
模块的load
和loads
函数的参数来自定义 JSON 字符串的解析过程,通过继承json
模块的JSONDecoder
类,可以实现相同的效果。在编写JSONDecoder
类的派生类之后,你需要将派生类赋值给load
或loads
函数的cls
参数。
- cls 参数
load
和loads
函数的cls
参数应该是继承自JSONDecoder
的派生类,如果忽略该参数或设置为None
,则load
和loads
函数将使用JSONDecoder
类来完成 JSON 字符串的解析任务。- kwds 参数
load
和loads
函数的kwds
参数,可用于为cls
表示的派生类的构造器指定参数。
修改 Python json 模块的 JSONDecoder 对象的变量可能不会产生效果
Pythonjson
模块的JSONDecoder
对象拥有一些与其构造器参数同名的变量,比如parse_int
,修改这些变量可能不会达到自定义 JSON 解析方式的效果,因为自定义已经在构造器中完成。
下面的示例展示了 JSON 解码器MyDecoder
,在其构造器中,我们指定了函数和方法用于处理 JSON 字符串中表示数字的文本,最后,在调用loads
函数解析字符串时,我们将cls
参数赋值为类MyDecoder
,并添加了一个名称为strict
的关键字(命名)参数,该参数在创建MyDecoder
对象时被使用。
由于,loads
函数的strict
参数为False
,因此,解码器MyDecoder
将允许 JSON 字符串出现控制关键字,比如,代码中的\t
。
import json
import math
# 一个继承自 JSONDecoder 的类
class MyDecoder(json.JSONDecoder):
def __init__(self, strict):
super().__init__(
# 指定处理 JSON 数字的函数或方法
parse_float=lambda v: math.ceil(float(v)),
parse_int=MyDecoder.convert_int,
strict=strict
)
# 直接修改变量 parse_int 不会产生任何效果
self.parse_int = lambda v: int(v) + 1000
# 用于处理整数的方法 convert_int
@staticmethod
def convert_int(value):
i = int(value)
return 0 if i < 0 else i
# 使用类 MyDecoder 完成 JSON 字符串的解析
o = json.loads(
'[-100, 1.2, "\t一个制表符"]',
cls=MyDecoder,
# 参数 strict 将在创建 MyDecoder 对象时被使用
strict=False
)
print(o)
[0, 2, '\t一个制表符']
使用 Python json 模块解析混合在文本中的 JSON 字符串
如果需要解析的 JSON 字符串被混合在某些文本中,比如'+++[1,2,3]---'
,那么你可以使用 Pythonjson
模块的JSONDecoder
对象的decode
或raw_decode
方法来提取并解析 JSON 字符串。
JSONDecoder
对象的decode
方法的返回值是解析 JSON 字符串后得到的 Python 对象。JSONDecoder
对象的raw_decode
方法的返回值是一个格式为(object,index)
的元组,其中object
为解析 JSON 字符串后得到的 Python 对象,index
为 JSON 字符串在文本中结束的位置(1
对应文本中第一个字符的位置)。
decode(s, _w)
raw_decode(s, idx)
- s 参数
s
参数是一个包含了 JSON 的 Python 字符串。- _w 参数
_w
参数可以是一个已编译正则表达式对象re.Pattern
的match
方法,用于确定 JSON 字符串的开始和结束位置,默认值为WHITESPACE.match
,其中WHITESPACE
是定义在json
模块中的re.Pattern
对象。- idx 参数
idx
参数用于确定 JSON 字符串的开始位置,0
表示从第一个字符开始。
在调用方法decode
时,我们将 3 个连续的+
或-
作为 JSON 的边界。在调用方法raw_decode
时,我们指定第 4 个字符{
为 JSON 的开始,返回元组中的19
表示 JSON 结束于第 19 个字符}
。
from json import JSONDecoder
import re
d = JSONDecoder()
# 将 +++ 或 --- 作为 JSON 的边界
print(d.decode('+++[1, 2, 3, "A", "B", "C"]---', re.compile(r'\+{3}|-{3}').match))
# 将第 4 个字符 { 作为 JSON 的开始
print(d.raw_decode('开始:{"name": "Jack"},这里有个 JSON', 3))
[1, 2, 3, 'A', 'B', 'C']
({'name': 'Jack'}, 19)
使用 Python json 模块将 Python 对象格式化并转储为 JSON 字符串
如果需要将一个 Python 对象格式化并转储(编码)为 JSON 字符串,这通常是为了将数据存储至本地或更为轻松的交换数据,那么你可以使用 Pythonjson
模块的dump
或dumps
函数,他们的参数几乎相同。dump
函数会将转换得到的 JSON 字符串写入参数fp
所表示的 Python 类文件对象(事实上是一个拥有write
方法并支持写入str
的对象),而dumps
函数会直接返回转换得到的 JSON 字符串。
dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kwds)
dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kwds)
- obj 参数
obj
参数是需要转储为 JSON 字符串的 Python 对象。- fp 参数
fp
参数是一个具有write
方法的 Python 对象,该方法需要支持写入字符串(str
)。- skipkeys 参数
skipkeys
参数用于指示是否忽略 Python 对象中的键值对,如果键值对的键不是None
或以下基础类型之一,str
,int
,float
,bool
,被忽略的键值对将不会出现在 JSON 字符串中。skipkeys
参数的默认值为False
,表示不会忽略键值对,并将抛出异常TypeError: keys must be str, int, float, bool or None, not …
。- ensure_ascii 参数
ensure_ascii
参数用于指示是否转义所有非 ASCII 字符,默认为True
(转义所有非 ASCII 字符)。- check_circular 参数
check_circular
参数用于指示是否检测 Python 对象中的循环引用,默认为True
(检测循环引用)。如果设置为False
并且 Python 对象中存在循环引用,则异常RecursionError
或更严重的问题将被引发。- allow_nan 参数
allow_nan
参数用于指示是否将 Python 浮点数中的非数字,正无穷和负无穷,转储为 JSON 字符串中的NaN
,Infinity
和-Infinity
。如果allow_nan
被设置为False
(默认为True
),那么 Python 对象中表示非数字,正无穷和负无穷的浮点数将导致异常ValueError: Out of range float values are not JSON compliant: …
。- indent 参数
indent
参数用于设置 JSON 字符串的缩进方式,如果indent
是一个正整数,那么将使用指定数量的空格进行缩进,如果indent
是一个非空字符串(比如'\t'
),那么将使用该字符串进行缩进。如果indent
是非正整数或空字符串,那么 JSON 字符串不会进行缩进,仅会保留换行符。如果indent
为默认值None
,那么 JSON 字符串不会进行缩进也不会换行。- separators 参数
separators
参数是一个格式为(is,ks)
的元组,其中is
是用于分隔键值对或元素的字符串,ks
是用于分隔键值对的键与值的字符串。该参数为默认值None
时,他将等效于(', ', ': ')
(indent
参数为None
时)或(',', ': ')
(indent
参数不为None
时)。- default 参数
default
参数是一个 Python 函数,如果 Python 对象中包含不能编码(转储)的对象,那么这个不能编码的对象将被传递给该函数。你应该通过该函数返回一个可以编码的 Python 对象,或引发异常TypeError
。当然,如果default
参数为默认值None
,那么异常TypeError
同样会在遇到不能编码的对象时被抛出。- sort_keys 参数
sort_keys
参数用于指示是否在转储时按照键对键值对进行排序,默认为False
(不排序)。如果设置为True
并且存在无法进行排序的键值对,则将引发异常。- cls,kwds 参数
参数
cls
,kwds
主要用于自定义的 JSON 编码器,我们会在后面详细介绍。
在下面的示例中,由于dump
函数的skipkeys
为True
,因此键值对date(2024,7,17):True
将被忽略。第一个dumps
函数设置了separators
参数,其转储的 JSON 字符串为非标准的,他可能无法被正常解析。第二个dumps
函数将allow_nan
参数设置为了False
,因此表示负无穷的浮点数导致了异常。
import json
from io import StringIO
from datetime import date
# 使用 StringIO 对象存储 JSON 字符串
s = StringIO()
json.dump(
{'name': '你好,Jack', 'age': 10, date(2024, 7, 17): True}, s,
# 第三个键值对将被忽略
skipkeys=True,
# 不转义非 ASCII 字符
ensure_ascii=False
)
print(s.getvalue())
# dumps 函数直接返回 JSON 字符串
s = json.dumps(
[
{'name': 'Tom', 'age': 10},
{'name': 'Harry', 'age': 11}
],
# 使用一个空格进行缩进
indent=1,
# 使用 ; 分隔键值对或元素,使用 = 分隔键和值
separators=(';', '='),
# 对键值对进行排序
sort_keys=True
)
print(s)
# ERROR 表示负无穷的浮点数无法被转储
json.dumps(float('-inf'), allow_nan=False)
{"name": "你好,Jack", "age": 10}
[
{
"age"=10;
"name"="Tom"
};
{
"age"=11;
"name"="Harry"
}
]
…
ValueError: Out of range float values are not JSON compliant: -inf
在下面的示例中,我们创建了一个循环引用的字典对象,并通过check_circular
参数控制是否检测循环引用,不过,无论是否检测都会引发异常。
import json
# 循环引用字典对象
o = dict()
o['self'] = o
try:
# 检测对象是否存在循环引用
json.dumps(o, check_circular=True)
except Exception as err:
print(err)
try:
# 不检测对象是否存在循环引用,但依然会导致异常
json.dumps(o, check_circular=False)
except Exception as err:
print(err)
Circular reference detected
maximum recursion depth exceeded while encoding a JSON object
自定义 Python json 模块的 JSON 编码器
默认情况下,Pythonjson
模块通过编码器JSONEncoder
将 Python 对象转储为 JSON 字符串,dump
或dumps
函数会使用相关参数创建JSONEncoder
编码器对象,并由该对象的iterencode
或encode
方法负责将对象编码(转储)为一个 JSON 字符串。其中,encode
方法返回编码得到的 JSON 字符串,iterencode
方法返回一个包含了被分割的 JSON 字符串的迭代器对象。
以下是 Pythonjson
模块的JSONEncoder
类的构造器以及方法iterencode
和encode
,其中构造器的参数的名称和作用与dump
或dumps
函数相同。
JSONEncoder(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)
iterencode(o)
encode(o)
- o 参数
o
参数为需要转储为 JSON 字符串的 Python 对象。
在大部分情况下,json
模块的dump
或dumps
函数可以完成 JSON 的编码任务,并不需要主动创建JSONEncoder
对象,然后调用其iterencode
或encode
方法。当然,创建JSONEncoder
或其派生类的实例也是可行的,你可以重写JSONEncoder
的default
方法,该方法的作用与之前讲述的dump
或dumps
函数的default
参数的作用相同,可用于将不能编码的 Python 对象转换为可编码的对象。
default(o)
- o 参数
o
参数为不能进行编码的 Python 对象。
自定义的 JSON 编码器的构造器最好保留 JSONEncoder 类的构造器的关键字参数
如果你定义了自己的 JSON 编码器(即json
模块的JSONEncoder
类的派生类),那么编码器的构造器最好保留JSONEncoder
的构造器的关键字参数,最简单的做法是在末尾定义以**
为前缀的可变关键字。否则,将自定义的 JSON 编码器传递给dump
或dumps
函数将导致错误。
在下面的示例中,我们定义了自己的 JSON 编码器MyEncoder
,并重写了方法default
来处理无法编码的date
对象,变量date_template
用于格式化日期。构造器末尾以**
为前缀的参数kwds
可以让MyEncoder
轻松接受dump
或dumps
函数的关键字参数。
import json
from datetime import date
# 自定义 JSON 编码器 MyEncoder
class MyEncoder(json.JSONEncoder):
# 构造器需要保留 JSONEncoder 的关键字参数
def __init__(self, date_template, **kwds) -> None:
super().__init__(**kwds)
super().__init__()
self.date_template = date_template
# 重写 default 方法,用来处理不能编码的对象
def default(self, o):
# 如果是日期对象,则转换为一个字符串
if (isinstance(o, date)):
return self.date_template.format(o.year, o.month, o.day)
return super().default(o)
# 创建编码器并调用 encode 方法进行编码
print(MyEncoder('{0}/{1}/{2}').encode(date(2024, 1, 1)))
# 使用 dumps 函数和编码器 MyEncoder 进行编码
print(json.dumps(
[10, 'Tom', date(2024, 10, 10)],
cls=MyEncoder,
separators=(',', ':'),
date_template='{0}-{1}-{2}')
)
"2024/1/1"
[10,"Tom","2024-10-10"]