pythonで2つのjsonファイルを比較する


###diffコマンドだとjsonの比較には微妙

いろいろな設定情報をjsonで保管することが多くなったので、これらjsonファイルの差分を出したいのだが、pprint等で整形したものを比較しても、一番深い層の差分だけが検出されてしまい、構造がよく分からない。

以下、pprintで整形出力したjsonデータをdiff/sdiffした結果。
一応はdiffできるのだが、どのような構造なのか一見して分からない。
(sdiffの方を見れば分かるように「"key15-1":"testdata15-1abc"」は"key15"以下のデータなのだが、diff結果からは、その構造が判断できない。)

sdiffの方であればなんとか構造を把握できるが、大量のデータのうち一部が異なっている場合にsdiffでチェックするのは大変。
ついでに整形時のカンマ(,)位置の都合上、差分では無いはずの"testdata19-6"が差分として検出されてしまう。

```shell
[root]# diff test01.json test02.json
4c4
< "key03":"testdata03",
---
> "key03":"testdata03abc",
7c7
< "key06":3456789012,
---
> "key06":3456789012000,
10c10
< "key09":3.45678912,
---
> "key09":3.45678912001,
13c13
< "key12":true,
---
> "key12":false,
15c15
<   "testdata14-1",
---
>   "testdata14-1abc",
20c20
<     "key14-3a-3":"testdata14-3a-3"},
---
>     "key14-3a-3":"testdata14-3a-3abc"},
27c27
<   "key15-1":"testdata15-1",
---
>   "key15-1":"testdata15-1abc",
32c32
<     "key15-3a-3":"testdata15-3a-3"},
---
>     "key15-3a-3":"testdata15-3a-3abc"},
38,39c38
< "key17":1000000000,
< "key18":2000000000,
---
> "key16":"testdata16",
43,45d41
<   "testdata19-A01",
<   "testdata19-A02",
<   "testdata19-A03",
48c44,47
<   "testdata19-A04",
---
>   "testdata19-B01",
>   "testdata19-B02",
>   "testdata19-B03",
>   "testdata19-B04",
50,52c49
<   "testdata19-6",
<   "testdata19-A05",
<   "testdata19-A06"
---
>   "testdata19-6"
[root]# sdiff test01.json test02.json
{                                                               {
"key01":"testdata01",                                           "key01":"testdata01",
"key02":"testdata02",                                           "key02":"testdata02",
"key03":"testdata03",                                         | "key03":"testdata03abc",
"key04":1234567890,                                             "key04":1234567890,
"key05":2345678901,                                             "key05":2345678901,
"key06":3456789012,                                           | "key06":3456789012000,
"key07":1.23456789,                                             "key07":1.23456789,
"key08":2.34567891,                                             "key08":2.34567891,
"key09":3.45678912,                                           | "key09":3.45678912001,
"key10":true,                                                   "key10":true,
"key11":false,                                                  "key11":false,
"key12":true,                                                 | "key12":false,
"key14":[                                                       "key14":[
  "testdata14-1",                                             |   "testdata14-1abc",
  "testdata14-2",                                                 "testdata14-2",
  {"key14-3a":{                                                   {"key14-3a":{
    "key14-3a-1":"testdata14-3a-1",                                 "key14-3a-1":"testdata14-3a-1",
    "key14-3a-2":"testdata14-3a-2",                                 "key14-3a-2":"testdata14-3a-2",
    "key14-3a-3":"testdata14-3a-3"},                          |     "key14-3a-3":"testdata14-3a-3abc"},
  "key14-3b":{                                                    "key14-3b":{
    "key14-3b-1":"testdata14-3b-1",                                 "key14-3b-1":"testdata14-3b-1",
    "key14-3b-2":"testdata14-3b-2",                                 "key14-3b-2":"testdata14-3b-2",
    "key14-3b-3":"testdata14-3b-3"}}                                "key14-3b-3":"testdata14-3b-3"}}
],                                                              ],
"key15":{                                                       "key15":{
  "key15-1":"testdata15-1",                                   |   "key15-1":"testdata15-1abc",
  "key15-2":"testdata15-2",                                       "key15-2":"testdata15-2",
  "key15-3":{"key15-3a":{                                         "key15-3":{"key15-3a":{
    "key15-3a-1":"testdata15-3a-1",                                 "key15-3a-1":"testdata15-3a-1",
    "key15-3a-2":"testdata15-3a-2",                                 "key15-3a-2":"testdata15-3a-2",
    "key15-3a-3":"testdata15-3a-3"},                          |     "key15-3a-3":"testdata15-3a-3abc"},
   "key15-3b":{                                                    "key15-3b":{
    "key15-3b-1":"testdata15-3b-1",                                 "key15-3b-1":"testdata15-3b-1",
    "key15-3b-2":"testdata15-3b-2",                                 "key15-3b-2":"testdata15-3b-2",
    "key15-3b-3":"testdata15-3b-3"}}                                "key15-3b-3":"testdata15-3b-3"}}
},                                                              },
"key17":1000000000,                                           | "key16":"testdata16",
"key18":2000000000,                                           <
"key19":[                                                       "key19":[
  "testdata19-1",                                                 "testdata19-1",
  "testdata19-2",                                                 "testdata19-2",
  "testdata19-A01",                                           <
  "testdata19-A02",                                           <
  "testdata19-A03",                                           <
  "testdata19-3",                                                 "testdata19-3",
  "testdata19-4",                                                 "testdata19-4",
  "testdata19-A04",                                           |   "testdata19-B01",
                                                              >   "testdata19-B02",
                                                              >   "testdata19-B03",
                                                              >   "testdata19-B04",
  "testdata19-5",                                                 "testdata19-5",
  "testdata19-6",                                             |   "testdata19-6"
  "testdata19-A05",                                           <
  "testdata19-A06"                                            <
]                                                               ]
}                                                               }

[root]#

```

上記のような差異から、構造が分かるように差分を抜き出す方法を考える。

調べてみたところjson patchを使って、一方のjsonからもう一方のjsonへの差分をpatchとして生成するようなものもあるが、もっとシンプルに変更差分を格納したlistで出力させたい。


###jsonファイルの前提と比較方法

自分が作っているjsonファイルは以下のルールで作成しているので、ひとまずそれを前提に考える。
①json内には、辞書型(Object)、リスト(Array)、int、String、float、bool値のいずれか型のデータのみ収納している
②辞書型の特定のキー内には、必ず同じ型のデータを格納している
（同じキーに格納されているデータが、片方のファイルではint、もう片方ではstrということは無い）
③リストの中身は、その順番についても意味のあるデータとして扱う
（たとえば["sda","sdb","sdc","sdd"]と["sdc","sdb","sdd","sda"]の2つは、入っているデータは同じものだが順番が異なるため別のデータと考える）


これを以下の方法で比較してみる。
・辞書型のデータは再帰的に潜って比較。辞書型以外にぶつかるまで潜り続ける。
　変更差分が、どのようなキーを潜った先のデータなのか分かるように出力する
・int,str,float,bool値の場合は単純に比較
・リスト型は比較前にsortせず、再帰的にも潜らない。
　一番浅いリスト型の各要素を比較し、その差分を出力する
　（リスト内リストの一部に差異がある場合とか、何を元に「一部が異なるだけの同じデータ」と判断するか、汎用化しようとすると頭痛くなるので。）


###差分を出力するスクリプト

2つのjsonファイルのファイル名を引数として、比較結果を出力するスクリプトを作成する。
スクリプトから実行する場合、3つ目の引数として「print」と入れると、標準出力に差分をpprintする

```py
[root]# cat json_diff.py
import sys , json , pprint

def list_diff(list1,list2):
  offset=0 #比較している場所のズレ。片方の途中にデータ挿入されていると、比較先のアドレスがズレるので
  skip=0 #list2をforで回すが、list2の次の行がすでに差分と分かっている場合はskipする
  diff_list1=[]  #list1側のみに存在する行、list2側のみに存在する行を差分としてそれぞれ格納
  diff_list2=[]
  #print(list1,list2)
  for count in range(len(list2)):
    #print("count=",count,"  offset=",offset,"   skip=",skip)
    if skip > 0:
      skip=skip-1
      continue
    if len(list1)-1 < count+offset:  #list2は要素が残っているがlist1が終了
      diff_list2=diff_list2+list2[count:]  #list2の残りの要素を差分とする
      break
    if list2[count] == list1[count+offset]:
      continue
    for check_offset in range(1,max(len(list1),len(list2))-count-offset):  # 差異がある場合、それぞれの値と、相方のより後ろの値とを比較
      try:
        if list2[count+check_offset] == list1[count+offset]:   # list2のcheck_offset後と一致した場合
          diff_list2=diff_list2+list2[count:count+check_offset]
          offset=offset-check_offset
          skip=check_offset
          break
      except:pass
      try:
        if list2[count] == list1[count+offset+check_offset]:   # list1のcheck_offset後と一致した場合
          diff_list1=diff_list1+list1[count+offset:count+offset+check_offset]
          offset=offset+check_offset
          break
      except:pass
    else: #breakされなかった場合:list1[count+offset],list2[count]のいずれも、ほかのlist1,list2の値と一致しなかった場合
      diff_list1=diff_list1+[list1[count+offset]]
      diff_list2=diff_list2+[list2[count]]

  return {">":diff_list1+list1[len(list2)+offset:],"<":diff_list2}


def dict_diff(dict1,dict2):
  outdata={}
  for ele1 in dict2:
    if ele1 not in dict1:  #dict2側に存在するがdict1側に存在しないデータ(key)
      if "<" not in outdata : outdata["<"]={}
      outdata["<"].update({ele1:dict2[ele1]})
    elif dict2[ele1] == dict1[ele1]:
      pass
    elif type(dict2[ele1]) is str or type(dict2[ele1]) is int or type(dict2[ele1]) is float or type(dict2[ele1]) is bool:
      outdata[ele1]={"<":dict2[ele1],">":dict1[ele1]}
    elif type(dict2[ele1]) is list:
      outdata[ele1]=list_diff(dict1[ele1],dict2[ele1])
    elif (type(dict2[ele1]) is dict) and (dict_diff(dict1[ele1],dict2[ele1]) != []): #dict_diffを2回実行は無駄かな？
      outdata[ele1]=dict_diff(dict1[ele1],dict2[ele1])
    else:
      outdata[ele1]="#####type error"

  for ele2 in dict1:  #dict1側に存在するがdict2側に存在しないデータ(key)を抽出
    if ele2 not in dict2:
      if ">" not in outdata : outdata[">"]={}
      outdata[">"].update({ele2:dict1[ele2]})

  return outdata


def json_diff(file1,file2):

  jsondata1 = json.load(open(file1, 'r'))
  jsondata2 = json.load(open(file2, 'r'))

  return dict_diff(jsondata1,jsondata2)


def main(file1="",file2=""):
  args = sys.argv
  if len(args) != 1 and len(args) != 3 and len(args) != 4: print("引数エラー")  #コマンドから実行した場合
  elif len(args) == 3 or len(args) == 4 :
    file1=args[1]
    file2=args[2]

  elif file1=="" or file2=="":
    print("2つの引数が必要です")  #モジュールとして呼び出した場合

  if len(args) == 4 and args[3]=="print":  #3番目の引数に"print"を指定すると、returnする前に標準出力にpprintする
    pprint.pprint(json_diff(file1,file2))
  return json_diff(file1,file2)

if __name__=='__main__':
    main()
[root]#
```
※2019/7/23 片方のjsonにしか存在しないキーに対する差分情報が、紛らわしい出力だったため修正

main()は、下記例のようにインタプリタから直接呼び出す場合用。
importして外部から呼び出す場合は、json_diff.json_diff(file1,file2)の方を使った方が良い。(使い方次第ではsys.argvが悪さするので)

上記スクリプトにより、先のjsonを比較してみた結果は以下。

```
[root]# python3.6 ./json_diff.py ./test01.json ./test02.json print
{'<': {'key16': 'testdata16'},
 '>': {'key17': 1000000000, 'key18': 2000000000},
 'key03': {'<': 'testdata03abc', '>': 'testdata03'},
 'key06': {'<': 3456789012000, '>': 3456789012},
 'key09': {'<': 3.45678912001, '>': 3.45678912},
 'key12': {'<': False, '>': True},
 'key14': {'<': ['testdata14-1abc',
                 {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                               'key14-3a-2': 'testdata14-3a-2',
                               'key14-3a-3': 'testdata14-3a-3abc'},
                  'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                               'key14-3b-2': 'testdata14-3b-2',
                               'key14-3b-3': 'testdata14-3b-3'}}],
           '>': ['testdata14-1',
                 {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                               'key14-3a-2': 'testdata14-3a-2',
                               'key14-3a-3': 'testdata14-3a-3'},
                  'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                               'key14-3b-2': 'testdata14-3b-2',
                               'key14-3b-3': 'testdata14-3b-3'}}]},
 'key15': {'key15-1': {'<': 'testdata15-1abc', '>': 'testdata15-1'},
           'key15-3': {'key15-3a': {'key15-3a-3': {'<': 'testdata15-3a-3abc',
                                                   '>': 'testdata15-3a-3'}}}},
 'key19': {'<': ['testdata19-B01',
                 'testdata19-B02',
                 'testdata19-B03',
                 'testdata19-B04'],
           '>': ['testdata19-A01',
                 'testdata19-A02',
                 'testdata19-A03',
                 'testdata19-A04',
                 'testdata19-A05',
                 'testdata19-A06']}}
[root]#
```

変更差分がどのキーを潜った場所なのか、なんとなく分かるようになった。
「<」と「>」とで、どちらのjsonファイルにのみ存在するデータ差分なのか分かるようにした。
同じキーの中身が違う場合は{'キー': {'<': 'file1のデータ', '>': 'file2のデータ'}}のように出力
片方のファイルにしか存在しないキーについては{'キー': {'<': 'file1のみ存在するデータ'}}のように出力
リスト型を再帰的に潜らない仕様にしたため、key14のデータなどが、最上位のlistの要素単位でまとめて差分になっているが妥協する。

###仕様外動作確認

以下の前提を外したデータに対して動作を確認する
>②辞書型の特定のキー内には、必ず同じ型のデータを格納している
>（同じキーに格納されているデータが、片方のファイルではint、もう片方ではstrということは無い）
>③リストの中身は、その順番についても意味のあるデータとして扱う。
>（たとえば["sda","sdb","sdc","sdd"]と["sdc","sdb","sdd","sda"]の2つは、入っているデータは同じものだが順番が異なるため別のデータと考える）

以下、の2つのjsonを比較してみる。
"key03"の中身はデータ型をflotとstrとで変えている。
"key04"の中身のリストは全く同じ要素だが、順番を並び替えている。ついでに重複データも含まれる。

```shell
[root]# sdiff test03.json test04.json
{                                                               {
"key01":"testdata01",                                           "key01":"testdata01",
"key02":1234567890,                                             "key02":1234567890,
"key03":1.23456789,                                           | "key03":"hogemoga",
"key04":[                                                       "key04":[
  1024,                                                       <
  "testdata4-1",                                              <
  "testdata4-1",                                                  "testdata4-1",
                                                              >   ["testdat4-3-1","testdat4-3-2"],
  "testdata4-2",                                                  "testdata4-2",
                                                              >   1024,
                                                              >   ["testdat4-4-1","testdat4-4-2"],
  "testdata4-2",                                                  "testdata4-2",
  ["testdat4-3-1","testdat4-3-2"],                            |   "testdata4-1"
  ["testdat4-4-1","testdat4-4-2"]                             <
]                                                               ]
}                                                               }

[root]#
```

これを先のスクリプトで比較すると

```
[root]# python3.6 ./json_diff.py ./test03.json ./test04.json print
{'key03': {'<': 'hogemoga', '>': 1.23456789},
 'key04': {'<': ['testdata4-2',
                 1024,
                 ['testdat4-4-1', 'testdat4-4-2'],
                 'testdata4-2',
                 'testdata4-1'],
           '>': [1024,
                 'testdata4-1',
                 'testdata4-2',
                 'testdata4-2',
                 ['testdat4-4-1', 'testdat4-4-2']]}}
[root]#
```

"key03"は別の型でも比較できている様子。値の比較前にtype()=type()で型チェックする必要があるかと思ったが、別の型のデータを==で比較してもエラーにはならないのか。

"key04"は、test04.jsonの1つ目の'testdata4-1'が、test03.jsonの2つ目の値と一致。
次にtest04.jsonの2つ目の["testdat4-3-1","testdat4-3-2"]が、test03.jsonの6番目の値と一致。
まで見た時点で全データ比較が完了し、残りが差分として出力された。
仕様どおりではあるが、これは分かりづらい。
今回の例のような、listの中身の順番が整列していないデータには、この比較方法は向いていない。
リストの中身の順番が意味を持たないなら、list_diff()を呼び出す前にlistをsortしておけば良さそう。

###判明している問題点

先のlistの問題以外に思いつくところとして、

キー"<",">"の辞書型として差異を出力しているが、仮に"<",">"といったキーの辞書データを含むjsonファイルの場合、出力が読み取りづらくなってしまう。

今回のスクリプトを使う場合、"<",">"キーが現れた時点で、そこから下は差分を表すものとして解釈することになるため、辞書を潜る"<",">"キーが存在すると読み込み時にバグを引き起こすことになる。


###差分の出力を表形式に整形するスクリプト

先のスクリプトの出力を使って、さらに以下の形式で整形する。

[
[差分位置(辞書を潜ったキーのリスト) , jsonfile1側のデータ , jsonfile2側のデータ ],
[差分位置(辞書を潜ったキーのリスト) , jsonfile1側のデータ , jsonfile2側のデータ ],
[差分位置(辞書を潜ったキーのリスト) , jsonfile1側のデータ , jsonfile2側のデータ ],
・
・
・
]

後でexcel等に整理するなら、この方が便利そうなので。

```python
[root]# cat create_diff_table.py
import sys
import json_diff
import pprint

def create_diff_table(file1="",file2=""):

  difflist=[]
  def dict_check(diffs,keylist):
    #print("debug:",diffs,"  keylist:",keylist)
    if "<" in diffs and ">" in diffs:
      difflist.append([keylist,diffs[">"],diffs["<"]])
    elif "<" in diffs:
      difflist.append([keylist,"",diffs["<"]])
    elif ">" in diffs:
      difflist.append([keylist,diffs[">"],""])
    for diff in diffs:
      if diff == "<" or diff == ">": continue
      if type(diffs[diff]) is dict: dict_check(diffs[diff],keylist+[diff])

  #pprint.pprint(json_diff.main(file1,file2))
  dict_check(json_diff.json_diff(file1,file2),[])

  return difflist

def main(file1="",file2=""):

  args = sys.argv
  if len(args) != 1 and len(args) != 3 and len(args) != 4: print("引数エラー")  #コマンドから実行した場合
  elif len(args) == 3 or len(args) == 4 :
    file1=args[1]
    file2=args[2]
  elif file1=="" or file2=="":
    print("2つの引数が必要です")  #モジュールとして呼び出した場合

  if len(args) == 4 and args[3]=="print":  #3番目の引数に"print"を指定すると、returnする前に標準出力にpprintする
    pprint.pprint(create_diff_table(file1,file2))

  return create_diff_table(file1,file2)

if __name__=='__main__':
    main()
[root]#
```

これで、最初にsdiffした test01.jsonとtest02.jsonを比較すると

```
[root]# python3.6 ./create_diff_table.py ./test01.json ./test02.json print
[[[], {'key17': 1000000000, 'key18': 2000000000}, {'key16': 'testdata16'}],
 [['key03'], 'testdata03', 'testdata03abc'],
 [['key06'], 3456789012, 3456789012000],
 [['key09'], 3.45678912, 3.45678912001],
 [['key12'], True, False],
 [['key14'],
  ['testdata14-1',
   {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                 'key14-3a-2': 'testdata14-3a-2',
                 'key14-3a-3': 'testdata14-3a-3'},
    'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                 'key14-3b-2': 'testdata14-3b-2',
                 'key14-3b-3': 'testdata14-3b-3'}}],
  ['testdata14-1abc',
   {'key14-3a': {'key14-3a-1': 'testdata14-3a-1',
                 'key14-3a-2': 'testdata14-3a-2',
                 'key14-3a-3': 'testdata14-3a-3abc'},
    'key14-3b': {'key14-3b-1': 'testdata14-3b-1',
                 'key14-3b-2': 'testdata14-3b-2',
                 'key14-3b-3': 'testdata14-3b-3'}}]],
 [['key15', 'key15-1'], 'testdata15-1', 'testdata15-1abc'],
 [['key15', 'key15-3', 'key15-3a', 'key15-3a-3'],
  'testdata15-3a-3',
  'testdata15-3a-3abc'],
 [['key19'],
  ['testdata19-A01',
   'testdata19-A02',
   'testdata19-A03',
   'testdata19-A04',
   'testdata19-A05',
   'testdata19-A06'],
  ['testdata19-B01', 'testdata19-B02', 'testdata19-B03', 'testdata19-B04']]]
[root]#
```

'testdata15-3a-3'と'testdata15-3a-3abc'との差異が、'key15'→'key15-3'→'key15-3a'→'key15-3a-3'の順に深く潜った先の差分であることが分かる。

単純なリスト形式になったので、たとえば以下のようなスクリプトで簡単にhtmlに整形できる

```
[root]# cat ./create_diff_html.py
import sys
import create_diff_table


def main(file1="",file2="",file1_title="",file2_title=""):

  args = sys.argv
  file1=args[1]
  file2=args[2]
  file1_title=args[3]
  file2_title=args[4]

  with open('diff_test.html',mode='w') as f:
    f.write("<html><head><title>テスト08</title></head><body bgcolor=white>")

    f.write("<table border>")
    f.write("<tr><th>item</th><th>"+file1_title+"</th><th>"+file2_title+"</th></tr>")
    for diff in create_diff_table.create_diff_table(file1,file2):
      f.write("<tr><td>"+str(diff[0]).replace("'","").replace("[","").replace("]","").replace(","," -> ")+"</td><td>"+str(diff[1])+"</td><td>"+str(diff[2])+"</td></tr>")

    f.write("</table>")

    f.write("<br></body></html>")

if __name__=='__main__':
    main()
[root]#
[root]# python3.6 ./create_diff_html.py ./test01.json ./test02.json new old
[root]# cat diff_test.html
<html><head><title>テスト08</title></head><body bgcolor=white><table border><tr><th>item</th><th>new</th><th>old</th></tr><tr><td></td><td>{'key17': 1000000000, 'key18': 2000000000}</td><td>{'key16': 'testdata16'}</td></tr><tr><td>key03</td><td>testdata03</td><td>testdata03abc</td></tr><tr><td>key06</td><td>3456789012</td><td>3456789012000</td></tr><tr><td>key09</td><td>3.45678912</td><td>3.45678912001</td></tr><tr><td>key12</td><td>True</td><td>False</td></tr><tr><td>key14</td><td>['testdata14-1', {'key14-3a': {'key14-3a-1': 'testdata14-3a-1', 'key14-3a-2': 'testdata14-3a-2', 'key14-3a-3': 'testdata14-3a-3'}, 'key14-3b': {'key14-3b-1': 'testdata14-3b-1', 'key14-3b-2': 'testdata14-3b-2', 'key14-3b-3': 'testdata14-3b-3'}}]</td><td>['testdata14-1abc', {'key14-3a': {'key14-3a-1': 'testdata14-3a-1', 'key14-3a-2': 'testdata14-3a-2', 'key14-3a-3': 'testdata14-3a-3abc'}, 'key14-3b': {'key14-3b-1': 'testdata14-3b-1', 'key14-3b-2': 'testdata14-3b-2', 'key14-3b-3': 'testdata14-3b-3'}}]</td></tr><tr><td>key15 ->  key15-1</td><td>testdata15-1</td><td>testdata15-1abc</td></tr><tr><td>key15 ->  key15-3 ->  key15-3a ->  key15-3a-3</td><td>testdata15-3a-3</td><td>testdata15-3a-3abc</td></tr><tr><td>key19</td><td>['testdata19-A01', 'testdata19-A02', 'testdata19-A03', 'testdata19-A04', 'testdata19-A05', 'testdata19-A06']</td><td>['testdata19-B01', 'testdata19-B02', 'testdata19-B03', 'testdata19-B04']</td></tr></table><br></body></html>[root]#
```