Json逐次デシリアライズをつくった(Dictionary<string, object>限定)
目的
・デシリアライズ実行時に渡すStringが長すぎるとコケる(OutOfMemory)という悲しい事象に陥っていた。
・理由はわからないけど、長すぎると落ちるらしい。32Bitモードで実行されていることも起因している様子。
・とりあえず、まとめてデシリアライズせずに逐次で動かせばいけるんじゃね。ということで。
・どうして大量のパラメータをぶち込んでしまうんだ・・・(仕方なかったんだ)
git
https://github.com/yukiYamada/JsonStreamDeserializer
用途が限定的すぎて。僕以外つかわないような。
そもそも本当にstream開いたらメモリの節約になるのかも不明だ。ひどい。
Streamの処理
public Dictionary<string, object> Deserialize(string jsonString) { var ret = new Dictionary<string, object>(); var stream = new System.IO.MemoryStream(Encoding.GetEncoding("utf-8").GetBytes(jsonString)); using (var reader = new System.IO.StreamReader(stream)) { // 最初の{はいらない reader.ReadLine(); JContainer result = (JContainer)Deserialize(reader, null, null).First().Value; foreach(JProperty value in result.Values().Values()) { ret.Add(value.Name, value.Value); } } return ret; }
やっつけ感がわりとひどいのである。 StringをMemoryStreamにしてStreamReaderで・・・
とこれを書いている最中に気付く。GetBytesで結局おちるのでは。やりたいことができていない。
ネットを漁って直接StringをStreamにできないか見てみる
お世話になります。お世話になります。
実際、内部的なメモリの使用方法はわからないけど、たぶんこっちのほうがいいと思う。
ということで書きなおしたのが以下。
public Dictionary<string, object> Deserialize(string jsonString) { var ret = new Dictionary<string, object>(); using (var reader = new System.IO.StringReader(jsonString)) { // 最初の{はいらない reader.ReadLine(); JContainer result = (JContainer)Deserialize(reader, null, null).First().Value; foreach(JProperty value in result.Values().Values()) { ret.Add(value.Name, value.Value); } } return ret; }
テストかいててよかった。適当に変更してもとりあえずテストは通るんだから。
すばらしい。
コメントの「// 最初の{はいらない」は、jsonは
{
sumple: {
"key": "value",
"key2": "value2",
}
}
というような形式で来る。と、Dictionaryとしては
new Dictionary
{
{key:value},
{key2:value2}
};
ということに読み替えられる。
先頭の{。末尾の}はデシリアライズではいらないのだ。中身のペアが必要になってくる。
うちがわ。最初の呼び出し
private Dictionary<string, object> Deserialize(System.IO.StringReader reader, Newtonsoft.Json.Linq.JContainer parent, string parentType) { Dictionary<string, object> ret = new Dictionary<string, object>(); string header = string.Empty; if (parent == null) { header = "dummy" + ret.Count.ToString(); string jsonstring = "{" + header + ": { }}"; JContainer container = (JContainer)JsonConvert.DeserializeObject(jsonstring); ret.Add(header, container); Deserialize(reader, (JContainer)container.First().Last(), "{"); } //・・・省略・・・ while (reader.Peek() > -1) { string line = reader.ReadLine(); string replacedLine = line.Replace("\t", "").Replace(" ", "").Replace(",", ""); JToken jconvalue; if (line.IndexOf(":") != -1) { if (replacedLine.StartsWith("{") && replacedLine.EndsWith("}")) { jconvalue = (JObject)JsonConvert.DeserializeObject(line.Replace("\t", "").TrimEnd(',')); } else { jconvalue = ((JObject)JsonConvert.DeserializeObject("{" + line.Replace("\t", "").TrimEnd(',') + "}")).First; } } else { jconvalue = ((JObject)JsonConvert.DeserializeObject("{ \"dummy\" :[" + line.Replace("\t", "").TrimEnd(',') + "]}")).First.First.First; } parent.Add(jconvalue); }
再帰処理的な考慮が多いので、そのまま再帰させてやった。
parentがnullなのは最初の呼び出しだけ。あとは再帰的に呼び出す場合は先に親をつくって、
親のコンテナを渡すイメージになる。 省略の部分は再帰処理。後述する。
後半の塊で、parentにオブジェクトを追加している。
1行抜き取りでデシリアライズを実行するので、Jsonのフォーマットになっていないのでエラーになる。そこでJsonとしてのフォーマット合わせをしている。
もっとスマートにできるきがするが。
「line.indexOf(":")~」は 値の状態を判定している。Jsonの値の中身は
・{key1:value1, key2: value2 } (KeyPair)
・key1:value1 ({}が別行)
・[value1, value2] (配列)
・value1 ([]が別行)
の4パターン。のはず。詳しくはリファレンスを参照だが、子オブジェクトとしてうまいこと入れるにはそれぞれ別の考慮が必要になる。
それを分けている。
一番上は何も加工が必要なくてそのまま。
2番目は親改装がないので親改装を付与する(1版上の形になる)
3番目も親の階層がないので付与する。
そんな感じ。出来上がったJContainerも階層が微妙に違うので必要なところだけ取り出している。これ絶対おかしい。
ちょくでJPropertyとか作ってやるべきだと思う。
内側の再帰
if (line.EndsWith(": {") || line.EndsWith(": [")) { string newHeader = line.Split(':')[0].Replace("\t", "").Replace("\"", ""); string type = line.EndsWith(": {") ? "{" : "["; string newHeaderJson = "{" + newHeader + ": " + (type == "{" ? "{}" : "[]") + "}"; JContainer newContainer = (JContainer)JsonConvert.DeserializeObject(newHeaderJson); Deserialize(reader, (JContainer)newContainer.First().Last(), type); parent.Add(newContainer.First()); continue; } if (replacedLine == "{" || replacedLine == "[") { string newHeader = "dummy"; string type = replacedLine == "{" ? "{" : "["; string newHeaderJson = "{" + newHeader + ": " + (type == "{" ? "{}" : "[]") + "}"; JContainer newContainer = (JContainer)JsonConvert.DeserializeObject(newHeaderJson); Deserialize(reader, (JContainer)newContainer.First().Last(), type); parent.Add(newContainer.First().First); continue; }
新しい親階層を示す行が来た場合の処理。
すなわち「{」か「[」。Jsonではこれだけ相手にすればいい。
が、若干ちがって正確には以下の4ケース考慮しないといけなかった
・ key: {
・ key: [
・ {
・ [
上2つは基本的な形。 そのまま親としてContaierを作って、再帰的に自分を呼び出す。
かえってきた値を追加して完了。
下2つは上の親から再度指定される場合の形式
key: [
{
sub1: value1,
sub2: value2
},
{
sub1: value1,
sub2: value2
}
こんな形。{([)単独では親のコンテナとしてデシリアライズできないので、かりの名前を付与して再帰。
帰ってきた値も親のコンテナとして生成した分、余計な階層があるので一段落として格納している。
かんそう。
・Json難しい。書いてる最中にも何が正解のフォーマットなのかたびたびわからなくなった。
・テストまじ強い。とりあえずできることを細かくテスト書きながら作っていったが、
再帰の関係でちょっといじるとちょくちょくひとつ前のテストが動かなくなったりしていた。
それを早めにフィードバックを受けれるのがすごく助かった。間違ったままの実装で突き進む時間が非常に少なくなったと思う。
(合計で言えば3・4時間だと思う。すごく楽だった)
・本番でつかえるかは不明。不明あんど不明。
・はてなブログ。コード書けるのはいいんだけど行数表示できないかな。もうちょいコードの内側に注釈書くイメージで書きたいなぁ。
追記(11/19)
本番環境でつかったけどやっぱりめもりおーばーふろぅした。無駄なもんつくったなぁ・・・