Coding4Fun - 挑戰 JSON 體積縮小優化
| | 0 | | ![]() |
最近的 Coding4Fun 專案是練習用 Razor Pages 寫英文單字測驗網站,其中有個情境是每次測驗時可選擇涵蓋難度等級、是否只顯示打星號的單字、是否略過已記住的單字,最後產生一組單字清單當成題庫。 為了方便能用相同字庫重新測驗一次比較成績,系統要保留完整單字清單及每題選擇題選項確保測驗基準相同。
/// <summary>
/// 測驗設定
/// </summary>
public class TestSetting
{
/// <summary>
/// 涵蓋難度等級
/// </summary>
public int[] Levels { get; set; } = new int[] { 1, 2, 3, 4, 5, 6 };
/// <summary>
/// 是否只包含標記星號的單字
/// </summary>
public bool StarOnly { get; set; }
/// <summary>
/// 是否排除註記跳過的單字
/// </summary>
public bool Skip { get; set; }
/// <summary>
/// 字彙清單
/// </summary>
public short[] WordIds { get; set; }
/// <summary>
/// 每題單字選擇題的選項
/// </summary>
public Dictionary<short, short[]> Options { get; set; }
}
我計劃用 JSON 方式保存測試設定,若測驗範圍包含全部個單字(8,799個),JSON 大小將近 250KB,肥大了點,便興起讓它瘦身的念頭。
最先想到的方法是將 JSON ZIP 壓縮,於是我寫了以下的共用函式:
using Newtonsoft.Json;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace EngVoc7000.Model
{
public class ZipJsonHelper
{
/// <summary>
/// JSON序列化後並壓縮
/// </summary>
/// <param name="data">要JSON化的資料物件</param>
/// <returns>經ZIP壓縮的JSON資料</returns>
public static byte[] SerializeZipJson(object data)
{
var json = JsonConvert.SerializeObject(data);
using (var ms = new MemoryStream())
{
using (var zip = new DeflateStream(ms, CompressionMode.Compress))
{
var buff = Encoding.UTF8.GetBytes(json);
zip.Write(buff, 0, buff.Length);
}
return ms.ToArray();
}
}
/// <summary> /// <summary>
/// 測驗設定
/// </summary>
public class TestSetting
{
/// <summary>
/// 涵蓋難度等級
/// </summary>
public int[] Levels { get; set; } = new int[] { 1, 2, 3, 4, 5, 6 };
/// <summary>
/// 是否只包含標記星號的單字
/// </summary>
public bool StarOnly { get; set; }
/// <summary>
/// 是否排除註記跳過的單字
/// </summary>
public bool Skip { get; set; }
/// <summary>
/// 字彙清單
/// </summary>
public int[] WordIds { get; set; }
}
/// 反序列化經壓縮的JSON
/// </summary>
/// <typeparam name="T">反序列化還原型別</typeparam>
/// <param name="data">經ZIP壓縮的JSON資料</param>
/// <returns>還原後的物件</returns>
public static T DeserializeZipJson<T>(byte[] data)
{
using (var ms = new MemoryStream(data))
{
using (var zip = new DeflateStream(ms, CompressionMode.Decompress))
{
using (var msJson = new MemoryStream())
{
zip.CopyTo(msJson);
var json = Encoding.UTF8.GetString(msJson.ToArray());
return JsonConvert.DeserializeObject<T>(json);
}
}
}
}
}
}
弄個 8800 字的模擬資料測試一下:
var count = 8800;
var rnd = new Random(9527);
var setting = new TestSetting()
{
WordIds = Enumerable.Range(0, count).Select(o => (short)o).ToArray(),
Options = Enumerable.Range(0, count).ToDictionary(
o => (short)o,
o => Enumerable.Range(0, 3).Select(p => (short)rnd.Next(count)).ToArray())
};
var json = JsonConvert.SerializeObject(setting);
var zipJson = ZipJsonHelper.SerializeZipJson(setting);
Debug.WriteLine($"JSON size = {Encoding.UTF8.GetByteCount(json)}");
Debug.WriteLine($"Zip JSON size = {zipJson.Length}");
var res = ZipJsonHelper.DeserializeZipJson<TestSetting>(zipJson);
Debug.WriteLine($"Deserialization Check = {JsonConvert.SerializeObject(res)==json}");
效果不錯,資料從 250KB 縮到 105KB,使用上也很簡便。
JSON size = 249697 Zip JSON size = 105120 Deserialization Check = True
還能不能再縮小呢? 我決定繼續挑戰,連資料結構都納入優化範圍。short[] WordIds 與 Dictionary<short, short[]> 的 Keys 是重複的,宣告一個 Question 物件包含 short WordId 與 short[] Options,用單一 Question[] 陣列取代原本兩種資料。 至於資料儲存格式,我想將 Question[] 陣列用自訂格式轉成 byte[] 保存,除了每個數字可再減少 1-2 個 Byte(1-4 位數字 UTF8 原本 1-4 Bytes,轉 short 只要 2 Bytes),物件陣列純二進位格式,再省去大量「,」以及 [ ] 等符號。 程式要費點手腳,但難不倒我。
我將 Question[] Questions 標為 [JsonIgnored],其資料實際保存於 byte[] QuestionBin。 Question[] Questions 改為 get 時動態解析 QuestionBin 傳回 Question[] (有加上 Cache 概念避免重複解析), set 時則動態將 Question[] 依自訂邏輯轉成 byte[],並記錄 QuestionCount 供後續解析。 這種實際資料儲存格式與應用資料型別獨立處理的技巧蠻好用的,尤其是應用在將物件儲存資料庫時,例如:C# 端看到的是物件陣列,寫入資料表時自動轉成 JSON。 延伸閱讀:C# 小技巧:讓複雜型別屬性與資料庫欄位無縫接軌 二進位格式版程式如下:
public class Question
{
[JsonProperty("q")]
public short WordId { get; set; }
[JsonProperty("o")]
public short[] Options { get; set; }
}
/// <summary>
/// 測驗設定
/// </summary>
public class TestSetting
{
/// <summary>
/// 涵蓋難度等級
/// </summary>
public int[] Levels { get; set; } = new int[] { 1, 2, 3, 4, 5, 6 };
/// <summary>
/// 是否只包含標記星號的單字
/// </summary>
public bool StarOnly { get; set; }
/// <summary>
/// 是否排除註記跳過的單字
/// </summary>
public bool Skip { get; set; }
/// <summary>
/// 題目筆數
/// </summary>
public int QuestionCount { get; set; }
private Question[] _questions = null;
int optionCount = 0;
private IEnumerable<Question> ParseQuestionBin(byte[] data)
{
using (var ms = new MemoryStream(data))
{
using (var r = new BinaryReader(ms))
{
var wordCount = data.Length / 2 / (optionCount + 1);
for (var i = 0; i < wordCount; i++)
{
var wordId = r.ReadInt16();
var list = new List<short>();
for (var j = 0; j < optionCount; j++)
list.Add(r.ReadInt16());
yield return new Question() { WordId = wordId, Options = list.ToArray() };
}
}
}
}
/// <summary>
/// 每題單字選擇題的選項
/// </summary>
[JsonIgnore]
public Question[] Questions
{
get
{
if (QuestionBin != null)
{
if (_questions == null)
{
//前題:每個單字選項個數固定
if (QuestionBin.Length / 2 % QuestionCount != 0)
throw new ApplicationException("QuestionBin 長度與 QuestionCount 不吻合");
optionCount = (QuestionBin.Length / QuestionCount / 2) - 1;
_questions = ParseQuestionBin(QuestionBin).ToArray();
}
return _questions;
}
return new Question[] { };
}
set
{
if (value != null && value.Any())
{
using (var ms = new MemoryStream())
{
using (var w = new BinaryWriter(ms))
{
var optCountChk = -1;
value.ToList().ForEach(o =>
{
w.Write(o.WordId);
if (optCountChk == -1)
optCountChk = o.Options.Length;
else if (optCountChk != o.Options.Length)
throw new ArgumentException("選項長度不一致@" + o.WordId);
o.Options.ToList().ForEach(p => w.Write(p));
});
}
QuestionBin = ms.ToArray();
QuestionCount = value.Length;
}
}
else
{
QuestionBin = null;
}
_questions = null;
}
}
/// <summary>
/// 問題資料(自訂二進位格式儲存)
/// </summary>
public byte[] QuestionBin { get; set; }
}
產生模模擬資料的程式需要小改:
var setting = new TestSetting()
{
Questions = Enumerable.Range(0, count).Select(
o => new Question()
{
WordId = (short)o,
Options = Enumerable.Range(0, 3).Select(p => (short)rnd.Next(count)).ToArray()
}).ToArray()
};
試跑改良版,原始 JSON 大小由 250KB 縮小至 93KB,壓縮版由 105 KB 減少到 70KB。
JSON size = 93960 Zip JSON size = 69768 Deserialization Check = True
玩夠了,繼續上工。 😛
Sample code to reduce JSON size with DeflateStream compression. Further, using binary format reduce the size to half.
Comments
Be the first to post a comment