前段时间武哥安排了个任务:把结构动态的Json数据结构解析出来。所以要求无论嵌套了多少层,都要拿到最终节点,并且给特定的节点赋予规则,让这一类json数据对应节点进行对比时,遵循节点的规则。这个任务其实可以拆解为三个任务:
- 拿到这类json的标准结构描述,并且在节点上标记规则
- 将json数据层层解构拿到所有节点,然后拿着数据节点去标准结构json里找到对应的节点,然后读取规则
- 将节点和规则存储为字典,key要独一无二
这样就将整个json数据转为了一个无序的规则字典。而这个规则字典不仅key唯一,还要在value里存储值和规则。这个任务不能说太难,也不能说太简单。首先我采用了一个看似合理的方案,即对json数据进行反序列化,心里想着反序列化为字典后就好搞多了,**但问题是面对复杂的json结构,你得层层反序字典,最关键的是你根本不知道有多少层。**最后武哥说可以参照下java里的JNode,于是乎在网上搜了很多,终于让我找到一种解决方案,就是用JToken,也就是Java里的JNode。然后翻遍全网,发现没有说的特别仔细点 ,于是基本JToken所有的方法我都实现了一遍,这里详细记录下,希望对大家有帮助。当然,从这篇博客开始,我准备采用解决方案和日常学习分离的策略,所以解决方案或者最佳实践不会掺杂原理性的介绍,直接上实战,为什么这样做,文末会有答案.
依照上边三个抛出的问题,制定了这样一套解决方案:
- json的标准结构使用JsonSchema来搞定,通过在Shema这个json数据的结构标准化描述上添加规则,因为Schema是此类数据的最标准描述,无论结构如何嵌套,Schema都可以搞定
- json层层结构拿到所有节点需要使用递归加循环的算法,这样才能走到每一个节点
- 唯一的key可以使用JToken里的JPath来限定,限定好后,就可以生成规则字典,value使用两个属性的类,一个为规则,一个为值,通过key可以拿到节点一切相关信息。
准备工作
在真正的代码实现前,先对要用到的知识来个实战,这样实现代码的时候很方便,再强调一下,每一个方法都要有单元测试!单元测试很重要!可以节约大量的找bug时间
JsonSchema简单使用
简单来说就是描述Json数据的一种数据结构,本身也是Json结构。什么是JsonSchema,以及基本原理,我在自己的另一篇博客里有对类似的XML Schema的详细记录,如果想详细了解,传送门送上:
【XML学习 三】XML Schema原理及使用 https://blog.csdn.net/sinat_33087001/article/details/80890714
如果想了解的更加深入,这里有标准制定的文档,一并奉上:
Json Schema文档说明 http://json-schema.org/latest/json-schema-validation.html#rfc.section.3.2.1
还是那句话,这里只讲实战:
- 首先要生成Json Schema就要拿到标准的Json数据,设计方案前期,由于考虑到xml也有解析的需求,所以通过xml转json来使两种数据结构公用一套代码,那么乱序的json和xml首先就要格式化一下,直接用在线格式化:
xml转Json以及json格式标准化网址: https://www.json.cn/
- 其次就要对拿到的标准json数据进行Schema生成,这个也可以使用在线生成工具:
Json Schema自动生成工具: https://jsonschema.net/
- 最后可以校验生成的Schema是否标准(其实第二步生成的应该是标准的,这一步无需做):
Json Schema在线验证工具 https://jsonschemalint.com/#/version/draft-06/markup/json
做完了这些,一份Json Schema就生成成功了**(注意我会在Schema的description里添加Description属性,并且在里边写上自己的对比规则)**,我自己的项目里是存储到了数据库里面,为了方便说明和讲解,这里我放到txt里说明,并且为此写了一个txt的读取方法。
#region 文件读取类 /// <summary> /// 读取Schema文件 /// </summary> /// <param name="filePath"></param> /// <returns></returns> public string ReadFile(string filePath) { StreamReader sr = null; string json = ""; try { `这里写代码片` //一定要注意这里要用Encoding.Default限定编码格式,否则你的数据可能会乱码哦。 sr = new StreamReader(@filePath, Encoding.Default); string nextLine; while ((nextLine = sr.ReadLine()) != null) { json = json + nextLine.ToString(); } } catch (Exception ex) { logger.Error(ex + "文件读取失败"); } finally { sr.Close(); } return json; } #endregion 文件读取类
JToken的简单使用
这里展示JToken的常用方法,我认为解构一个Json解构这些操作基本就够了。
首先给出一个Json数据的样本格式:
{ "checked": false, "dimensions": { "width": 5, "height": 10 }, "variables": [ { "code": "id", "name": "TML", "dataType": "String", "defaultValue": "", "showFlag": "0", "showValue": "" }, { "code": "metaObjName", "name": "beijing", "defaultValue": "", "showValue": "", "dataType": "String", "showFlag": "0" }, { "code": "detailViewName", "name": "shagnhai ", "defaultValue": "", "showValue": "", "dataType": "String", "showFlag": "0" } ], "id": 1, "name": "A green door", "price": 12.5, "tags": [ "home", "green" ] }
然后给出Jtoken对该数据的解析操作:
#region Jtoken数据使用测试 [TestMethod] public void JsonTokenTest() { //一切的一切开始之前,需要把Json数据转换为JTken格式的数据 JToken jsonValueToken = JToken.Parse(jsr.ReadFile(filePath));//将json数据转换为JToken JToken first = jsonValueToken.First; //first为:{"checked": false},也就是第一个子节点 JToken last = jsonValueToken.Last;//last为:{"tags": ["home","green"]},也就是最后一个子节点 var jsonHaveChild = jsonValueToken.HasValues;//为true,表名当前节点并非叶子节点 JToken itemTages = jsonValueToken.SelectToken("tags");//{"tags": ["home","green"]},该方法的作用是依据传入的路径来获取节点的值,这个方法非常重要!!!,就是依靠它和唯一的路径我才能拿到值 var itemTagesType = itemTages.Type;//当前节点类型,目前已知有Array.object,int ,string,bool var itemTagesHaveChild = itemTages.HasValues; JToken items = jsonValueToken.SelectToken("variables"); var itemType = items.Type; var itemHaveChild = items.HasValues; var jpath = "variables[0].code"; //如果遇到数组,路径会加索引标记哦,这就是为什么虽然数组结构统一,我依然能有唯一的路径!! var enumNode = jsonValueToken.SelectToken(jpath);//通过路径获取一个实体 var enumType = enumNode.Type; var enumHaveChild = enumNode.HasValues; foreach (var item in items) { var path = item.Path; //路径为从根节点root开始一直到当前节点 var next = item.Next; //当前节点的兄弟节点,同级的 var parent = item.Parent; //当前节点的父节点 var lasts = item.Last; var root = item.Root; //当前节点的根节点 var type = item.Type; //当前节点的类型 var haveChild = item.HasValues; } var childs = jsonValueToken.Children(); foreach (var item in jsonValueToken) { var path = item.Path; var next = item.Next; var parent = item.Parent; var lasts = item.Last; var root = item.Root; var type = item.Type; var haveChild = item.HasValues; } } #endregion Jtoken数据使用测试
开始实战
掌握了上边两个利器,就可以开始实战了(以下过程都是基于json数据和jsonSchema数据都已经搞定并存储在txt): 最初的想法开始的想法是将Json Schema读取出来,然后生成一个key为路径,value为规则的字典,然后拿着路径到json数据中找到数据,最后达到生成<path,(rule,value)>d字典的目标
初始化Json Schema字典
代码清单如下:
#region 递归schema树获取所有规则和路径 public void SchemalevelTraverse(JToken json, ref Dictionary<string, string> jsonDic) { //如果没有属性节点了,说明已经是叶子节点了 if (json.SelectToken("properties") == null) { if (json.SelectToken("items") == null) { if (json.SelectToken("description") != null) //这里是我用于填充规则的 { string rule = json.Value<string>("description");//从Json里取出规则值 if (!jsonDic.ContainsKey(json.Path)) { jsonDic.Add(json.Path, rule); } } } else { var itemProperties = json.SelectToken("items").SelectToken("properties"); if (itemProperties != null) { foreach (var item in itemProperties) { if (item.First != null) { SchemalevelTraverse(item.First, ref jsonDic); } } } } return; } foreach (var item in json.SelectToken("properties")) //循环所有子节点 { if (item.First != null) { SchemalevelTraverse(item.First, ref jsonDic); //递归调用 } } } #endregion 递归schema树获取所有规则和路径