前段时间武哥安排了个任务:把结构动态的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原理及使用
如果想了解的更加深入,这里有标准制定的文档,一并奉上:
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树获取所有规则和路径
转换路径
因为生成的路径多有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 的使用也加深了。