如何使用 Python json 模組解析和傾印 JSON?Python json 模組介紹

閱讀 26:07·字數 7836·發佈 
Youtube 頻道
訂閱 133

先決條件

閱讀本節的先決條件是已經對 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-hant/json·codebeatme/python·GitHub