diffコマンドだとjsonの比較には微妙
いろいろな設定情報をjsonで保管することが多くなったので、これらjsonファイルの差分を出したいのだが、pprint等で整形したものを比較しても、一番深い層の差分だけが検出されてしまい、構造がよく分からない。
以下、pprintで整形出力したjsonデータをdiff/sdiffした結果。
一応はdiffできるのだが、どのような構造なのか一見して分からない。
(sdiffの方を見れば分かるように「"key15-1":"testdata15-1abc"」は"key15"以下のデータなのだが、diff結果からは、その構造が判断できない。)
sdiffの方であればなんとか構造を把握できるが、大量のデータのうち一部が異なっている場合にsdiffでチェックするのは大変。
ついでに整形時のカンマ(,)位置の都合上、差分では無いはずの"testdata19-6"が差分として検出されてしまう。
[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する
[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"の中身のリストは全く同じ要素だが、順番を並び替えている。ついでに重複データも含まれる。
[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等に整理するなら、この方が便利そうなので。
[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]#