如何使用 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"]