13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ijsonでjsonを少しずつ読み込む

Posted at

はじめに

大容量のjsonをpythonのjsonライブラリで解析しようとしてメモリがすごく食われてメモリエラーになる事態になってしまいました。
そこでストリーム形式のパーサーを探してijsonを見つけましたが、資料が少ないためまとめます。
※簡単に使えるのでそもそも資料が不要かもしれません。

環境

  • python:3.6.5
  • ijson:2.4

インストール

インストールは普通にpipでインストールします。

>pip install ijson
Collecting ijson
...
Successfully installed ijson-2.4

使用方法

jsonの値の取得

ijsonのitems関数にjsonのファイルオブジェクトと取得したいキーを与えるとジェネレータが返却されます。
そのジェネレータからnextやループでjsonの値をとることができます。

import ijson
with open(jsonファイル, "r") as file_obj:
    ijson_generator = ijson.items(file_obj, "キー1.キー2")
    key2_value = next(ijson_generator)

jsonのリストの取得

ほとんど値の取得と同様です。
リストの場合は取得したいキーの後にitemを追加してあげる必要があります。
返却されたジェネレータからループでjsonの値をとることができます。

import ijson
with open(jsonファイル, "r") as file_obj:
    ijson_generator = ijson.items(file_obj, "キー1.キー2.item")
    for value in ijson_generator:
        value

json全体の解析

例えば、キーが可変のjsonを受け取る際などにキーを指定せずにjsonの解析をすることもできます。
prefixにはキーがeventには型、valueには値が格納されます。
以下の点が特殊なので気を付けてください。

  • jsonのルート階層の情報を解析したときのprefixは空
  • dictの始まりのeventはstart_map, valueはNone
  • dictの終わりのeventはend_map, valueはNone
  • dictのキーのeventはmap_key
  • listの始まりのeventはstart_array, valueはNone
  • listの終わりのeventはend_array, valueはNone
  • listの要素のキーはキー.item
import ijson
with open(jsonファイル, "r") as file_obj:
    pet_parse = ijson.parse(file_obj)
    for prefix, event, value  in pet_parse:
        print("prefix:{}, event:{}, value:{}".format(prefix, event, value))

文字列の解析

今まではjsonファイルを開いて解析していましたが、WEBアプリケーションなどでは送信された文字列を解析したい場面があると思います。
そのような場合でも、ストリーム形式にしてあげることでijsonで解析ができます。

import ijson
import io
json_str = '{"pets": {"type": "dog","age": 5,"like": ["walking","eating","hamster"]},"dog": "bow"}'
pet_parser = ijson.parse(io.StringIO(json_str))
for prefix, event, value  in pet_parser:
    print("prefix:{}, event:{}, value:{}".format(prefix, event, value))

小ネタ

jsonの階層を無視してすべてのkeyを取りたい場合は、eventを利用して楽にできる
イコールの値を変えれば階層を無視して同じ型の情報を楽に集められる

with open(ファイル名, "r") as file:
    pet_parser = ijson.parse(file)
    map_keys = [value for pet_parse, event, value in pet_parser if event == 'map_key']
    print('map_keys:{}'.format(map_keys))

実例

使用するjson

animal.json
{
    "pets": {
        "type": "dog",
        "age": 5,
        "like": [
            "walking",
            "eating",
            "hamster"
        ]
    },
    "dog": "bow"
}

使用するpython

pyMod.py
import ijson
import io

print('------ get value ---------')
with open('animal.json', 'r') as file:
    pet_type = ijson.items(file, 'pets.type')
    print(next(pet_type))

print('------ get list ---------')
with open('animal.json', 'r') as file:
    pet_like = ijson.items(file, 'pets.like.item')
    for value in pet_like:
        print(value)

print('------ get all ---------')
with open('animal.json', 'r') as file:
    pet_parse = ijson.parse(file)
    for prefix, event, value  in pet_parse:
        print('prefix:{}, event:{}, value:{}'.format(prefix, event, value))

print('------ get all str ---------')
json_str = '{"pets": {"type": "dog","age": 5,"like": ["walking","eating","hamster"]},"dog": "bow"}'
pet_parser = ijson.parse(io.StringIO(json_str))
for prefix, event, value  in pet_parser:
    print('prefix:{}, event:{}, value:{}'.format(prefix, event, value))

print('------ get keys ---------')
with open('animal.json', 'r') as file:
    pet_parser = ijson.parse(file)
    map_keys = [value for pet_parse, event, value in pet_parser if event == 'map_key']
    print('map_keys:{}'.format(map_keys))

結果

------ get value ---------
dog
------ get list ---------
walking
eating
hamster
------ get all ---------
prefix:, event:start_map, value:None
prefix:, event:map_key, value:pets
prefix:pets, event:start_map, value:None
prefix:pets, event:map_key, value:type
prefix:pets.type, event:string, value:dog
prefix:pets, event:map_key, value:age
prefix:pets.age, event:number, value:5
prefix:pets, event:map_key, value:like
prefix:pets.like, event:start_array, value:None
prefix:pets.like.item, event:string, value:walking
prefix:pets.like.item, event:string, value:eating
prefix:pets.like.item, event:string, value:hamster
prefix:pets.like, event:end_array, value:None
prefix:pets, event:end_map, value:None
prefix:, event:map_key, value:dog
prefix:dog, event:string, value:bow
prefix:, event:end_map, value:None
------ get all str ---------
prefix:, event:start_map, value:None
prefix:, event:map_key, value:pets
prefix:pets, event:start_map, value:None
prefix:pets, event:map_key, value:type
prefix:pets.type, event:string, value:dog
prefix:pets, event:map_key, value:age
prefix:pets.age, event:number, value:5
prefix:pets, event:map_key, value:like
prefix:pets.like, event:start_array, value:None
prefix:pets.like.item, event:string, value:walking
prefix:pets.like.item, event:string, value:eating
prefix:pets.like.item, event:string, value:hamster
prefix:pets.like, event:end_array, value:None
prefix:pets, event:end_map, value:None
prefix:, event:map_key, value:dog
prefix:dog, event:string, value:bow
prefix:, event:end_map, value:None
------ get keys ---------
map_keys:['pets', 'type', 'age', 'like', 'dog']

おわりに

jsonをストリーム形式で解析する方法を記載しましたが、キーの位置次第で一括で変換するよりも遅くなる場合があると思います。
そもそも論を言うとそこまで大きなjsonにするべきではない。少なくともdictの中にdictやlistを持たせるような複雑な構造にしてはいけないと思います。
慣れの問題もあるかもしれませんが、csvは情報が足りないと感じ、xmlは情報が過剰な気がしてjsonが程よい気がして、なんでもかんでもjsonにしたい気持ちもすごくわかります。
ならばせめてjsonの構造を簡単にして大容量にならないように工夫するのが一番かなと思っています。

13
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?