13.10. json — JS 对象简谱

未匹配的标注

目标:将 Python 对象编码为 JSON 字符串,将 JSON 字符串解码为 Python 对象。

json  模块提供了类似 pickle 的 API ,可以在内存中将 Python 对象转换成被称作 JavaScript Object Notation(JSON)的格式。不同于 pickle , JSON 可以在很多语言中便捷的执行 (特别是 JavaScript )。在 REST API 中,被广泛的应用于服务器与客户端的交互中,同时也被用作应用内交互。

编译和解码简单的数据类型

编译器默认支持 Python 原生数据类型
(strintfloatlisttuple, 以及 dict).

json_simple_types.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATA:', repr(data))

data_string = json.dumps(data)
print('JSON:', data_string)

数据被编码的方式很像 Python 的 repr() 所输出的内容。

$ python3 json_simple_types.py

DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
JSON: [{"a": "A", "b": [2, 4], "c": 3.0}]

先编码,接着再解码将会得到不同类型的对象。

json_simple_types_decode.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATA   :', data)

data_string = json.dumps(data)
print('ENCODED:', data_string)

decoded = json.loads(data_string)
print('DECODED:', decoded)

print('ORIGINAL:', type(data[0]['b']))
print('DECODED :', type(decoded[0]['b']))

尤其是,元组会变成列表。

$ python3 json_simple_types_decode.py

DATA   : [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
ENCODED: [{"a": "A", "b": [2, 4], "c": 3.0}]
DECODED: [{'a': 'A', 'b': [2, 4], 'c': 3.0}]
ORIGINAL: <class 'tuple'>
DECODED : <class 'list'>

烫烫烫 Vs 锟斤拷

JSON 相比 pickle 的另一个好处是其结果是可读的。
dumps() 函数可接受一些参数让输出结果格式变得更漂亮可读。比如, 指定 sort_keys 就可以让输出结果按字典的 key 排序而不是随机排序。

json_sort_keys.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATA:', repr(data))

unsorted = json.dumps(data)
print('JSON:', json.dumps(data))
print('SORT:', json.dumps(data, sort_keys=True))

first = json.dumps(data, sort_keys=True)
second = json.dumps(data, sort_keys=True)

print('UNSORTED MATCH:', unsorted == first)
print('SORTED MATCH  :', first == second)

排序后的结果我们可以更容易去寻找想要的结果,同时也可以进行 JSON 输出测试。

$ python3 json_sort_keys.py

DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
JSON: [{"a": "A", "b": [2, 4], "c": 3.0}]
SORT: [{"a": "A", "b": [2, 4], "c": 3.0}]
UNSORTED MATCH: True
SORTED MATCH  : True

对于嵌套了多层的数据结构,我们可以给 indent 指定一个值,输出的结果就会按给定值缩进。

json_indent.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATA:', repr(data))

print('NORMAL:', json.dumps(data, sort_keys=True))
print('INDENT:', json.dumps(data, sort_keys=True, indent=2))

indent 是非负整数时,输出结果与 pprint 打印的结果差不多,每一层级都对应缩进等级的几个空格。

$ python3 json_indent.py

DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
NORMAL: [{"a": "A", "b": [2, 4], "c": 3.0}]
INDENT: [
  {
    "a": "A",
    "b": [
      2,
      4
    ],
    "c": 3.0
  }
]

像这样的输出会增加传输所需的数据量,在生产环境中,基本上不需要这样的形式。事实上,在生产环境中一般会将分离的数据编码为比默认更加紧凑的形式进行传输。

json_compact_encoding.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATA:', repr(data))

print('repr(data)             :', len(repr(data)))

plain_dump = json.dumps(data)
print('dumps(data)            :', len(plain_dump))

small_indent = json.dumps(data, indent=2)
print('dumps(data, indent=2)  :', len(small_indent))

with_separators = json.dumps(data, separators=(',', ':'))
print('dumps(data, separators):', len(with_separators))

dumps() 的参数 separators 应是一个包含列表和字典中数据分隔符字符串的元组。默认是 (',', ':')。用来去除更多的空格,这样会生成更加紧凑的数据。

$ python3 json_compact_encoding.py

DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
repr(data)             : 35
dumps(data)            : 35
dumps(data, indent=2)  : 73
dumps(data, separators): 29

编码字典

JSON 格式编码时只能编码字典中以字符串为 key 的项,如果是非字符串则会抛出 TypeError 错误。一种避免发生此类错误的方式是告诉编码器跳过这样的项,我们可以用 skipkeys来告诉它。

json_skipkeys.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0, ('d',): 'D tuple'}]

print('First attempt')
try:
    print(json.dumps(data))
except TypeError as err:
    print('ERROR:', err)

print()
print('Second attempt')
print(json.dumps(data, skipkeys=True))

我们可以看到,比起抛出错误,非字符串 key 的项被跳过了。

$ python3 json_skipkeys.py

First attempt
ERROR: keys must be a string

Second attempt
[{"a": "A", "b": [2, 4], "c": 3.0}]

编码自定义对象

目前为止所有的例子都是用的 Python 的内置类型,json 原生就支持它们。不过有时我们也想编码一些自定义类,这里我们有两种方式来实现它。

我们尝试把下面的类编码:

json_myobj.py


class MyObj:

    def __init__(self, s):
        self.s = s

    def __repr__(self):
        return '<MyObj({})>'.format(self.s)

编码 MyObj 实例最简单的方法是定义一个函数来把未知的类型转换成已知类型。它不需要进行编码操作,它只是把一个对象转换成另一个对象。

json_dump_default.py

import json
import json_myobj

obj = json_myobj.MyObj('instance value goes here')

print('First attempt')
try:
    print(json.dumps(obj))
except TypeError as err:
    print('ERROR:', err)

def convert_to_builtin_type(obj):
    print('default(', repr(obj), ')')
    # 根据其的属性将它放入一个字典中。
    d = {
        '__class__': obj.__class__.__name__,
        '__module__': obj.__module__,
    }
    d.update(obj.__dict__)
    return d

print()
print('With default')
print(json.dumps(obj, default=convert_to_builtin_type))

convert_to_bulitin_type() 中不能被 json 识别的对象被转换成携带其信息的字典,如果程序有必要访问这个 Python 的模块,转换后的信息足够对其进行重建。

$ python3 json_dump_default.py

First attempt
ERROR: Object of type 'MyObj' is not JSON serializable

With default
default( <MyObj(instance value goes here)> )
{"__class__": "MyObj", "__module__": "json_myobj", "s": "instance
value goes here"}

我们要想根据解码结果重建 MyObj() 实例,需要使用 loads()object_hook 参数,这样在处理时就可以从模块中导入并用此来创建实例。

数据流中的每个字典都会调用 object_hook,这样就不会错过要转换的字典。hook 函数处理后的结果应是应用程序想要的对象。

json_load_object_hook.py

import json

def dict_to_object(d):
    if '__class__' in d:
        class_name = d.pop('__class__')
        module_name = d.pop('__module__')
        module = __import__(module_name)
        print('MODULE:', module.__name__)
        class_ = getattr(module, class_name)
        print('CLASS:', class_)
        args = {
            key: value
            for key, value in d.items()
        }
        print('INSTANCE ARGS:', args)
        inst = class_(**args)
    else:
        inst = d
    return inst

encoded_object = '''
    [{"s": "instance value goes here",
      "__module__": "json_myobj", "__class__": "MyObj"}]
    '''

myobj_instance = json.loads(
    encoded_object,
    object_hook=dict_to_object,
)
print(myobj_instance)

由于 json 会将字符串转成 Unicode 对象,在把它们作为类构造器的关键字参数之前我们还需要把它们重新编码为 ASCII 。

$ python3 json_load_object_hook.py

MODULE: json_myobj
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': 'instance value goes here'}
[<MyObj(instance value goes here)>]

类似的 hook 还能用在内置类型整数,浮点数和其他常量的转换上。

编码器和解码器类

除了已经涵盖的简便函数外,json 模块还提供用于解码和编码的类。使用类可以对自定义行为直接提供额外的 API。

JSONEncoder 使用的是可迭代的接口,可以生成编码数据的「块」,我们使用它可以更容易得将数据写入文件或网络套接字中而无需将整个数据结构放到内存中。

json_encoder_iterable.py

import json

encoder = json.JSONEncoder()
data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]

for part in encoder.iterencode(data):
    print('PART:', part)

输出以单个逻辑单位生成,不管之前是何种数据。

$ python3 json_encoder_iterable.py

PART: [
PART: {
PART: "a"
PART: :
PART: "A"
PART: ,
PART: "b"
PART: :
PART: [2
PART: , 4
PART: ]
PART: ,
PART: "c"
PART: :
PART: 3.0
PART: }
PART: ]

encode() 方法基本上等同于 ''.join(encoder.iterencode()),除了一些额外的错误检测。

要编码任何想要编码的对象,我们需要覆盖 default() 方法并实现类似于 convert_to_bulitin_type() 功能的代码。.

json_encoder_default.py

import json
import json_myobj

class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        print('default(', repr(obj), ')')
        # 将对象写入字典。
        d = {
            '__class__': obj.__class__.__name__,
            '__module__': obj.__module__,
        }
        d.update(obj.__dict__)
        return d

obj = json_myobj.MyObj('internal data')
print(obj)
print(MyEncoder().encode(obj))

本例的输出与之前相同。

$ python3 json_encoder_default.py

<MyObj(internal data)>
default( <MyObj(internal data)> )
{"__class__": "MyObj", "__module__": "json_myobj", "s": "internal
data"}

解码文本,然后转换字典到对象需要比之前稍多的步骤。

json_decoder_object_hook.py

import json

class MyDecoder(json.JSONDecoder):

    def __init__(self):
        json.JSONDecoder.__init__(
            self,
            object_hook=self.dict_to_object,
        )

    def dict_to_object(self, d):
        if '__class__' in d:
            class_name = d.pop('__class__')
            module_name = d.pop('__module__')
            module = __import__(module_name)
            print('MODULE:', module.__name__)
            class_ = getattr(module, class_name)
            print('CLASS:', class_)
            args = {
                key: value
                for key, value in d.items()
            }
            print('INSTANCE ARGS:', args)
            inst = class_(**args)
        else:
            inst = d
        return inst

encoded_object = '''
[{"s": "instance value goes here",
  "__module__": "json_myobj", "__class__": "MyObj"}]
'''

myobj_instance = MyDecoder().decode(encoded_object)
print(myobj_instance)

输出也与之前的相同。

$ python3 json_decoder_object_hook.py

MODULE: json_myobj
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': 'instance value goes here'}
[<MyObj(instance value goes here)>]

流与文件

之前所有的例子都假设所编码后的整个数据都可以一次性写入内存。对于巨大的数据来说,写入到类文件对象中更加好一些。 load()dump() 函数则可以将数据写入到一个类文件对象中以读取或写入。

json_dump_file.py

import io
import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]

f = io.StringIO()
json.dump(data, f)

print(f.getvalue())

套接字或正常的文件处理都与本例使用的 StringIO 缓存器一样可以工作。

$ python3 json_dump_file.py

[{"a": "A", "b": [2, 4], "c": 3.0}]

尽管不能优化到一次只读取部分数据,load() 函数仍是书写从流输入中生产对象的好方法。

json_load_file.py

import io
import json

f = io.StringIO('[{"a": "A", "c": 3.0, "b": [2, 4]}]')
print(json.load(f))

dump() 一样,任何类文件对象都能传入到 load() 中。

$ python3 json_load_file.py

[{'a': 'A', 'c': 3.0, 'b': [2, 4]}]

混合数据流

JSONDecoder 包含一个叫 raw_decode() 的方法,这个方法用于解码跟在有结构的数据之后的数据,比如带有尾文本的 JSON 数据。返回的值是由解码后的输入数据所创建的对象和解码结束的位置的索引。

json_mixed_data.py

import json

decoder = json.JSONDecoder()

def get_decoded_and_remainder(input_data):
    obj, end = decoder.raw_decode(input_data)
    remaining = input_data[end:]
    return (obj, end, remaining)

encoded_object = '[{"a": "A", "c": 3.0, "b": [2, 4]}]'
extra_text = 'This text is not JSON.'

print('JSON first:')
data = ' '.join([encoded_object, extra_text])
obj, end, remaining = get_decoded_and_remainder(data)

print('Object              :', obj)
print('End of parsed input :', end)
print('Remaining text      :', repr(remaining))

print()
print('JSON embedded:')
try:
    data = ' '.join([extra_text, encoded_object, extra_text])
    obj, end, remaining = get_decoded_and_remainder(data)
except ValueError as err:
    print('ERROR:', err)

不过,它只能在 JSON 对象在数据首部的时候才能正常工作,否则就会发生异常。

$ python3 json_mixed_data.py

JSON first:
Object              : [{'a': 'A', 'c': 3.0, 'b': [2, 4]}]
End of parsed input : 35
Remaining text      : ' This text is not JSON.'

JSON embedded:
ERROR: Expecting value: line 1 column 1 (char 0)

命令行中的 JSON

json.tool 模块实现了一个命令行程序来格式化 JSON 数据使其在命令行下更易阅读。

[{"a": "A", "c": 3.0, "b": [2, 4]}]

example.json 文件包含一个以字母表为 key 的映射对象。第一个例子按生成后本来的顺序打印出数据。第二个例子则使用 --sort-keys 来根据 key 排序后在打印输出。

$ python3 -m json.tool example.json

[
    {
        "a": "A",
        "c": 3.0,
        "b": [
            2,
            4
        ]
    }
]

$ python3 -m json.tool --sort-keys example.json

[
    {
        "a": "A",
        "b": [
            2,
            4
        ],
        "c": 3.0
    }
]

参阅

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/pymotw/json-jav...

译文地址:https://learnku.com/docs/pymotw/json-jav...

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~