Unity 基于excel2json批处理读取Excel表并反序列化
excel2json是一款将Excel表格文件快速生成json和C#数据类的高效插件,详情了解如下:
https://neil3d.github.io/coding/excel2json.html
该插件有两种模式,分别是命令行和图像界面;当然了,为了更方便愉快的进行大规模转换,可以写两个批处理文件来执行:
Single文件表示执行单个选中文件,AutoAll表示执行该路径下所有xlsx文件;输出文件夹的位置为output,如果该目录下无output文件夹,则自动创建:
Single.bat详情如下:
1 @SET OUTPUT_FOLDER=.\output 2 @SET EXE=.\tool\excel2json.exe 3 4 if not exist %OUTPUT_FOLDER% md %OUTPUT_FOLDER% 5 6 @echo off 7 set path=%1 8 echo 源文件路径%path% 9 set name=%~n1 10 echo 源文件名%name% 11 set outputpath=%OUTPUT_FOLDER%\%name% 12 echo 输出文件路径%outputpath% 13 14 @CALL %EXE% --excel %path% --json %outputpath%.json --header 3 --csharp %outputpath%.cs -a 15 16 pause
前两行为参数设置,分别为输出文件夹路径和可执行文件路径,一个%表示参数,后面使用该参数作为变量时格式为[%参数%]。[.\]代表相对路径
第四行,如果不存在该路径文件夹则自动创建,注意如果没有这一行也没有对应参数所指示的路径,这时并不会自动创建路径而是会直接报错
第七行,得到当前选中的第一个文件路径作为参数
第九行,得到当前选中的第一个文件的文件名(不包含后缀)
类似的还有:
%~d1\ 得到当前选中的第一个文件所在磁盘符
%~dp1 得到当前选中的第一个文件路径位置(不包含文件名和文件后缀名)
%~nx1 得到当前选中的第一个文件的文件名和后缀
这里主要是为了保持输出文件的名称与选择的文件名称一致,所以最终的输出路径为设置的输出路径位置+源文件名
最后调用@CALL 执行参数对应路径下的exe文件,根据excel2json提供的命令行参数设置启动参数。
AutoAll.bat详情如下:
1 @SET EXCEL_FOLDER=.\ 2 @SET OUTPUT_FOLDER=.\output 3 @SET EXE=.\tool\excel2json.exe 4 5 @ECHO Converting excel files in folder %EXCEL_FOLDER% ... 6 if not exist %OUTPUT_FOLDER% md %OUTPUT_FOLDER% 7 8 for /f "delims=" %%i in ('dir /b /a-d /s %EXCEL_FOLDER%\*.xlsx') do ( 9 @echo processing %%~nxi 10 @CALL %EXE% --excel %EXCEL_FOLDER%\%%~nxi --json %OUTPUT_FOLDER%\%%~ni.json --header 3 --csharp %OUTPUT_FOLDER%\%%~ni.cs -a 11 ) 12 pause
上面这个批处理文件在帮助页面中有实例,最主要是做了一个路径内的文件查询和批量执行:
dir /b /a-d /s 从指定路径遍历搜索文件,路径参数即为当前路径下的所有.xlsx文件,当然了,也可以动态修改前面的excel所在文件夹参数配置
%%~nxi与%%~ni 和上面的类似只不过不是1而是循环体中的变量i,表示对应数目索引的文件
需要注意的是,在cmd模式下的循环变量是一个百分号加循环标识符(即%i)而在批处理文件中需要两个百分号才行(%%i)
下面提供已经写好批处理的文件下载链接:
https://files.cnblogs.com/files/koshio0219/excel2json.zip
这里的批处理统一将Execl导出为数组类型,方便在Unity中进一步反序列化,如果需要字典类型,可以继续修改或添加帮助中指定的参数,也可以直接利用图形界面分别导出
之所以默认导出数组类型,因为Unity默认的JsonUtility解析字典类型几乎是不可能,即使强行可以,那也是用的两个List做对应关系,
跟真正的字典类型导出的Json文件格式区别很大,直接解析出来就是个空文件。当然了,如果只是用于数据保存和读写是这么做是完全可以的,
只要读和写都是用的同一个序列化和反序列化的方式即可。但excel2json本身也并不是专门为了Unity的序列化而做的;
查看该工程的源代码就可以知道,该工程用的序列化方式为Newtonsoft.Json,如果实在需要用字典类型来解析,可以直接导入该Dll使用;
下面分别进行数组型Json与字典型Json的反序列化讨论:
1.数组型Json(或List型)
比如下面这段测试Json和C#文件:(通过excel2json导出)
1 [ 2 { 3 "ID": "4l523", 4 "Hp": 5, 5 "Atk": 6.3, 6 "Def": 7, 7 "State": "" 8 }, 9 { 10 "ID": "p6", 11 "Hp": 7, 12 "Atk": 8, 13 "Def": 2.3, 14 "State": "" 15 }, 16 { 17 "ID": 0.3, 18 "Hp": 0.2, 19 "Atk": "2.3,7", 20 "Def": 9, 21 "State": "" 22 } 23 ]
1 [System.Serializable] 2 public class Buff 3 { 4 public string ID; // 编号 5 public int Hp; // 血量 6 public float Atk; // 攻击 7 public float Def; // 防御 8 public BuffData State; // 状态 9 }
为了进行测试,我在Excel表格中故意填错一些与当前类型不匹配的数据,例如第三组中的ID,Hp,Atk,Def都设置得与当前的数据类型不同,且Atk一个表格中填了两个数字;
Unity解析数组(或List)Json文件也不能直接反序列化,例如直接写为:
1 var data = JsonUtility.FromJson<Buff[]>(json.text);
只会得到一个空的数据结构。
这里需要一个额外的序列化转换:
1 using UnityEngine; 2 3 public class JsonHelper 4 { 5 public static T[] GetJsonArray<T>(string json) 6 { 7 string newJson = "{ \"array\": " + json + "}"; 8 Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(newJson); 9 return wrapper.array; 10 } 11 12 [System.Serializable] 13 private class Wrapper<T> 14 { 15 public T[] array; 16 } 17 }
需要注意的是,如果以该方式反序列化数组,之前导出的Json文件不能包含文件名,在上面的脚本中统一将文件名添加为array。
newJson的文件名称必须与Wrapper类中的泛型数组T[]的名字保持一致,才能反序列化出指定数据。
如果不利用泛型的话,需要每一个文件单独再写一个类来进行反序列化,同样的数组的标识符必须与Json中的Array文件名称保持一致。
为了更方便的通过ID来读取数据,也可以将得到的数组再重新写入一个字典中,通过反射在获取ID的值作为键,前提是规定每一个Json文件中必须有ID这一字段:
1 public class JsonDatas<T> 2 { 3 public Dictionary<string, T> Dict = new Dictionary<string, T>(); 4 public static JsonDatas<T> FromJson(string json) 5 { 6 var re = new JsonDatas<T>(); 7 var datas = JsonHelper.GetJsonArray<T>(json); 8 foreach(var data in datas) 9 { 10 var info = data.GetType().GetField("ID"); 11 var idstr = info.GetValue(data).ToString(); 12 re.Dict.Add(idstr, data); 13 } 14 return re; 15 } 16 17 public T Get(string ID) 18 { 19 return Dict[ID]; 20 } 21 }
这里反射取字段值得时候遇到了一个坑,特意记录一下:
Type.GetField(string name) 这个是取字段的值,取不了属性
Type.GetProperty(string name) 这个是取属性的值,取不了字段
这两个取出来的内容是不一样的,请注意区分,不然半天也查不出错误出在哪里(说的就是我本人)
调试后的结果如下,能够成功解析出Json了:
这里特意来看看第三组数据为什么没有报错 ,神奇的是,JsonUtility竟然自动帮你转化为了对应的类型:
ID 0.3被转为了“0.300000”;Hp 0.2 变为了0;更震惊的是,Atk竟然也没有报错,而是成功解析出了逗号前面的数字,emm有点迷。
个人猜想是JsonUtility先尝试将错误的数据类型转为正确类型,如果无法转换,则从头开始读,读取到该类型下无法识别的字符就自动终止。(只是随便猜猜不用太当真)
2.字典型Json
如果非要导出字典型Json来反序列化,那就不能再用Unity自带的JsonUtility了,而是最好导入和序列化时用的是一样的Newtonsoft.Json
下面提供与Unity适配的Newtonsoft.Json包JsonNet.9.0.1.unitypackage下载地址:
https://files.cnblogs.com/files/koshio0219/JsonNet.9.0.1.zip
如果是反序列化单个不带任何签名的字典,只用一句话就可以了,不需要建立任何新类:
1 var data = JsonConvert.DeserializeObject<Dictionary<string, Buff>>(json.text);
试比较带签名和不带签名的Json:
1 { 2 "Buff": { 3 "4l523": { 4 "ID": "4l523", 5 "Hp": 5, 6 "Atk": 6.3, 7 "Def": 7, 8 "State": "" 9 }, 10 "p6": { 11 "ID": "p6", 12 "Hp": 7, 13 "Atk": 8, 14 "Def": 2.3, 15 "State": "" 16 }, 17 "0.3": { 18 "ID": 0.3, 19 "Hp": 2, 20 "Atk": 7, 21 "Def": 9, 22 "State": "" 23 } 24 } 25 }
View Code
1 { 2 "4l523": { 3 "ID": "4l523", 4 "Hp": 5, 5 "Atk": 6.3, 6 "Def": 7, 7 "State": "" 8 }, 9 "p6": { 10 "ID": "p6", 11 "Hp": 7, 12 "Atk": 8, 13 "Def": 2.3, 14 "State": "" 15 }, 16 "0.3": { 17 "ID": 0.3, 18 "Hp": 2, 19 "Atk": 7, 20 "Def": 9, 21 "State": "" 22 } 23 }
View Code
只要带有签名或者存在多个表单文件在同一个Json中,就只能重新建立新类并解析该新类了,新类中的变量顺序和标识符都必须与Json文件中的顺序与签名保持一致:
1 public class Buffs 2 { 3 //变量名称Buff必须与Json中的签名Buff一样 4 public Dictionary<string, Buff> Buff = new Dictionary<string, Buff>(); 5 }
叫人失落的是,Newtonsoft.Json并不会良心的帮你把错误的数据自动转换,而是直接给你抛出一个错误,这一点和JsonUtility不同。
补充:
一个有趣的实验——强行用Unity中的字典序列化方式来序列化Json文件会是怎样?
开始之前,我们要明白的是,Unity默认根本就没有给出任何字典序列化的方式,它只能蠢蠢的序列化List或者Array,但这并不能阻止我们,我们可以讨巧的利用ISerializationCallbackReceiver接口来实现一个伪序列化:
1 using UnityEngine; 2 using System; 3 using System.Collections.Generic; 4 5 // Dictionary<TKey, TValue> 6 [Serializable] 7 public class Serialization<TKey, TValue> : ISerializationCallbackReceiver 8 { 9 [SerializeField] 10 List<TKey> keys; 11 [SerializeField] 12 List<TValue> values; 13 14 Dictionary<TKey, TValue> target; 15 public Dictionary<TKey, TValue> ToDictionary() { return target; } 16 17 public Serialization(Dictionary<TKey, TValue> target) 18 { 19 this.target = target; 20 } 21 22 public void OnBeforeSerialize() 23 { 24 keys = new List<TKey>(target.Keys); 25 values = new List<TValue>(target.Values); 26 } 27 28 public void OnAfterDeserialize() 29 { 30 var count = Math.Min(keys.Count, values.Count); 31 target = new Dictionary<TKey, TValue>(count); 32 for (var i = 0; i < count; ++i) 33 { 34 target.Add(keys[i], values[i]); 35 } 36 } 37 }
把之前反序列化出的数据再用该伪序列化方式来序列化为Json文件:
1 var SerializedBuff= new Serialization<string, Buff>(new Dictionary<string, Buff>()); 2 var data = JsonConvert.DeserializeObject<Buffs>(json.text); 3 foreach(var item in data.Buff) 4 { 5 SerializedBuff.ToDictionary().Add(item.Key, item.Value); 6 } 7 var jsont = JsonUtility.ToJson(SerializedBuff); 8 Debug.Log(jsont);
实验结果如下:
1 { 2 "keys":[ 3 "4l523", 4 "p6", 5 "0.3"], 6 "values":[ 7 { 8 "ID":"4l523", 9 "Hp":5, 10 "Atk":6.300000190734863, 11 "Def":7.0, 12 "State":{ 13 } 14 }, 15 { 16 "ID":"p6", 17 "Hp":7, 18 "Atk":8.0, 19 "Def":2.299999952316284, 20 "State":{ 21 } 22 }, 23 { 24 "ID":"0.3", 25 "Hp":2, 26 "Atk":7.0, 27 "Def":9.0, 28 "State":{ 29 } 30 }] 31 }
我们发现它根本不是一个字典类型,序列化之后的结构和原来的结构相差非常大,实际上是Keys在一起Values在一起,只是它们的索引是相互对应的。