如何使用 Python json 模块解析和转储 JSON?Python json 模块介绍

我被代码海扁署名-非商业-禁演绎
阅读 26:12·字数 7865·发布 
Bilibili 空间
关注 950

前提

阅读本节的前提是已经对 JSON 有所了解,你可以查看编程教程JSON,JSON 字符串,JSON 数据类型介绍一节来获取相关信息。

Python json 模块

Python 的json模块包含了一系列与 JSON 相关的函数和类,开发人员可以通过该模块将 JSON 字符串解析(解码)为一个 Python 对象,或将 Python 对象转储(编码)为 JSON 字符串,或者自定义 JSON 的解析过程。

在解析和转储的过程中,JSON 中的数字将对应 Python 中的intfloat类型,JSON 中的truefalse将对应 Python 中的TrueFalse,JSON 中的null将对应 Python 中的空值None,JSON 中使用双引号表示的字符串将对应 Python 中的str类型,JSON 中使用方括号表示的数组将对应 Python 中的list类型,JSON 中使用花括号表示的对象将对应 Python 中的dict类型。

Python json 模块允许 JSON 字符串出现非标准内容

在标准的 JSON 中,允许书写nulltruefalse,比如{"success":false},但不允许书写NaNInfinity-Infinity,比如,{"age":NaN}将被视为一个错误。

不过,Python 对 JSON 字符串的要求更为宽松,解析字符串'{"age":NaN,"size":Infinity}'或转储对象[float('nan'),float('-inf')]是可行的,NaNInfinity-Infinity将被转换为表示非数字,正无穷和负无穷的 Python 浮点数,他们在 Python 中可显示为naninf-inf

使用 Python json 模块将 JSON 字符串解析为 Python 对象

要将 JSON 格式的字符串解析为 Python 对象,你可以使用 Python 的json模块的loadloads函数,这两个函数拥有几乎相同的参数。不同的是,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 字符串(strbytesbytearray对象)。

cls,object_hook,parse_float,parse_int,parse_constant,object_pairs_hook,kwds 参数

参数clsobject_hookparse_floatparse_intparse_constantobject_pairs_hookkwds主要用于自定义 JSON 字符串的解析过程,我们会在后面详细介绍。

对于 JSON 字符串中键相同的键值对,Python json 模块仅接受最后一个

在 JSON 字符串中,如果使用{}表示的同一个对象存在键相同的键值对,那么最后出现的键值对被视为有效的,比如,使用loadloads函数解析'{"name":"Jack","name":"Tom","other":{"name":"Harry","name":"Jerry"}}'的结果为 Python 对象{'name':'Tom','other':{'name':'Jerry'}}

在下面的示例中,我们分别使用loadloads函数解析了由参数fps提供的 JSON 字符串。其中第二个height是有效的,因此最终结果显示300而不是200

decode.py
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 字符串的解析,loadloads函数会使用相关参数创建JSONDecoder解码器对象,并由该对象负责将 JSON 字符串转换为一个 Python 对象。如果 JSON 字符串是无效的,那么loadloads函数可能会抛出异常JSONDecodeError

以下是 Pythonjson模块的JSONDecoder类的构造器,其大部分参数的名称和作用与loadloads函数相同,使用这些参数可以完成 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 字符串中出现的NaNInfinity-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_hookobject_hook,则object_hook将被忽略。

strict 参数

strict参数用于说明是否不允许在 JSON 字符串中使用控制字符,当该参数被设置为True且 JSON 字符串包含控制字符时,将引发异常json.decoder.JSONDecodeError: Invalid control character at: …

Python 3.1 之前,parse_constant参数对应的函数还针对 JSON 字符串中出现的nulltruefalse

什么控制字符?

上面提到的控制字符,是指编码在031以内的字符,这包括常见的\t\r\n等。

在下面的示例中,我们使用loads函数解析 JSON 字符串,转换后的 Python 对象将被函数hook处理,该函数会检查分数中的所有负数。

decode_hook.py
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对象。

decode_hook.py
# …
# 函数 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 中与数字相关的NaNInfinity-Infinity转换为 Python 空值None。当然,由于'-Infinity'表示的是字符串,因此他不会被转换为空值None

decode_parse.py
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 字符串中的NaNInfinity-Infinity转换为 Python 浮点数。

decode_parse.py
# …
# 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模块的loadloads函数的参数来自定义 JSON 字符串的解析过程,通过继承json模块的JSONDecoder类,可以实现相同的效果。在编写JSONDecoder类的派生类之后,你需要将派生类赋值给loadloads函数的cls参数。

cls 参数

loadloads函数的cls参数应该是继承自JSONDecoder的派生类,如果忽略该参数或设置为None,则loadloads函数将使用JSONDecoder类来完成 JSON 字符串的解析任务。

kwds 参数

loadloads函数的kwds参数,可用于为cls表示的派生类的构造器指定参数。

修改 Python json 模块的 JSONDecoder 对象的变量可能不会产生效果

Pythonjson模块的JSONDecoder对象拥有一些与其构造器参数同名的变量,比如parse_int,修改这些变量可能不会达到自定义 JSON 解析方式的效果,因为自定义已经在构造器中完成。

下面的示例展示了 JSON 解码器MyDecoder,在其构造器中,我们指定了函数和方法用于处理 JSON 字符串中表示数字的文本,最后,在调用loads函数解析字符串时,我们将cls参数赋值为类MyDecoder,并添加了一个名称为strict的关键字(命名)参数,该参数在创建MyDecoder对象时被使用。

由于,loads函数的strict参数为False,因此,解码器MyDecoder将允许 JSON 字符串出现控制关键字,比如,代码中的\t

decode_cls.py
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对象的decoderaw_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.Patternmatch方法,用于确定 JSON 字符串的开始和结束位置,默认值为WHITESPACE.match,其中WHITESPACE是定义在json模块中的re.Pattern对象。

idx 参数

idx参数用于确定 JSON 字符串的开始位置,0表示从第一个字符开始。

在调用方法decode时,我们将 3 个连续的+-作为 JSON 的边界。在调用方法raw_decode时,我们指定第 4 个字符{为 JSON 的开始,返回元组中的19表示 JSON 结束于第 19 个字符}

decode.py
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模块的dumpdumps函数,他们的参数几乎相同。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或以下基础类型之一,strintfloatbool,被忽略的键值对将不会出现在 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 字符串中的NaNInfinity-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 参数

参数clskwds主要用于自定义的 JSON 编码器,我们会在后面详细介绍。

在下面的示例中,由于dump函数的skipkeysTrue,因此键值对date(2024,7,17):True将被忽略。第一个dumps函数设置了separators参数,其转储的 JSON 字符串为非标准的,他可能无法被正常解析。第二个dumps函数将allow_nan参数设置为了False,因此表示负无穷的浮点数导致了异常。

encode.py
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参数控制是否检测循环引用,不过,无论是否检测都会引发异常。

encode_circular.py
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 字符串,dumpdumps函数会使用相关参数创建JSONEncoder编码器对象,并由该对象的iterencodeencode方法负责将对象编码(转储)为一个 JSON 字符串。其中,encode方法返回编码得到的 JSON 字符串,iterencode方法返回一个包含了被分割的 JSON 字符串的迭代器对象。

以下是 Pythonjson模块的JSONEncoder类的构造器以及方法iterencodeencode,其中构造器的参数的名称和作用与dumpdumps函数相同。

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模块的dumpdumps函数可以完成 JSON 的编码任务,并不需要主动创建JSONEncoder对象,然后调用其iterencodeencode方法。当然,创建JSONEncoder或其派生类的实例也是可行的,你可以重写JSONEncoderdefault方法,该方法的作用与之前讲述的dumpdumps函数的default参数的作用相同,可用于将不能编码的 Python 对象转换为可编码的对象。

default(o)

o 参数

o参数为不能进行编码的 Python 对象。

自定义的 JSON 编码器的构造器最好保留 JSONEncoder 类的构造器的关键字参数

如果你定义了自己的 JSON 编码器(即json模块的JSONEncoder类的派生类),那么编码器的构造器最好保留JSONEncoder的构造器的关键字参数,最简单的做法是在末尾定义以**为前缀的可变关键字。否则,将自定义的 JSON 编码器传递给dumpdumps函数将导致错误。

在下面的示例中,我们定义了自己的 JSON 编码器MyEncoder,并重写了方法default来处理无法编码的date对象,变量date_template用于格式化日期。构造器末尾以**为前缀的参数kwds可以让MyEncoder轻松接受dumpdumps函数的关键字参数。

encode_cls.py
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"]

源码

src/zh/json·codebeatme/python·GitHub