Python实现Json结构对比的小工具兼谈编程求解问题
摘要: 通过使用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 可递归跳转到该主函数。在主递归函数最外层有一个调用者,用于设置主递归函数的初始值,比如空列表的解结构、其他的初始值。
总结记录
总结与记录也是必不可少的。回顾一下,在完成该问题的求解过程中,遇到了什么问题, 收获了怎样的技法呢? 无论多小都值得记录,积微至著;尤其是一些不引人注意的"编程微思想"。其实只要是编程问题,核心总是"数据结构+算法"。 即使在应用编程中, 其实也是"数据结构+算法"的引申。"数据结构" 变成了应用中的 "数据库+缓存", "算法" 变成了 "流程+规则",所做的需求开发,也就是在 "数据库+缓存" 的数据背景下,设计和规划 "流程和规则", 以适应产品和业务的发展需求。