前段时间武哥安排了个任务:把结构动态的Json数据结构解析出来。所以要求无论嵌套了多少层,都要拿到最终节点,并且给特定的节点赋予规则,让这一类json数据对应节点进行对比时,遵循节点的规则。这个任务其实可以拆解为三个任务:

  1. 拿到这类json的标准结构描述,并且在节点上标记规则
  2. 将json数据层层解构拿到所有节点,然后拿着数据节点去标准结构json里找到对应的节点,然后读取规则
  3. 将节点和规则存储为字典,key要独一无二

这样就将整个json数据转为了一个无序的规则字典。而这个规则字典不仅key唯一,还要在value里存储值和规则。这个任务不能说太难,也不能说太简单。首先我采用了一个看似合理的方案,即对json数据进行反序列化,心里想着反序列化为字典后就好搞多了,**但问题是面对复杂的json结构,你得层层反序字典,最关键的是你根本不知道有多少层。**最后武哥说可以参照下java里的JNode,于是乎在网上搜了很多,终于让我找到一种解决方案,就是用JToken,也就是Java里的JNode。然后翻遍全网,发现没有说的特别仔细点 ,于是基本JToken所有的方法我都实现了一遍,这里详细记录下,希望对大家有帮助。当然,从这篇博客开始,我准备采用解决方案和日常学习分离的策略,所以解决方案或者最佳实践不会掺杂原理性的介绍,直接上实战,为什么这样做,文末会有答案.

依照上边三个抛出的问题,制定了这样一套解决方案:

  1. json的标准结构使用JsonSchema来搞定,通过在Shema这个json数据的结构标准化描述上添加规则,因为Schema是此类数据的最标准描述,无论结构如何嵌套,Schema都可以搞定
  2. json层层结构拿到所有节点需要使用递归加循环的算法,这样才能走到每一个节点
  3. 唯一的key可以使用JToken里的JPath来限定,限定好后,就可以生成规则字典,value使用两个属性的类,一个为规则,一个为值,通过key可以拿到节点一切相关信息。

准备工作

在真正的代码实现前,先对要用到的知识来个实战,这样实现代码的时候很方便,再强调一下,每一个方法都要有单元测试!单元测试很重要!可以节约大量的找bug时间

JsonSchema简单使用

简单来说就是描述Json数据的一种数据结构,本身也是Json结构。什么是JsonSchema,以及基本原理,我在自己的另一篇博客里有对类似的XML Schema的详细记录,如果想详细了解,传送门送上:

【XML学习 三】XML Schema原理及使用

如果想了解的更加深入,这里有标准制定的文档,一并奉上:

Json Schema文档说明 http://json-schema.org/latest/json-schema-validation.html#rfc.section.3.2.1

还是那句话,这里只讲实战:

  1. 首先要生成Json Schema就要拿到标准的Json数据,设计方案前期,由于考虑到xml也有解析的需求,所以通过xml转json来使两种数据结构公用一套代码,那么乱序的json和xml首先就要格式化一下,直接用在线格式化:

xml转Json以及json格式标准化网址: https://www.json.cn/

  1. 其次就要对拿到的标准json数据进行Schema生成,这个也可以使用在线生成工具:
Json Schema自动生成工具: https://jsonschema.net/

freemaker 动态解析sql 简书 如何动态解析json_Json动态解构


freemaker 动态解析sql 简书 如何动态解析json_数据校验_02

  1. 最后可以校验生成的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树获取所有规则和路径

转换路径

因为生成的路径多有Propertys和item,所以我将拿到的路径做处理,得到新的路径去Json数据里找值,生成路径代码如下所示:

#region 替换掉所有的properties

        public string GeValuePath(string oldPath)
        {
            var newPath = oldPath.Replace("properties.", "");  //处理所有properties
            var lastPath = newPath.Replace(".items", "[0]"); //这里是个大坑!!!!!
            return lastPath;
        }

        #endregion 替换掉所有的properties

没错,很快我就发现了问题,object类型还好,Array类型就麻烦了,由于JsonSchema只是描述Json结构的,所以它无需拥有每个数组的结构,也就没有每个数组的路径:看下JsonSchema长这样:

{
  "$id": "http://example.com/example.json",
  "type": "object",
  "properties": {
    "checked": {
      "$id": "/properties/checked",
      "type": "boolean",
      "title": "The Checked Schema ",
      "description": "Equal",
      "default": false,
      "examples": [
        false
      ]
    },
    "dimensions": {
      "$id": "/properties/dimensions",
      "type": "object",
      "properties": {
        "width": {
          "$id": "/properties/dimensions/properties/width",
          "type": "integer",
          "title": "The Width Schema ",
          "description": "Equal",
          "default": 0
        },
        "height": {
          "$id": "/properties/dimensions/properties/height",
          "type": "integer",
          "title": "The Height Schema ",
          "description": "Equal",
          "default": 0,
          "examples": [
            10
          ]
        }
      }
    },
    "variables": { 
      "$id": "/properties/variables",
      "type": "array",   //就是这里的问题,不管你有多少个元素,它只给你展示结构
      "items": { 
        "$id": "/properties/variables/items",
        "type": "object",
        "properties": {
          "code": {
            "$id": "/properties/variables/items/properties/code",
            "type": "string",
            "title": "The Code Schema ",
            "description": "Equal",
            "default": "",
            "examples": [
              "id"
            ]
          },
          "name": {
            "$id": "/properties/variables/items/properties/name",
            "type": "string",
            "title": "The Name Schema ",
            "description": "Equal",
            "default": "",
            "examples": [
              "业务实体数据Id"
            ]
          },
          "dataType": {
            "$id": "/properties/variables/items/properties/dataType",
            "type": "string",
            "title": "The Datatype Schema ",
            "default": "",
            "examples": [
              "String"
            ]
          },
          "defaultValue": {
            "$id": "/properties/variables/items/properties/defaultValue",
            "type": "string",
            "title": "The Defaultvalue Schema ",
            "description": "Equal",
            "default": "",
            "examples": [
              ""
            ]
          },
          "showFlag": {
            "$id": "/properties/variables/items/properties/showFlag",
            "type": "string",
            "title": "The Showflag Schema ",
            "description": "Equal",
            "default": "",
            "examples": [
              "0"
            ]
          },
          "showValue": {
            "$id": "/properties/variables/items/properties/showValue",
            "type": "string",
            "title": "The Showvalue Schema ",
            "description": "Equal",
            "default": "",
            "examples": [
              ""
            ]
          }
        }
      }
    },
    "id": {
      "$id": "/properties/id",
      "type": "integer",
      "title": "The Id Schema ",
      "description": "Mapping",
      "default": 0,
      "examples": [
        1
      ]
    },
    "name": {
      "$id": "/properties/name",
      "type": "string",
      "title": "The Name Schema ",
      "description": "Equal",
      "default": "",
      "examples": [
        "A green door"
      ]
    },
    "price": {
      "$id": "/properties/price",
      "type": "number",
      "title": "The Price Schema ",
      "description": "Equal",
      "default": 0,
      "examples": [
        12.5
      ]
    },
    "tags": {
      "$id": "/properties/tags",
      "type": "array",
      "items": {
        "$id": "/properties/tags/items",
        "type": "string",
        "title": "The 0th Schema ",
        "default": "",
        "examples": [
          "home",
          "green"
        ]
      }
    }
  }
}

由于结构使我拿不到其它数组元素的路径,我也就拿不到所有数组元素的节点的值,我想了很多,甚至递归判断共有多少个节点,和武哥讨论后,他给出了一个方案,就是拿大的装小的,现在json数据比schema大,那么可以先获取数据的节点路径和值,再将节点路径简化为schema的路径(也就是把各种索引去掉),这样就可以拿到规则。

最佳实践

采用大装小的战略后,事情解决起来很容易,事实证明,换个角度思考,可能问题就解决掉了。

初始化Json数据字典

初始化数据的<path,value>字典

#region 递归JsonValue树获取所有路径-值字典

        public void JsonValueTraverse(JToken json, ref Dictionary<string, string> jsonDic)
        {
            if (!json.HasValues)  //如果json没有子节点了,说明已经到了叶子节点,该完整路径为叶子节点path
            {
                var value = json.Value<object>().ToString();
                if (!jsonDic.ContainsKey(json.Path))  //如果已经存在了,则不需要重复添加
                {
                    jsonDic.Add(json.Path, value);
                }
                else {
                    logger.Error("添加了重复节点,节点路径为" + json.Path + "节点值为" + value);
                }
            }

            foreach (var item in json.Children())    
            {
                JsonValueTraverse(item, ref jsonDic);   //递归选取子节点
            }
        }

        #endregion 递归JsonValue树获取所有路径-值字典

转换为schema路径

将数据路径转换为Schema路径

#region 转换为schema路径

        public string GetRulePath(string oldPath)
        {
            var newPath = oldPath.Replace(".", ".properties.");  //处理所有properties
            var expr = @"\[\d+\]";   

            var lastPath = Regex.Replace(newPath, expr, ".items");   //用正则替换所有索引
            return lastPath;
        }

        #endregion 转换为schema路径

得到最终字典

代码清单:

#region 初始化路径-(值,规则)字典

        /// <summary>
        /// 初始化字典
        /// </summary>
        /// <param name="json"></param>
        /// <param name="schema"></param>
        /// <param name="type">0表示json数据,1表示xml数据</param>
        /// <returns></returns>
        public Dictionary<string, JsonSchemaObject> InitialJsonRule(string json, string schema, int type)
        {
            var jsonValue = JToken.Parse(json);
            var JsonSchema = JToken.Parse(schema);
            //初始化路径--规则字典
            Dictionary<string, string> jsonDic = new Dictionary<string, string>();
            JsonValueTraverse(jsonValue, ref jsonDic);
            Dictionary<string, JsonSchemaObject> ruleDic = new Dictionary<string, JsonSchemaObject>();
            //规范化字典名称
            foreach (var item in jsonDic)
            {
                JsonSchemaObject jso = new JsonSchemaObject();
                var schemaPath = GetRulePath(item.Key);  //将路径转为schemaPath去获取规则
                if (type == 1)
                {
                    schemaPath = GetStrAfterFirstPoint(schemaPath); //如果是xml类型的数据,还需要把路径头的名去掉
                }
                JToken node = JsonSchema.SelectToken(schemaPath);
                if (node != null)
                {
                    if (node.SelectToken("description") != null)
                    {
                        jso.rule = node.Value<string>("description");//从Json里取出规则值
                    }
                    else {
                        jso.rule = "Equal";
                    }
                    jso.value = item.Value;
                    ruleDic.Add(item.Key, jso);  //只存储有规则的字段,没有规则的不管,不比较,也不关心
                }
            }
            return ruleDic;
        }

        #endregion 初始化路径-(值,规则)字典

因为xml数据转为json前,需要带着最外层尖括号的名字,为了路径一致,这里我们去掉

#region xml转json需要替换头一个名

        public string GetStrAfterFirstPoint(string schema)
        {
            var index = schema.IndexOf(".");
            var newSchema = schema.Substring(index);
            return newSchema;
        }

        #endregion xml转json需要替换头一个名

附送一个xml转json的方法哦:

#region xml转为json

        public string XmlToJson(string xmlStr)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xmlStr);
            return Newtonsoft.Json.JsonConvert.SerializeXmlNode(doc);
        }

        #endregion xml转为json

思考过程

一开始想着遍历JsonSchema,拿到【schema路径,规则】字典,然后再通过schem路径遍历json数据获取路径的值,结果遇到数组的情况就不能取到正确的值了,这就是小装不下大
发现实现不了后通过遍历数据Json,拿到【数据路径,数据值】字典,然后将数据路径转为schema路径去拿规则,这个就是大可以缩小

第一步将Schema和Json数据都存储好之后,使用上述方法读取,要知道读取之后只是简单的字符串,在尝试了反序列化字典无果之后,突发奇想,如果把Json当成颗多叉树呢?,使用递归遍历的方式不就可以拿到所有节点了么?
做完之后有些反思:解决方案性学习和原理性学习是两回事儿,譬如CLR这类的可以追更溯源,而解决方案类问题不应将过多精力投入去从基础学习,应该自顶向下,先找到大致的解决方案,然后哪里不会再查哪里,这样效率高。就像Schema这个学习,之前学习虽说出了几篇博文,但对于实质问题的解决并没有帮助,反倒是直接搜索解决方案,之后对Schema的理解更深了。对JToken 的使用也加深了。