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.exe3 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 -a15 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在一起,只是它們的索引是相互對應的。