jsonを使ってみよう
jsonモジュール
Python の標準ライブラリ jsonモジュールは、メモリ内のPythonオブジェクトをJSON(JavaScript Object Notation) と呼ばれるシリアル化された表現に変換するためのpickleに似たAPIを提供します。 pickleとは異なり、JSONには多くの言語で実装できるという利点があります。 これは、REST APIでWebサーバーとクライアント間の通信に最も広く使用されていますが、他のアプリケーション間通信のニーズにも役立ちます。
シリアル化では過去にはXMLが使用されることもありますが、昨今では通信量を削減することを目的として、より少ないデータ量でシリアル化できるjson が使われることが多くなっています。
単純なデータ型のエンコードとデコード
エンコーダーは、デフォルトでPythonのネイティブの型(str、int、float、list、tuple、およびdict)を理解します。
table: PythonとJSONのデータタイプ比較
Python JSON
dict, namedtuple object
list, tuple array
str, unicode string
int, long, float number
True true
False false
None null
code: json_simple_types.py
import json
print('DATA:', repr(data))
data_string = json.dumps(data)
print('JSON:', data_string)
型が保持する値は、Pythonの組み込み関数 repr()の出力に類似した方法でエンコードされます。
code: bash
$ python json_simple.types.py
JSON: [{"a": "A", "b": 2, 4, "c": 3.0}] エンコードしてからデコードすると、まったく同じタイプのオブジェクトが得られない場合があります。
code: json_simple_types_decode.py
% python json_simple.types_decode.py
ENCODED: [{"a": "A", "b": 2, 4, "c": 3.0}] DECODED: [{'a': 'A', 'b': 2, 4, 'c': 3.0}] ORIGINAL: <class 'tuple'>
DECODED : <class 'list'>
特にタプルはリストになることに注意してください。
人間が読みやすい出力とコンパクトな出力
pickle に対する json のもう1つの利点は、結果が人間が読める形式であることです。 dumps()関数は、出力をさらに良く整形するためにいくつかの引数を受け入れます。 たとえば、sort_keys引数は、辞書のキーをランダムではなくソートされた順序で出力するようにエンコーダに指示します。
code: json_sort_keys.py
import json
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出力を比較することもできます。
code: bash
$ python json_sort_keys.py
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引数に値を指定すると、出力も適切に字下げ(indent)して出力されます。
code: json_indent.py
import json
print('DATA:', repr(data))
print('NORMAL:', json.dumps(data, sort_keys=True))
print('INDENT:', json.dumps(data, sort_keys=True, indent=2))
indentが正の整数の場合、出力は pprintモジュールの出力により近くなり、データ構造の各レベルの先頭のスペースがインデントレベルと一致します。
code: bash
$ python json_indet.py
NORMAL: [{"a": "A", "b": 2, 4, "c": 3.0}] INDENT: [
{
"a": "A",
"b": [
2,
4
],
"c": 3.0
}
]
ただし、このような詳細な出力では、同じ量のデータを送信するために必要なバイト数が増えるため、実稼働環境での使用は意図されていません。 実際、エンコードされた出力でデータを分離するための設定を調整して、デフォルトよりもさらにコンパクトにすることができます。
code: json_compacrt_encoding.py
import json
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()のseparator引数は、リスト内の項目とキーを辞書内の値から分離するための文字列を含むタプルである必要があります。 デフォルトは(', ', ': ')です。 空白を削除することにより、よりコンパクトな出力が生成されます。
code: bash
$ python json_compact_encoding.py
repr(data) : 35
dumps(data) : 35
dumps(data, indent=2) : 73
dumps(data, separators): 29
辞書のエンコード
JSON形式では、辞書へのキーが文字列であることが想定されています。 文字列以外のタイプをキーとして辞書をエンコードしようとすると、TypeErrorが発生します。 この制限を回避する1つの方法は、skipkeys引数を使用して文字列以外のキーをスキップするようにエンコーダーに指示することです。
code: json_skipkeys.py
import json
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))
例外を発生させるのではなく、文字列以外のキーは無視されます。
code: bash
$ python json_skipkeys.py
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によってネイティブにサポートされているためです。 通常は、カスタムクラスもエンコードする必要があるはずで、それを行うには2つの方法があります。
このクラスをエンコードする場合を考えてみます。
code: json_myobj.py
class MyObj:
def __init__(self, s):
self.s = s
def __repr__(self):
return f'<MyObj({self.s})>'
MyObjインスタンスをエンコードする簡単な方法は、未知の型を既知の型に変換する関数を定義することです。 エンコードを行う必要がないため、あるオブジェクトを別のオブジェクトに変換するだけです。
code: 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_builtin_type()では、jsonによって認識されないクラスのインスタンスは、プログラムが必要なPythonモジュールにアクセスできる場合に、オブジェクトを再作成するのに十分な情報を備えた辞書に変換されます。
code: bash
$ python 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引数は、入力データストリームからデコードされたディクショナリごとに呼び出され、ディクショナリを別のタイプのオブジェクトに変換する機会を提供します。 フック関数は、辞書の代わりに、呼び出し元のアプリケーションが受け取る必要のあるオブジェクトを返す必要があります。
code: json_load_object_hoot.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文字列として再エンコードする必要があります。
code: bash
$ python json_load_object_hook.py
MODULE: json_myobj
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': 'instance value goes here'}
組み込み型の整数(parse_int)、浮動小数点数(parse_float)、および定数(parse_constant)についても、同様のフックを使用できます。
エンコーダーとデコーダーのクラス
すでに説明した便利な関数に加えて、jsonモジュールはエンコードとデコードのためのクラスを提供します。 クラスを直接使用すると、動作をカスタマイズするための追加のAPIに直接アクセスできます。
JSONEncoderクラスは、エンコードされたデータの断片(chunks)を生成するために反復可能なインターフェイスを使用し、メモリ内のデータ構造全体を表す必要なしに、ファイルまたはネットワークソケットへの書き込みを容易にします。
code: json_encoder_iterable.py
import json
encoder = json.JSONEncoder()
for part in encoder.iterencode(data):
print('PART:', part)
出力は、任意のサイズ値に基づくのではなく、論理単位で生成されます。
code: bash
$ python 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())と同じですが、事前に追加のエラーチェックが行われます。
任意のオブジェクトをエンコードするには、convert_to_builtin_type()で使用されているものと同様の実装でdefault()メソッドをオーバーライドします。
code: json_encoder_default.py
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))
出力は前回の例と同じものになります。
code: bash
$ python json_encoder_default.py
<MyObj(internal data)>
default( <MyObj(internal data)> )
{"__class__": "MyObj", "__module__": "json_myobj", "s": "internal data"}
テキストをデコードしてから辞書をオブジェクトに変換するには、以前の実装よりもセットアップに少し手間がかかりますが、それほど多くはありません。
code: json_decoder_object_default.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)
これも、出力は前の例と同じです。
code: bash
$ python json_decoder_object_hook.py
MODULE: json_myobj
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': 'instance value goes here'}
ストリームとファイルの操作
これまでのすべての例では、データ構造全体のエンコードされたバージョンを一度にメモリに保持できると想定しています。 データ構造が大きい場合は、ファイルのようなオブジェクトに直接エンコーディングを書き込むことが望ましい場合があります。 そうしたときに便利なload()およびdump()関数は、読み取りまたは書き込みに使用するファイルのようなオブジェクトへの参照を受け入れます。
code: json_dump_file.py
import io
import json
f = io.StringIO()
json.dump(data, f)
print(f.getvalue())
ソケットまたは通常のファイルハンドルは、この例で使用されているStringIOバッファーと同じように機能します。
code: bash
$ python json_dump_file.py
[{"a": "A", "b": 2, 4, "c": 3.0}] 一度にデータの一部のみを読み取るように最適化されていませんが、load()関数には、ストリーム入力からオブジェクトを生成するロジックをカプセル化するという利点があります。
code: 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()関数に渡すことができます。
code: bash
$ python json_load_file.py
[{'a': 'A', 'c': 3.0, 'b': 2, 4}] 混合データストリーム
JSONDecoderクラスには、データ構造をデコードするためのraw_decode()メソッドが含まれています。これは、末尾にテキストが含まれるJSONデータなどのデータが続きます。 戻り値は、入力データをデコードすることによって作成されたオブジェクトであり、デコードが中断された場所を示すそのデータへのインデックスです。
code: 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_dataend: return (obj, end, remaining)
encoded_object = '[{"a": "A", "c": 3.0, "b": 2, 4}]' extra_text = 'This text is not JSON.'
print('JSON first:')
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:
obj, end, remaining = get_decoded_and_remainder(data)
except ValueError as err:
print('ERROR:', err)
残念ながら、これはオブジェクトが入力の先頭にある場合にのみ機能します。
code: bash
$ python 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データを読みやすく再フォーマットするためのコマンドラインプログラムを実装しています。
code: example.json
[{"a": "A", "c": 3.0, "b": 2, 4}] code: bash
$ python -m json.tool example.json
[
{
"a": "A",
"c": 3.0,
"b": [
2,
4
]
}
]
$ python -m json.tool --sort-keys example.json
[
{
"a": "A",
"b": [
2,
4
],
"c": 3.0
}
]
おまけ: jq コマンド
json.tool は単順に整形するだけの機能が実装されていますが、 jq というJSONパーサーを使うと、特定のフィールドの値をコマンドラインから簡単に取り出すことができます。 この jq コマンドは、Linux, Mac, Windows で使用することができます。
Anaconda Python では次のように簡単にインストールすることができます。
code: bash
$ conda install -c conda-forge jq
code: bash
% cat example.json | jq '.[].a'
"A"
$ cat example.json | jq '.[].b0' 2
simplejson モジュール
simplejsonは、高速でシンプルかつ完全で拡張可能なJSONエンコーダーおよびデコーダーです。 Pythonで実装されていて依存モジュールはありません。
Python 2.6以降 simplejson は json として標準ライブラリに同包されるようになりました。両者の違いは、simplejson は頻繁に更新されています。また、simplejsonの方が高速に処理することができます。
最新の高速なJSONエンコーダ/デコーダを利用したいというのであれば、simplejson を利用する方がよいでしょう。
このとき、次のように simpejson がインストールされていないプラットフォームでの動作を考慮しておくことをお勧めします。
code: python
try:
import simplejson as json
except ImportError:
import json
simplejson を利用するのであれば、次のようにインストールします。
code: bash
$ pip install simplejson
次のコードがTrueを返す場合は、エンコーダ/デコーダではC言語で実装されたルーチンが呼び出されて高速に処理できることになります。
code: python
import simplejson
'_speedups' in dir(simplejson)
ujsonモジュール
ujson モジュールは ultrajson として知られているC言語で実装した超高速のJSONエンコーダー/デコーダーです。Python用の他のほとんどのJSONパーサーの置き換えとして使用できます。
ujson のインストール
ujson は次のようにインストールします。
code: bash
$ pip install ujson
ujsonの使用方法
code: ujson_dump_load.py
import ujson
sirilized_data = ujson.dumps(data)
print(sirilized_data)
new_data = ujson.loads(sirilized_data)
print(new_data)
実行すると次のようになります。
code: bash
$ python ujson_dump_load.py
[{"a":"A","b":2,4,"c":3.0}] [{'a': 'A', 'b': 2, 4, 'c': 3.0}] encode_html_chars引数は、「安全でない」HTML文字をより安全なUnicodeシーケンスに特別にエンコードできるようにするために使用されます。 デフォルトはFalseです。
code: ujson_html.py
import ujson
html_data = "<script>Beer&Wine"
json_data = ujson.dumps(html_data, encode_html_chars=True)
print(json_data)
code: bash
% python ujson_html.py
"\u003cscript\u003eBeer\u0026Wine"
ensure_ascii引数は、出力をASCIIに制限し、127を超えるすべての拡張文字をエスケープします。デフォルトはTrueです。 最終フォーマットがUTF-8をサポートしている場合は、データサイズを小さくするために、このオプションを False に設定することを強くお勧めします。
code: ujson_ensure_ascii.py
import ujson
data = "åäö"
ascii_data = ujson.dumps(data, encode_html_chars=True)
print(ascii_data)
code: bash
$ python ujson_ensure_ascii.py
"\u00e5\u00e4\u00f6"
escape_forward_slashes引数は、スラッシュ(/)をエスケープするかどうかを制御します。 デフォルトはTrueです。
code: ujson_escape_slashes.py
import ujson as json
serialized = json.dumps(data)
print(serialized)
serialized = json.dumps(data, escape_forward_slashes=False)
print(serialized)
code: bash
% python ujson_escape_slashes.py
"http:\/\/www.google.com"
indent引数は、出力で整形のためのインデントを有効にするかどうかを制御します。 デフォルトはゼロ(0)で、つまりインデントなしで出力します。
code: ujson_indent.py
import ujson as json
print('DATA:', repr(data))
print('NORMAL:', json.dumps(data))
print('INDENT:', json.dumps(data, indent=2))
code: bash
$ python ujson_indent.py
NORMAL: [{"a":"A","b":2,4,"c":3.0}] INDENT: [
{
"a": "A",
"b": [
2,
4
],
"c": 3.0
}
]
まとめ
pickleとjsonpickleは、非シリアライズ化するときに任意のPythonコードやシステムコマンドを実行することができます。信頼できない/認証されていないソースからのPicle化データはロードするべきではありません。
jsonpickleでは、load_backend()メソッドとset_preferred_backend()メソッドを使用してJSONのエンコードとデコードに使用するJSONバックエンドを選択できます。 必要に応じて、シリアル化ハンドラーをカスタマイズすることもできます。 多くの場合、jsonpickleはシリアル化されたデータを簡単に読み取ることができる必要がある開発者にとって便利なものになります。
ujson は、標準ライブラリ json との互換性を維持しながら高速かつ正確にシリアル化/非シリアル化を行うことができます。
参考
Python 公式ドキュメント
jq - 軽量で柔軟なコマンドラインJSONパーサー jsonpickle - 複雑なPythonオブジェクトとJSONを双方向に変換するためのライブラリ ujson - C言語で実装した超高速のJSONエンコーダー/デコーダー