読者です 読者をやめる 読者になる 読者になる

機械語と数学とことば

更新が進みません.

【プログラミング】多階層の辞書を一階層の辞書に変換する(Python3)

大変久しぶりにブログを書きます。最近は研究プロジェクトが再開したり、個人的な勉強会で数学を学ぶ機会を持ったり、仕事も変わったりと色々とありました。

今年の目標としては「書くこと」であるので、今回も研究プロジェクトで必要になったコードをこちらでご紹介します。

今回、DBにそのままバルクインサートすることも躊躇してしまうほど、某APIから全くぐちゃぐちゃなJSONデータが得られました。 jsonパッケージをそのまま使うと、Pythonのディクショナリ形式に変換することができるのですが、 しかしながら、中身がぐちゃぐちゃ・・・keyの値も正規化されておりません。

今回は、anaconda3-2.4.0によってインストールされる、Python3.5やその他パッケージを利用して実行します。

まず、JSONデータの中身は下記の通り・・・ (JSON Data Set Sampleより引用)

{
    "id": "0001",
    "type": "donut",
    "name": "Cake",
    "image":
        {
            "url": "images/0001.jpg",
            "width": 200,
            "height": 200
        },
    "thumbnail":
        {
            "url": "images/thumbnails/0001.jpg",
            "width": 32,
            "height": 32
        }
}

これを全て一階層で取れるようにしたいと思います。

そのためには、「再帰」を使って、ディクショナリ内から、keyがなくなるまで取得し続けるということを繰り返します。

while文でも書くことができるのですが、コードを短く済ませることができ、便利なので再帰を使いましょう。

まずはそこまでのパスを取得する関数です。

# 複雑なディクショナリから、どんなKeyがあるのか再帰的に取り出す。
# 引数は、対象の辞書、ルート(最下階層)、区切り文字です。
# 利用例:get_dict_hierarchy(cake_image,'','_')
def get_dict_hierarchy(target_dict,root_path,sep):
    if isinstance(target_dict,dict):
        for key in target_dict.keys():
            value = target_dict[key]
            target_path = root_path + sep + key
            # valueが辞書じゃない場合にはパスを返す。
            if not isinstance(value,dict):
                yield target_path[1:]
            # 辞書の場合にはもう一度探索する。
            else:
                yield from get_dict_hierarchy(value,target_path,sep)

ループのところ、target_dict.items()でやってもいいかもしれません。そうすると一行削れるかもしれないですね。 ちなみに上のデータで実行すると下記のようになります。

In[2]:
cake_dict = {
    "id": "0001",
    "type": "donut",
    "name": "Cake",
    "image":
        {
            "url": "images/0001.jpg",
            "width": 200,
            "height": 200
        },
    "thumbnail":
        {
            "url": "images/thumbnails/0001.jpg",
            "width": 32,
            "height": 32
        }
}
In[3]: list(get_dict_hierarchy(cake_dict,'','_'))
Out[3]:
['thumbnail_width',
 'thumbnail_height',
 'thumbnail_url',
 'image_width',
 'image_height',
 'image_url',
 'type',
 'id',
 'name']

これで階層ごとのデータを取れました。(データはジェネレータなので、リストに直して表示しています。) では、それぞれの階層からディクショナリを深堀し、ターゲットの値を取得して、 一階層からなるディクショナリに変換します。

掘っていく関数は下記の通り。

# 階層が深いディクショナリを掘っていき、ターゲットの値を取得する。
# 対象となるディクショナリ、探したいデータまでのパス、区切り文字
# 利用例:dig_dict(cake_dict,'thumbnail_url','_')
def dig_dict(target_dict,target_branch,sep):
    limbs = target_branch.split(sep)
    leaf = target_dict
    for one_limb in limbs:
        leaf = leaf[one_limb]
    return leaf

では、データを作りましょう。

In[5]: flat_dict = {}
In[6]: for one_branch in list(get_dict_hierarchy(cake_dict,'','_')):
          flat_dict[one_branch] = dig_dict(cake_dict,one_branch,'_')
In[7]: flat_dict
Out[7]: 
{'id': '0001',
 'image_height': 200,
 'image_url': 'images/0001.jpg',
 'image_width': 200,
 'name': 'Cake',
 'thumbnail_height': 32,
 'thumbnail_url': 'images/thumbnails/0001.jpg',
 'thumbnail_width': 32,
 'type': 'donut'}

と、お目当てのデータが手に入りました。

再帰を使ってうまくネストを解いたり、JSONデータの解析をおこなったりすることもあるでしょう。

APIからくるデータは汚いことが多いですが、まずこれらの関数を使ってみて、 どういう結果が得られるか確かめてみるのもいかがでしょうか。