JSONは広く使われているテキスト形式の構造化データフォーマットです。
Pythonには、Pythonのデータ(オブジェクト)をJSON形式で書き出したり、反対にPythonオブジェクトにJSON形式のデータを読み込んだりするためのjsonモジュールが提供されています。
この記事では、このjsonモジュールを使ってJSONを読み書きする方法について詳しく解説していきます。なお、JSONのフォーマットについては次の記事で詳しく解説していますので、併せてご覧ください。
Pythonデータ(オブジェクト)をJSON形式で書き出す
jsonモジュールは、Pythonのデータ(オブジェクト)をJSON形式に書き出すためのdumps()関数とdump()関数を提供しています。dumps()関数は文字列へ、dump()関数はファイルオブジェクトへそれぞれJSONを書き出します。これらの関数はPythonのデータをJSON形式にエンコードするために、内部でエンコーダと呼ばれるオブジェクトを利用します。
はじめにこのエンコーダについて説明し、その後にdumps()関数とdump()関数を説明します。
デフォルトのエンコーダ
PythonのデータをJSON形式として書き出すには、Pythonのデータ型を適切なJSONのデータ型にエンコードする必要があります。この変換を行うオブジェクトはエンコーダと呼ばれます。
デフォルトのエンコーダは次の表の通りにPythonのオブジェクトをJSONのデータ型へエンコードします。
Python | JSON |
---|---|
辞書 | オブジェクト |
リスト、タプル | 配列 |
文字列 | 文字列 |
整数(int)、浮動小数点数(float)、およびintやfloadの派生列挙型 | 数値 |
True | true |
False | false |
None | null |
つまり、Pythonの辞書はJSONのオブジェクト型へ、PythonのリストやタプルはJSONの配列型へマッピングされます。エンコーダが理解できないPythonのデータ型(つまり、この表に無いのデータ型)をJSON形式にエンコードしようとするとTypeErrorが送出されます。ただし、このデフォルトの振る舞いは変更することもできます。
PythonデータをJSON文字列へ書き出す — dumps()
PythonデータをJSON形式の文字列として出力するには、Pythonデータを引数としてdumps()関数を呼び出します。
次のPythonのデータをJSON形式で書き出してみましょう。
>>> py_data = {
... "Str": "Hello!",
... "Int": 52,
... "Float": 3.14,
... "Bool": [True, False],
... "Null": None
... }
dumps()はデフォルトのエンコーダを使って、PythonデータをJSON形式にエンコードし、その結果を文字列として返します。
>>> import json
>>> json_data = json.dumps(py_data)
>>> json_data
'{"Str": "Hello!", "Int": 52, "Float": 3.14, "Bool": [true, false], "Null": null}'
Pythonのデータ型が前述の表にしたがってJSONのデータ型にエンコードされていることを確認してください。
PythonデータをJSONファイルへ書き出す — dump()
PythonデータをJSONファイルに出力するにはdump()関数を使います。最初の引数にPythonのデータを渡し、2番目の引数にはファイルオブジェクトを渡します。この例でもデフォルトのエンコーダが使われるのでPythonのデータは前述の表にしたがってエンコードされます。
>>> with open('py_data.json', 'w') as fout:
... json.dump(py_data, fout)
...
オープンしたファイルオブジェクトを確実にクローズするために、dump()関数はwith文の本体で呼び出しています。
出力されたpy_data.jsonファイルの内容は次の通りです。
{"Str": "Hello!", "Int": 52, "Float": 3.14, "Bool": [true, false], "Null": null}
dumps()とdump()に共通の引数
dump()にファイルオブジェクトの引数を指定できることを除けば、dumps()とdump()は共通の引数を持っています。ここではそれら共通の引数のうち、有用な引数について説明します。
インデントして出力する(indent引数)
indent引数を使うとインデントを使って見やすく出力することができます。indent引数のデフォルトはNoneで、これは最もコンパクトな形式で出力します(出力例は前述の例を参照)。正の整数を指定すると、出力は指定された数のスペースでインデントされます。
次の例は4つのスペースでインデントされます。
>>> indented = json.dumps(py_data, indent=4)
>>> print(indented)
{
"Str": "Hello!",
"Int": 52,
"Float": 3.14,
"Bool": [
true,
false
],
"Null": null
}
indent引数には文字列を指定することもできます。指定された文字列はそれぞれのレベルのインデントに使用されます。次の例ではインデントするための文字列としてタブ(\t)を指定しています。
>>> indented = json.dumps(py_data, indent='\t')
>>> print(indented)
{
"Str": "Hello!",
"Int": 52,
"Float": 3.14,
"Bool": [
true,
false
],
"Null": null
}
indent引数に0、負数、空文字列を指定した場合は改行だけが挿入されます。
>>> indented = json.dumps(py_data, indent=0)
>>> print(indented)
{
"Str": "Hello!",
"Int": 52,
"Float": 3.14,
"Bool": [
true,
false
],
"Null": null
}
辞書のキーでソートする(sort_keys引数)
sort_keys引数にTrue(デフォルトはFalse)を指定すると、辞書の出力はキーでソートされます。
>>> indented = json.dumps(py_data, indent=2, sort_keys=True)
>>> print(indented)
{
"Bool": [
true,
false
],
"Float": 3.14,
"Int": 52,
"Null": null,
"Str": "Hello!"
}
非ASCII文字をそのまま出力する(ensure_ascii引数)
デフォルトで非ASCII文字はエスケープされて出力されます。ensure_ascii引数にFalse(デフォルトはTrue)を指定すると非ASCII文字をそのまま出力します。
>>> print(json.dumps('こんにちは'))
"\u3053\u3093\u306b\u3061\u306f"
>>> print(json.dumps('こんにちは', ensure_ascii=False))
"こんにちは"
エンコードできないデータ型をエンコードする(default引数)
default引数には関数オブジェクトを指定します。この関数はJSONのデータ型に変換できないPythonのデータ型のオブジェクに対して、そのオブジェクトを引数として呼び出されます。JSONのデータ型に変換できないPythonのデータ型のオブジェクとは、つまり(デフォルトのエンコーダの場合)前述の表にないデータ型のことです。
default引数の関数は受け取ったPythonオブジェクトをエンコードできる型(つまり表のいずれかのデータ型)にして返すか、やはりJSONデータ型に変換できないことを示すためにTypeErrorを送出する必要があります。
デフォルトのエンコーダは、前述の表にないPythonのデータ型をエンコードできません。次の例はdatetimeオブジェクトをエンコードしようとしてTypeErrorが送出されています。
>>> from datetime import datetime
>>> now = datetime.now()
>>> json.dumps(now)
Traceback (most recent call last):
...
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type datetime is not JSON serializable
datetimeオブジェクトをJSON形式にエンコードできるようにしてみましょう。JSONには日時を表現するデータ型がないので、日時は文字列やUNIX時間(数値)として表現する必要があります。
今回はdatetimeオブジェクトが表す日時をJSONの文字列として返すtranslate_datetime()関数を定義します。この関数にはエンコードできないPythonのデータ型のオブジェクトが引数として渡されます。そのオブジェクトがdatetime型(またはそのサブクラス)であれば、’__datetime__’ をキー、値をISO形式の日時文字列の辞書として返します。datetime型(またはそのサブクラス)以外のオブジェクトが引数に渡された場合はエンコードできないのでTypeError例外を送出します。その際、TypeError()にはデフォルトのTypeError例外のメッセージと同じようなメッセージを表示するように文字列を設定しています。
>>> from datetime import datetime
>>> def translate_datetime(obj):
... if isinstance(obj, datetime):
... return {'__datetime__': obj.isoformat()}
... else:
... raise TypeError(f'Object of type {obj.__class__.__name__} '
... f'is not JSON serializable')
...
datetimeオブジェクトを {‘__datetime__’: 日時文字列} の形式として出力するのは、読み込む際の目印のためです。JSONを読み込む際に’__datetime__’という目印を見つけた場合は、datetimeオブジェクトに変換すれば元のPythonデータに復元できます。
実際にdatetimeオブジェクトを含むリストをJSON形式で出力してみましょう。
>>> data = [datetime(2023, 10, 21, 1, 23, 55),
... datetime(2023, 10, 28, 9, 52, 33),
... 'String']
>>> datetime_data = json.dumps(data, default=translate_datetime)
>>> datetime_data
'[{"__datetime__": "2023-10-21T01:23:55"}, {"__datetime__": "2023-10-28T09:52:33"}, "String"]'
カスタムエンコーダを使用する(cls引数)
独自のエンコーダを使いたい場合はcls引数に指定します。cls引数を指定しない場合はデフォルトのエンコーダが使われます。独自のエンコーダを使うと、Pythonのデータをもっと柔軟にJSON形式にエンコードすることが可能になります。
独自のエンコーダはJSONEncoderのサブクラスとして作成します。独自のエンコーダの例がPythonのドキュメントに記載されていますので、そのコードを引用しながら説明しましょう。
次のコードはPythonの複素数(complex型)をJSONに書き出すためのエンコーダ(ComplexEncoderクラス)を定義しています。ComplexEncoderクラスではcomplex型をエンコードできるようにJSONEncoderのdefault()メソッドをオーバーラードしています。
default()メソッドは前述のdefault引数の関数と同じ役割をするメソッドです。default()メソッドはPythonのオブジェクトをエンコードできる型(つまり前述の表のPythonデータ型)として返すか、そうでなければスーパークラスのdefault()メソッドを呼び出します。
ComplexEncoderクラスのdefault()メソッドでは引数objがcomplex型であれば、実部と虚部のリストを返し、complex型以外であればスーパークラスのdefault()メソッドに処理を移譲しています。つまり、自分で処理するデータ型(今回はcomplex型)以外はスーパークラスのdefault()メソッドを呼び出してデフォルトの処理を行います。
引用元:json — JSON エンコーダおよびデコーダ | Python 標準ライブラリ>>> import json >>> class ComplexEncoder(json.JSONEncoder): ... def default(self, obj): ... if isinstance(obj, complex): ... return [obj.real, obj.imag] ... return json.JSONEncoder.default(self, obj) ... >>> json.dumps(2 + 1j, cls=ComplexEncoder) '[2.0, 1.0]' >>> ComplexEncoder().encode(2 + 1j) '[2.0, 1.0]' >>> list(ComplexEncoder().iterencode(2 + 1j)) ['[2.0', ', 1.0', ']']
dumps()関数でcomplex型がJSONのリストしてエンコードできていることを確認してください。その他のコードについては今回は重要ではないのでPythonのドキュメントを参照ください。
JSON形式のデータをPythonに読み込む
JSON形式のデータをPythonに読み込むにはloads()関数かload()関数を使います。loads()関数はJSON形式の文字列から、load()関数はファイルオブジェクトからそれぞれ読み込みます。これらの関数はJSON形式のデータをPythonのオブジェクトにデコードするために、内部でデコーダと呼ばれるオブジェクトを利用します。
まずデコーダについて説明し、その後にloads()関数とload()関数を説明します。
デフォルトのデコーダ
JSON形式のデータをPythonに読み込むにはJSONのデータ型をPythonのデータ型にデコードする必要があります。デコードするのはデコーダの役割です。
デフォルトのデコーダは、次の表の通りにJSONのデータ型をPythonのデータ型にデコードします。
JSON | Python |
---|---|
オブジェクト | 辞書 |
配列 | リスト |
文字列 | 文字列 |
数値(整数) | 整数 |
数値(実数) | 浮動小数点数 |
true | True |
false | False |
null | None |
JSON文字列をPythonに読み込む — loads()
JSON形式の文字列をPythonのデータ型にデコードして読み込むにはloads()関数を使います。
次のJSON形式の文字列をPythonに読み込んでみましょう。
>>> json_data
'{"Str": "Hello!", "Int": 52, "Float": 3.14, "Bool": [true, false], "Null": null}'
JSON文字列を引数にloads()関数をJ呼び出すと、Pythonのオブジェクトにデコードして返します。
>>> json.loads(json_data)
{'Str': 'Hello!', 'Int': 52, 'Float': 3.14, 'Bool': [True, False], 'Null': None}
これはデフォルトのデコーダを使ってJSON文字列をPythonオブジェクトにデコードしますので、前述の表の通りデコードされていることが確認できるでしょう。
JSONファイルをPythonに読み込む — load()
JSONファイルをPythonのデータ型にデコードして読み込むにはload()関数を使います。引数にはJSONファイルのファイルオブジェクトを渡します。
>>> with open('py_data.json') as fin:
... json.load(fin)
...
{'Str': 'Hello!', 'Int': 52, 'Float': 3.14, 'Bool': [True, False], 'Null': None}
オープンしたファイルオブジェクトを確実にクローズするために、load()関数はwith文の本体で呼び出しています。
loads()とload()に共通の引数
loads()にファイルオブジェクトの引数を指定できることを除けば、loads()とloads()は共通の引数を持っています。ここではそれら共通の引数のうち、有用な引数について解説します。
JSONのオブジェクト型のデコードをカスタマイズする(object_hook引数)
object_hook引数には関数オブジェクトを渡します。JSONのオブジェクト型はPythonの辞書にデコードされますが、そのデコードされた辞書ごとに、その辞書を引数として関数が呼び出されます。この関数の返す戻り値は、その辞書の替わりに使われます。
つまり、object_hook引数はデフォルトで辞書として読み込まれるJSONのオブジェクト型のデータを、(関数で処理した)何らかのPythonオブジェクトに変換するために使用されます。このときの動作を順を追って詳しくみていきましょう。
- loads()またはload()のobject_hook引数に関数オブジェクトを指定して呼び出す。
- loads()またはload()はJSON形式のデータをデコードする。このときJSONのオブジェクト型はPythonの辞書にデコードされる。
- デコードされた辞書ごとに、その辞書を引数としてobject_hook引数の関数を呼び出す。
- 関数は引数に渡された辞書に何らかの処理を実施して、任意のオブジェクトを返す。
- (object_hook引数が指定されなければ)本来、辞書として出力されるところには、関数が返すオブジェクトが出力される。
Pythonのドキュメントにobject_hook引数に渡す関数の例がありますので、そのコードを引用します。このコードのas_complex関数では、Pythonの1つの複素数(complex型)データが、JSONでは1つのオブジェクト型のデータとして表現されていると仮定しています。そして、そのオブジェクト型のデータは「__complex__」というキーを含み、複素数の実部は”real”キーの値として、虚部は”imag”キーの値として表現されている必要があります。
as_complex関数はオブジェクト型がデコードされた辞書ごとに、その辞書を引数として呼び出されます。引数の辞書が「__complex__」というキーを含む場合は、その辞書の実部と虚部の値からPythonの複素数オブジェクトを生成して返します。それ以外の辞書は引数として受け取った辞書をそのまま返します。
引用元:json — JSON エンコーダおよびデコーダ | Python 標準ライブラリ>>> import json >>> def as_complex(dct): ... if '__complex__' in dct: ... return complex(dct['real'], dct['imag']) ... return dct ... >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', ... object_hook=as_complex) (1+2j)
datetimeオブジェクトをJSON形式で出力する例を示しましたが、反対にdatetimeオブジェクトへのデコードもobject_hook引数を使ってやってみましょう。decode_datetime関数は引数の辞書のキーが’__datetime__’であれば、その値の日時文字列をdatetimeオブジェクトに変換して返します。
>>> datetime_data
'[{"__datetime__": "2023-10-21T01:23:55"}, {"__datetime__": "2023-10-28T09:52:33"}, "String"]'
>>>
>>> from datetime import datetime
>>> def decode_datetime(dct):
... if '__datetime__' in dct:
... return datetime.fromisoformat(dct['__datetime__' ])
... return dct
...
>>> json.loads(datetime_data, object_hook=decode_datetime)
[datetime.datetime(2023, 10, 21, 1, 23, 55), datetime.datetime(2023, 10, 28, 9, 52, 33), 'String']
JSONのオブジェクト型のキー/値のペアのデコードをカスタマイズする(object_pairs_hook引数)
object_pairs_hook引数には関数オブジェクトを渡します。JSONのオブジェクト型は、(キー, 値)のタプルを要素とするリストにデコードされ、そのリストを引数として関数が呼び出されます。関数の返す戻り値は、辞書として読み込まれるはずだったところを置き換えます。
次の例は、object_pairs_hook引数の関数に渡される引数がどのようなものかを示しています。関数は引数に渡されたオブジェクトを単に返すだけの関数です。
>>> json_data
'{"Str": "Hello!", "Int": 52, "Float": 3.14, "Bool": [true, false], "Null": null}'
>>> json.loads(json_data, object_pairs_hook=lambda o: o)
[('Str', 'Hello!'), ('Int', 52), ('Float', 3.14), ('Bool', [True, False]), ('Null', None)]
JSONのオブジェクト型の要素のキー/値は、タプルとして、そして全体はリストになっていることがわかります。
JSONの浮動小数点数型のデコードをカスタマイズする(parse_float引数)
parse_float引数には関数オブジェクトを渡します。この関数はデコードされるJSONの浮動小数点数文字列を引数として呼び出されます。デフォルトではfloat(num_str)を指定したのと等価です。つまり、デフォルトでは浮動小数点数文字列をPythonの浮動小数点数に変換します。
次の例ではparse_floatの引数に無名関数を指定しています。この無名関数は浮動小数点数形式の文字列を引数に取り、それを浮動小数点数に変換し、小数点第一位で四捨五入した値を返します。
>>> json.loads('[2.431, 5.39, 42.943]',
... parse_float=lambda float_str: round(float(float_str), 1))
[2.4, 5.4, 42.9]
parse_float引数に渡された関数は、浮動小数点数形式の文字列ごとに呼び出されます。
このように、parse_float引数(に渡された関数)は、JSONの浮動小数点数文字列をPythonに読み込むときに何らかの処理をするために使用されます。
JSONの整数型のデコードをカスタマイズする(parse_int引数)
parse_int引数には関数オブジェクトを渡します。この関数はデコードされるJSONの整数文字列を引数として呼び出されます。デフォルトではint(num_str)を指定したのと等価です。つまり、デフォルトでは整数文字列をPythonの整数に変換します。
次の例は、JSONの整数を組み込みのfloatr()関数を使いPythonの浮動小数点数としてデコードします。
>>> json.loads('[3, 27]', parse_int=lambda int_str: float(int_str))
[3.0, 27.0]
浮動小数点数と整数の違いを除けばparse_float引数の使い方と同じですので、詳しくはparse_float引数の説明も参照ください。
カスタムデコーダを使用する(cls引数)
独自のデコーダを使いたい場合はcls引数に指定します。cls引数を指定しない場合はデフォルトのデコーダが使われます。独自のデコーダを使うと柔軟なデコード処理が可能になります。
まとめ
JSONは構文も簡潔で学ぶのも簡単です。Pythonでも簡単に扱うことができるのできるので、ぜひ使ってみてください。