每週一個 Python 模組 | json

yongxinz發表於2019-04-01

專欄地址: 每週一個 Python 模組

目的: 將 Python 物件編碼為 JSON 字串,並將 JSON 字串解碼為 Python 物件。

json 模組提供了一個類似於 pickle 的 API,將記憶體中的 Python 物件轉換為 JSON 序列。與 pickle 不同,JSON 具有以多種語言(尤其是 JavaScript)實現的優點。它在 REST API 中 Web 服務端和客戶端之間的通訊被廣泛應用,同時對於應用程式間通訊需求也很有用。

編碼和解碼簡單資料型別

Python 的預設原生型別(strintfloatlisttuple,和dict)。

import json

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

data_string = json.dumps(data)
print('JSON:', data_string)	# JSON: [{"a": "A", "b": [2, 4], "c": 3.0}]
複製程式碼

表面上看,類似於 Python repr() 的輸出。

編碼,然後重新解碼可能不會給出完全相同型別的物件。

import json

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

data_string = json.dumps(data)
print('ENCODED:', data_string)	# ENCODED: [{"a": "A", "b": [2, 4], "c": 3.0}]

decoded = json.loads(data_string)
print('DECODED:', decoded)	# [{'a': 'A', 'b': [2, 4], 'c': 3.0}]

print('ORIGINAL:', type(data[0]['b']))	# ORIGINAL: <class 'tuple'>
print('DECODED :', type(decoded[0]['b']))	# DECODED : <class 'list'>
複製程式碼

特別是,元組成為了列表。

格式化輸出

JSON 的結果是更易於閱讀的。dumps() 函式接受幾個引數以使輸出更易讀結果。例如,sort_keys 標誌告訴編碼器以排序而不是隨機順序輸出字典的鍵。

import json

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

unsorted = json.dumps(data)
print('JSON:', json.dumps(data))	# JSON: [{"a": "A", "b": [2, 4], "c": 3.0}]
print('SORT:', json.dumps(data, sort_keys=True))	# SORT: [{"a": "A", "b": [2, 4], "c": 3.0}]

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

print('UNSORTED MATCH:', unsorted == first)	# UNSORTED MATCH: True
print('SORTED MATCH  :', first == second)	# SORTED MATCH  : True
複製程式碼

對於高度巢狀的資料結構,可以指定 indent 引數來格式化輸出。

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))

# output
# 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
#   }
# ]
複製程式碼

當 indent 是非負整數時,輸出接近於 pprint,與資料結構的每個級別的前導空格匹配縮排級別。

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))

# output
# DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
# repr(data)             : 35
# dumps(data)            : 35
# dumps(data, indent=2)  : 73
# dumps(data, separators): 29
複製程式碼

dumps()separators 引數是一個元組,可以分開列表中的元素和字典中的鍵值對,預設是 (', ', ': ')。通過移除空白,可以產生更緊湊的輸出。

編碼字典

JSON 格式要求字典的鍵是字串,如果使用非字串型別作為鍵對字典進行編碼,會報錯 TypeError。解決該限制的一種方法是使用 skipkeys 引數告訴編碼器跳過非字串鍵:

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))

# output
# First attempt
# ERROR: keys must be str, int, float, bool or None, not tuple
# 
# 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 例項最簡單的方法是定義一個函式,把未知的型別轉換成已知型別。它不需要進行編碼操作,它只是把一個物件轉換成另一個物件。

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), ')')
    # Convert objects to a dictionary of their representation
    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))

# output
# 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"}
複製程式碼

convert_to_bulitin_type() 中不能被 json 識別的物件被轉換成攜帶其資訊的字典,如果程式有必要訪問這個 Python 的模組,轉換後的資訊足夠對其進行重建。

我們要想根據解碼結果重建 MyObj() 例項,需要使用 loads()object_hook 引數,這樣在處理時就可以從模組中匯入並用此來建立例項。

資料流中的每個字典都會呼叫 object_hook,這樣就不會錯過要轉換的字典。hook 函式處理後的結果應是應用程式想要的物件。

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)

# output
# MODULE: json_myobj
# CLASS: <class 'json_myobj.MyObj'>
# INSTANCE ARGS: {'s': 'instance value goes here'}
# [<MyObj(instance value goes here)>]
複製程式碼

由於 json 會將字串轉成 Unicode 物件,在把它們作為類構造器的關鍵字引數之前我們還需要把它們重新編碼為 ASCII 。

類似的 hook 還能用在內建型別整數,浮點數和其他常量的轉換上。

編碼器和解碼器類

除了已經涵蓋的簡便函式外,json 模組還提供用於解碼和編碼的類。使用類可以對自定義行為直接提供額外的 API。

JSONEncoder 使用的是可迭代的介面,可以生成編碼資料的「塊」,我們使用它可以更容易得將資料寫入檔案或網路套接字中而無需將整個資料結構放到記憶體中。

import json

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

for part in encoder.iterencode(data):
    print('PART:', part)
    
# output
# 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() 功能的程式碼。.

import json
import json_myobj


class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        print('default(', repr(obj), ')')
        # Convert objects to a dictionary of their representation
        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))

# output
# <MyObj(internal data)>
# default( <MyObj(internal data)> )
# {"__class__": "MyObj", "__module__": "json_myobj", "s": "internal data"}
複製程式碼

輸出與先前的實現相同。

解碼文字,然後轉換字典到物件需要比之前稍多的步驟。

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)

# output
# MODULE: json_myobj
# CLASS: <class 'json_myobj.MyObj'>
# INSTANCE ARGS: {'s': 'instance value goes here'}
# [<MyObj(instance value goes here)>]
複製程式碼

輸出與前面的例子相同。

使用流和檔案

到目前為止,所有示例都假設整個資料結構可以一次儲存在記憶體中。對於大型資料結構,最好將其直接寫入類檔案物件。load()dump()接受對類似檔案的物件的引用以用於讀取或寫入。

import io
import json

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

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

print(f.getvalue())	# [{"a": "A", "b": [2, 4], "c": 3.0}]
複製程式碼

套接字或普通檔案控制程式碼的工作方式與本示例中使用的 StringIO 緩衝區相同 。

import io
import json

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

就像 dump(),任何類似檔案的物件都可以傳遞給 load()

混合資料流

``JSONDecoder包含一個叫raw_decode()` 的方法,這個方法用於解碼跟在有結構的資料之後的資料,比如帶有尾文字的 JSON 資料。返回的值是由解碼後的輸入資料所建立的物件和解碼結束的位置的索引。

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)
    
# output
# 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

json.tool 模組實現了一個命令列程式,用於重新格式化 JSON 資料以便於閱讀。

[{"a": "A", "c": 3.0, "b": [2, 4]}]
複製程式碼

輸入檔案 example.json 包含按字母順序排列的鍵對映。下面的第一個示例按順序顯示重新格式化的資料,第二個示例用 --sort-keys,在列印輸出之前對對映鍵進行排序。

$ 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
    }
]
複製程式碼

原文連結:

pymotw.com/3/json/inde…

相關文章