摘要: 通過使用Python編寫一個解析Json結構對比的小工具,來提煉編程求解的通用步驟和技巧。
難度: 初級
先上代碼。
jsondiff.py
#!/usr/bin/python #_*_encoding:utf-8_*_ import argparse import json import sys reload(sys) sys.setdefaultencoding('utf-8') def parseArgs(): description = 'This program is used to output the differences of keys of two json data.' parser = argparse.ArgumentParser(description=description) parser.add_argument('file', help='Given file containing two json data separated by a new line with three semicolons.') args = parser.parse_args() filename = args.file return filename def readFile(filename): content = '' f = open(filename) for line in f: content += line.strip("\n") f.close() return content def parseKeys(jsonobj): jsonkeys = list() addJsonKeys(jsonobj, jsonkeys, '') return jsonkeys def addJsonKeys(jsonobj, keys, prefix_key): if prefix_key != '': prefix_key = prefix_key+'.' if isinstance(jsonobj, list): addKeysIflist(jsonobj, keys, prefix_key) elif isinstance(jsonobj, dict): addKeysIfdict(jsonobj, keys, prefix_key) def addKeysIflist(jsonlist, keys, prefix_key): if len(jsonlist) > 0: addJsonKeys(jsonlist[0], keys, prefix_key) def addKeysIfdict(jsonobj, keys, prefix_key): for (key, value) in jsonobj.iteritems(): keys.append(prefix_key + key) addJsonKeys(value, keys, prefix_key+key) def diffKeys(json1, json2): keys1 = parseKeys(json1) keys2 = parseKeys(json2) keyset1 = set(keys1) keyset2 = set(keys2) return keyset1.difference(keyset2) def cmpArray(jsonArr1, jsonArr2, diff, prefix_key): ''' need to be improved ''' arrlen1 = len(jsonArr1) arrlen2 = len(jsonArr2) minlen = min(arrlen1, arrlen2) if arrlen1 != arrlen2: diff.append((prefix_key+'.length', arrlen1, arrlen2)) for i in range(0, minlen): diffDict(jsonArr1[i], jsonArr2[i], diff, prefix_key) def cmpPrimitive(key, value1, value2, diff, prefix_key): if isinstance(value1,list) or isinstance(value1, dict) \ or isinstance(value2, list) or isinstance(value2, dict): return if value1 != value2: diff.append((prefix_key + key, str(value1), str(value2))) def diffDict(json1, json2, diff, prefix_key): if prefix_key != '': prefix_key = prefix_key+'.' for (key, value) in json1.iteritems(): json2Value = json2.get(key) #print "key: ", key, ", value: ", value, " , value2: ", json2Value if json2Value == None: diff.append((prefix_key + key, value, None)) if isinstance(value, dict) and isinstance(json2Value, dict): diffDict(value, json2Value, diff, prefix_key + key) if isinstance(value, list) and isinstance(json2Value, list): cmpArray(value, json2Value, diff, prefix_key + key) cmpPrimitive(key, value, json2Value, diff, prefix_key) def diffJson(json1, json2): jsondiff = list() diffDict(json1, json2, jsondiff, '') return jsondiff def diffJsonToFile(filename, json1, json2): f_res = open(filename, 'w') diff_res = diffJson(json1, json2) for diff in diff_res: (key,v1,v2) = diff if v2 is None: f_res.write('key %s in json1 not in json2. \n' % key) else: f_res.write('key %s in json1 = %s yet in json2 = %s. \n' %(key, v1, v2)) f_res.close() def tesParsetKeysSingle(jsonobj, expected): assert set(parseKeys(jsonobj)) == set(expected) def testParseKeys(): for v in ({}, [], "good", 1, 3.14, -2.71, -1, 0.1, 2.71E3, 2.71E+3, 2.71E-32, 2.71e3, 2.71e+3, 2.71e-32, True, False, None, "null\n\\\"\/\b\f\n\r\t\u"): tesParsetKeysSingle(parseKeys(v), []) tesParsetKeysSingle({"code": 200}, ['code']) tesParsetKeysSingle({"code": 200, "msg": "ok", "list": [], "extra":{}}, ['code', 'msg', 'list', 'extra']) tesParsetKeysSingle({"code": 200, "msg": "ok", "list": [{"id": 20, "no":"115"}], "extra":{"size": 20, "info": {"owner": "qin"}}}, ['code', 'msg', 'list', 'list..id', 'list..no', 'extra', 'extra.size', 'extra.info', 'extra.info.owner']) tesParsetKeysSingle({'msg': 'ok', 'code': 200, 'list': [{'items': [{'price': 21, 'infos': {'feature': ''}, 'name': 'n1'}], 'id': 20, 'no': '1000020'}], 'metainfo': {'total': 20, 'info': {'owner': 'qinshu', 'parts': [{'count': 13, 'time': {'start': 1230002456, 'end': 234001234}}]}}}, ['msg', 'code', 'list', 'list..items', 'list..items..price', 'list..items..infos', 'list..items..infos.feature', 'list..items..name','list..id', 'list..no', 'metainfo', 'metainfo.total', 'metainfo.info', 'metainfo.info.owner', 'metainfo.info.parts', 'metainfo.info.parts..count', 'metainfo.info.parts..time' ,'metainfo.info.parts..time.start', 'metainfo.info.parts..time.end']) print 'testPassed' def test(): testParseKeys() if __name__ == "__main__": test() filename = parseArgs() content = readFile(filename) jsondataArr = content.split(';;;') content1 = jsondataArr[0] content2 = jsondataArr[1] json1 = json.loads(content1) json2 = json.loads(content2) print "keys in json_data_v2: " print parseKeys(json2) print 'keys in json_data_v1 yet not in json_data_v2: ' print diffKeys(json1, json2) print 'keys in json_data_v2 yet not in json_data_v1: ' print diffKeys(json2, json1)
Json 測試數據:
{ "code": 200, "msg": "ok", "list": [ { "id": 20, "no": "1000020", "items": [ { "name": "n1", "price": 21, "infos": { "feature": "" } } ] } ], "metainfo": { "total": 20, "info": { "owner": "qinshu", "parts": [ { "count": 13, "time": { "start": 1230002456, "end": 234001234 } } ] } } } ;;; { "code": 200, "msg": "ok", "success": true, "list": [ { "id": 22, "no": "1000020", "items": [ { "name": "n1", "price": 21, "comment": "very nice", "infos": { "feature": "" } }, { "name": "n2", "price": 22, "comment": "good", "infos": { "feature": "small" } } ] } ], "metainfo": { "total": 20, "info": { "owner": "qinshu", "parts": [ { "count": 15, "range": { "start": 1230003456, "end": 234007890 } } ] } } }
使用:
將要比較的兩個 json 串拷貝到一個文本文件 json_data.txt 里,並使用一個 ;;; 的行隔開; 然后運行 python jsondiff.py json_data.txt
目前主要是能夠比較 json 的結構, 即輸出 json 串相異的 key 的集合。
編程求解問題
確定問題與求解方向 -> 結構解析與遞歸 -> 算法設計 -> 編程與測試 -> 總結
確定問題與求解方向
首先確定一個合適的問題, 一個合適的求解方向。在 json 串對比的問題域中, 可以有兩個目標: 1. 比較兩個 json 串的結構的不同; 常常用於 API 變更后的兼容; 2. 比較兩個 json 串的結構及值的差異。 第二個目標由於有數組的存在,而變得比較復雜。 鑒於目標一比較常用,可以先實現。
結構解析與遞歸
其次,要確定處理問題所涉及的對象結構。要解析復雜的結構, 通常也會涉及到遞歸求解。可以使用遞歸求解的問題特征是: 1. 對象結構是一個組合結構,該結構可以通過一個原子量與一個更小的同型結構組合而成; 2. 問題的解結構可以通過原子量的解結構與更小規模的同型結構的解結構組合而成; 3. 原子量的解是可行的。
幾乎所有常用的數據結構都是遞歸的。一個數值可以分解為兩個數值之和; 一個字符串可以分解為一個字符與一個子字符串的連接;一個列表、鏈表、棧、隊列均可以由列首或列尾元素與剩余元素組合而成; 一棵樹可以通過根節點與其左右子樹組合而成;一個圖可以通過其分割的子圖構成。無處不可遞歸。不過遞歸需要注意的一點是: 在子問題的解結構組合成原問題的解結構的時候,最好不存在解結構之間的順序關系。也就是說,原問題的解結構是一個無序集合,只要子問題的解結構也是無序集合,那么就盡可以將子問題的解集合添加到原問題的解結構中;如果存在順序關系,則在算法設計中要尤其注意確保這種順序。
算法設計
理解了所要處理的結構,就可以進行算法設計了。 JSON 串有很明顯的遞歸特性, 因此適合用遞歸來求解。Json 結構定義參見 http://www.json.org/ 。對於 Json 串的處理,可以分為三種情況: (1) 原子量的處理,比如數值、字符串、布爾值; (2) 映射的處理,遍歷每個 key-value 對, 如果 value 是映射,那么就遞歸使用(2);如果 value 是數組,則使用 (3); 3. 列表的處理, 遍歷每一個元素,若元素是映射,則使用(2) 處理;若元素是數組,則使用(3)處理。具體見程序。
編程測試
設計好算法,就可以開始愉快地編程啦! 編程可以使用意圖導航編程, 首先編寫出幾個空函數, 表達對問題求解的步驟,然后完善每個函數,必要時修改其接口定義。 編程完成后需要使用覆蓋性的測試來盡早檢測出 bug , 修復程序隱藏的錯誤, 提高程序的質量。
話說,富有經驗的程序員會花費更多時間在算法設計上,確保其可擴展性和完善性;算法設計也是更考驗程序員的思維能力,無需電腦就可進行; 而編程則是一種更實際的樂趣。
一點技巧
在遞歸求解中, 如何構造最終的解結構是個問題。一個較簡單的辦法是,構造一個空列表,然后在遞歸的過程中,在空列表中添加子解。通常有一個主遞歸函數, 比如程序中的 addJsonKeys , 用於控制子結構的流程跳轉; 而處理子結構的分函數 addKeysIflist , addKeysIfdict 可遞歸跳轉到該主函數。在主遞歸函數最外層有一個調用者,用於設置主遞歸函數的初始值,比如空列表的解結構、其他的初始值。
總結記錄
總結與記錄也是必不可少的。回顧一下,在完成該問題的求解過程中,遇到了什么問題, 收獲了怎樣的技法呢? 無論多小都值得記錄,積微至著;尤其是一些不引人注意的"編程微思想"。其實只要是編程問題,核心總是"數據結構+算法"。 即使在應用編程中, 其實也是"數據結構+算法"的引申。"數據結構" 變成了應用中的 "數據庫+緩存", "算法" 變成了 "流程+規則",所做的需求開發,也就是在 "數據庫+緩存" 的數據背景下,設計和規划 "流程和規則", 以適應產品和業務的發展需求。