はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pythonで実装したJSONのdiffトイスクリプト(自分コピペ用)

下記の記事と似た主旨のオレオレメモです。

itdepends.hateblo.jp

JSONファイルのdiffって有名ライブラリなどでは少しリッチすぎるかなという時に、機能が劣っていたり割り切りがあっても、自分で取り回しやすい自作のイディオムが欲しくなったのでコピペ用に書き起こしてみました。


■ツールの仕様・コンセプト

2つのJSON*1ファイルaとbを読み込んで、差異が出たプロパティを出力します。

JSONを比較したい時は、(Pretty形式の行単位の比較ではなく、)まずは差異が出ているプロパティ(ネストされていることが多い)がどこか知りたいよね、というところに注目した分析になっています。

具体的には、

x.y.zというノードで差異が出た場合、

{
   x.y.z : [
  ファイルaの当該プロパティの値,
       ファイルbの当該プロパティの値      
  ]
}

という、それ自体がJSONのレポートを吐き出します。

■jsondiff.py(Pythonです)

from itertools import zip_longest
import json

"""
JSONをdiffする関数
"""

diff = {}


def append(path, a, b):
    diff['.'.join(path)] = [a, b]


def d(a, b, path=[]):
    if type(a) is not type(b):
        append(path, a, b)
        return
    if type(a) is list:
        d4list(a, b, path)
        return
    if type(a) is not dict:
        if a != b:
            append(path, a, b)
        return
    for i in a.keys():
        if i in b.keys():
            d(a[i], b[i], path + [i])
        else:
            append(path + [i], a[i], None)

    for i in b.keys():
        if i not in a.keys():
            append(path + [i], None, b[i])


def make_pathsimbol(path, cnt):
    _path = path[:-2] + [path[-1] + '[' + str(cnt) + ']']
    return _path


def d4list(a, b, path):
    cnt = 0
    for i, j in zip_longest(a, b):
        d(i, j, make_pathsimbol(path, cnt))
        cnt += 1


def jsondiff(a, b):
    d(a, b)
    return diff


if __name__ == "__main__":

    a = {"a": 1, "b": 2, "c": {"d": 3, "x": "ああ"},
         "e": [{"f": 4}, {"g": 5}, {"g": 9, "y": [1, 2]}, {"z": "ない"}], "h": 3}

    b = {"a": 1, "b": 22, "c": {"d": 3, "x": "いい"},
         "e": [{"f": 4}, {"g": 55}, {"g": 99, "y": 9}], "h": "3", "w": "ないない"}

    print(json.dumps(jsondiff(a, b), ensure_ascii=False, indent=4, sort_keys=True))

■jsondiff.pyで食わせているインプットJSON(相当)の例(再掲)

ファイルa = {
    "a": 1,
    "b": 2,
    "c": { "d": 3, "x": "ああ" },
    "e": [
        { "f": 4 }, { "g": 5 }, { "g": 9, "y": [1, 2] }, { "z": "ない" }
    ],
    "h": 3
}

ファイルb = {
    "a": 1,
    "b": 22,
    "c": { "d": 3, "x": "いい" },
    "e": [
        { "f": 4 }, { "g": 55 }, { "g": 99, "y": 9 }
    ],
    "h": "3",
    "w": "ないない"
}

※プロパティbやプロパティc.xがdiff検知されてほしいという例になります。

■jsondiff.pyの実行結果

{
    "b": [
        2,
        22
    ],
    "c.x": [
        "ああ",
        "いい"
    ],
    "e[1].g": [
        5,
        55
    ],
    "e[2].g": [
        9,
        99
    ],
    "e[2].y": [
        [
            1,
            2
        ],
        9
    ],
    "e[3]": [
        {
            "z": "ない"
        },
        null
    ],
    "h": [
        3,
        "3"
    ],
    "w": [
        null,
        "ないない"
    ]
}

今回の目的の範囲ではうまく行っていると思いますが、コレは恥ずかしいッ!!系のバグがあるかもしれません。

ですが、「答案用紙を提出した後に閃めく理論」により、「公開する」と思わぬ不具合に自分で気づけるので、見込みでポストしちゃうこととしました。

*1:見ての通り、ここではエッセンスを示すため、JSONではなく、Pythonのdictで代用しています。実際にはこのコードに加えてjson.loads()などをうまく使うことが前提です。