Python JSONデータをTreeViewで表示してみた。

JSONデータで情報を受信していると、受信したJSONデータそのものをリアルタイムでそのまま表示させたいことがままあります。
json.dumpsメソッドにindent指定して、テキスト表示すれば、きれいに改行して表示すことが簡単にできます。
しかし、大量のJSONデータが全て展開されていると、所望のデータにたどり着くまでスクロールすることになります。あるいはエディタに展開して検索ですね。

TreeViewで閉じられた状態のほうが便利かなと思い、今回のコードを作りました。

# データ準備

記事のために利用させていただいたJSONデータは、以下のサイトです。
[京都市オープンデータポータルサイト](https://data.city.kyoto.lg.jp/riyou/api)

リクエストURLの例
https://data.city.kyoto.lg.jp/API/action/datastore/search.json?resource_id=f14b57c2-48dd-4aa7-b754-a4f4ac340f2d

WEBブラウザのURLにリクエスト例を入力すると、JSONデータが表示されますので、これをテキストエディタに全選択コピーをして、sample.jsonと名付けて保存してください。

# ソースコード
ソースコードファイルと同じフォルダに準備したsample.jsonを保存してください。

<details><summary>サンプルコード</summary>

``` Python
import json
import pathlib
import tkinter as tk
import tkinter.ttk as ttk


class TkJSON_TreeView():
    def __init__(self, root, max_item_length: int = 300, max_value_length: int = 500):
        inFrame = tk.Frame(root)
        tree = ttk.Treeview(
            inFrame,
            height=30,
            columns=["VALUE", ],
        )
        tree_h_scroll = ttk.Scrollbar(
            inFrame,
            orient=tk.HORIZONTAL,
            command=tree.xview
        )
        tree_v_scroll = ttk.Scrollbar(
            inFrame,
            orient=tk.VERTICAL,
            command=tree.xview
        )
        tree['xscrollcommand'] = tree_h_scroll.set
        tree['yscrollcommand'] = tree_v_scroll.set

        tree.column("#0", width=200)
        tree.column("VALUE", width=100)
        tree.heading("#0", text="構造")
        tree.heading("VALUE", text="値")

        tree.bind('<<TreeviewSelect>>', self.select_event)

        self.inFrame = inFrame
        self.tree = tree
        self.tree_h_scroll = tree_h_scroll
        self.tree_v_scroll = tree_v_scroll
        self.max_item_length = max_item_length
        self.max_value_length = max_value_length

    def select_event(self, envet):
        print(self.tree.selection())

    def run(self, json_dict: dict):
        tree = self.tree

        def _next_tree_(tree, head, text, value, deep=0, text_max=0, value_max=0):
            deep += 1
            _deep = None
            if isinstance(value, dict):
                for k, v in value.items():
                    tid = tree.insert(head, tk.END, text=k)
                    text_max = max(text_max, len(str(k)))
                    _deep_, text_max, value_max = _next_tree_(
                        tree,
                        tid,
                        text=k,
                        value=v,
                        deep=deep,
                        text_max=text_max,
                        value_max=value_max
                    )
                    if _deep:
                        _deep = max(_deep, _deep_)
                    else:
                        _deep = _deep_
            elif isinstance(value, list):
                for i, c_val in enumerate(value):
                    if isinstance(c_val, dict):
                        tid = tree.insert(head, tk.END, text=str(i))
                        text_max = max(text_max, len(str(i)))
                        _deep_, text_max, value_max = _next_tree_(
                            tree, tid,
                            text=str(i),
                            value=c_val,
                            deep=deep,
                            text_max=text_max,
                            value_max=value_max
                        )
                        if _deep:
                            _deep = max(_deep, _deep_)
                        else:
                            _deep = _deep_
                    elif isinstance(c_val, list):
                        # List内のListについては未対応
                        pass
                    else:
                        if c_val is None:
                            c_val = "None"
                        c_val = str(c_val)
                        tree.insert(head, tk.END, text=str(i), value=c_val)
                        text_max = max(text_max, len(str(i)))
                        value_max = max(value_max, len(c_val))
            else:
                if value is None:
                    value = "None"
                value = str(value)
                text = str(text)
                tree.insert(head, tk.END, text=text, value=value)
                text_max = max(text_max, len(text))
                value_max = max(value_max, len(value))

            if _deep:
                deep = _deep

            return deep, text_max, value_max

        deep, text_max, value_max = _next_tree_(tree, '', '', json_dict)

        item_length = deep*4*8
        value_length = value_max*8

        if self.max_item_length:
            if isinstance(self.max_item_length, int):
                if item_length > self.max_item_length:
                    item_length = self.max_item_length
        tree.column("#0", width=item_length)
        if self.max_value_length:
            if isinstance(self.max_value_length, int):
                if value_length > self.max_value_length:
                    value_length = self.max_value_length
        tree.column("VALUE", width=value_length)

        tree.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W)
        self.tree_h_scroll.grid(row=1, column=0, sticky=tk.EW)
        self.tree_v_scroll.grid(row=0, column=1, sticky=tk.NS)
        self.inFrame.columnconfigure(0, weight=1)
        self.inFrame.rowconfigure(0, weight=1)
        self.inFrame.pack()


file_path = pathlib.Path(__file__).parent / 'sample.json'

print(file_path)

with open(file_path, 'r') as f:
    data = f.read()
    data = json.loads(data)

# トップレベルウィンドウ作成
root = tk.Tk()

TkJSON_TreeView(root).run(data)

# メインループ
root.mainloop()


```

</details>

# 実行結果

実行結果は以下のように、データ構造の深さに合わせて、ツリーは深くなります。

:::note info
listは、index順に数字で並べられます。数値がキーとなっている辞書型と区別できません。
:::

:::note warn
listの中に直接listという構造なっている場合、対応していません。
:::

:::note warn
定期的に更新したい場合には、TreeViewを一旦クリアして再度作り直す必要があり、今まで展開して表示していた部分は閉じてしまいます。工夫が必要になります。
select_event関数で、現在の選択アイテムを取得しているので、これを利用して、再展開させるなど工夫が必要です。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542103/59e150b3-9d5c-9dd9-8296-5227e1528d4f.png)