WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

JSON 文字列から指定した name の value を取得

by WebSurfer 11. February 2021 18:12

JSON 形式の文字列 {"name":"value"} から name を指定して value を取得する C# のプログラムのサンプルを書きます。

JSON 文字列から指定した name の value を取得

先の記事「Json.NET の JToken をパース」と「System.Text.Json の JsonElement をパース」で、Json.NET (Newtonsoft.Json) と System.Text.Json を使って JSON 文字列をパースするサンプルを書きました。

先の記事のサンプルの使い道はなさそうですが、その応用でいろいろできそうということで、まず JSON 文字列を JToken (Json.NET) または JsonElement (System.Text.Json) にデシリアライズしたオブジェクトから、name を指定して再帰的に value 値を取得するコードを書いてみました。

それが何の役に立つかというと、例えば JSON 文字列構造が不定なので特定のクラスにデシリアライズできないが、必要な情報のある name は事前に分かっているのでそれに該当する value を取得できれば目的は果たせるというようなケースです。

レアケースかもしれませんけど、ASP.NET Forum でそういう話があったので考えてみました。

JToken (Json.NET) および JsonElement (System.Text.Json) の両方の場合のサンプルコードを以下にアップしておきます。

(1) JToken (Json.NET)

Json.NET (Newtonsoft.Json) の DeserializeObject メソッドで JSON 文字列を JToken クラスのオブジェクトにデシリアライズし、その中から name が "ObjectArray" の value 値を FindJTokenByName メソッドで取得しています。

その結果を Visual Studio 2019 のデバッガで見たのが上の画像です。元になる JSON 文字列はこの記事の下の方に書いてありますのでそちらを見てください。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;

namespace NewtonsoftJson
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:\Users\...\NewtonsoftJson\";
            string file = "TextFile5.txt";
            string jsonText = "";
            using (StreamReader sr = File.OpenText(path + file))
            {
                jsonText = sr.ReadToEnd();
            }

            JToken jtoken = JsonConvert.DeserializeObject<JToken>(jsonText);
            Parse(0, jtoken);
            JToken token = FindJTokenByName(jtoken, "ObjectArray");
        }

        private static void Parse(int padding, JToken jtoken)
        {
            // ・・・略・・・
        }

        private static JToken FindJTokenByName(JToken jtoken, string name)
        {
            if (jtoken is JObject)
            {
                foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
                {
                    if (kvp.Key == name)
                    {
                        return kvp.Value;
                    }
                    else
                    {
                        JToken retVal = FindJTokenByName(kvp.Value, name);
                        if (retVal != null)
                        {
                            return retVal;
                        }
                    }
                }
            }
            else if (jtoken is JArray)
            {
                foreach (JToken jtokenInArray in (JArray)jtoken)
                {
                    JToken retVal = FindJTokenByName(jtokenInArray, name);
                    if (retVal != null)
                    {
                        return retVal;
                    }
                }
            }
            else
            {
                return null;
            }
            return null;
        }
    }
}

(2) JsonElement (System.Text.Json)

System.Text.Json の Deserialize メソッドで JSON 文字列を JsonElement 構造体のオブジェクトにデシリアライズし、その中から name が "ObjectArray" の value 値を FindElementByName メソッドで取得しています。

using System;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using System.IO;

namespace ConsoleAppJson
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:\Users\surfe...\ConsoleAppJson\";
            string file = "TextFile5.txt";
            string jsonText = "";

            using (StreamReader sr = File.OpenText(path + file))
            {
                jsonText = sr.ReadToEnd();
            }

            JsonElement jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonText);
            Parse(0, jsonElement);
            var jelem = FindElementByName(jsonElement, "ObjectArray");
        }

        private static void Parse(int padding, JsonElement jelem)
        {
            // ・・・略・・・
        }

        // JsonElement は構造体。見つからないと null が返ってくるので
        // 戻り値は JsonElement? として null 許容にした
        private static JsonElement? FindElementByName(JsonElement jelem, string name)
        {
            if (jelem.ValueKind == JsonValueKind.Object)
            {
                foreach (JsonProperty jprop in jelem.EnumerateObject())
                {
                    if (jprop.Name == name)
                    {
                        return jprop.Value;
                    }
                    else
                    {
                        JsonElement? retVal = FindElementByName(jprop.Value, name);
                        if (retVal != null)
                        {
                            return retVal;
                        }
                    }
                }
            }
            else if (jelem.ValueKind == JsonValueKind.Array)
            {
                foreach (JsonElement jelemInArray in jelem.EnumerateArray())
                {
                    JsonElement? retVal = FindElementByName(jelemInArray, name);
                    if (retVal != null)
                    {
                        return retVal;
                    }
                }
            }
            else
            {
                return null;
            }
            return null;
        }
    }
}

上のコードで使った TextFile5.txt の JSON 文字列は以下の通りです。

{
  "Title": "This is my title",
  "Response": {
    "Version": 1,
    "StatusCode": "OK",
    "Result": {
      "Profile": {
        "UserName": "SampleUser",  
        "IsMobileNumberVerified": false,
        "MobilePhoneNumber": null
      },
      "ObjectArray" : [
          {"Code": 2000,"Description": "Fail"},
          {"Code": 3000,"Description": "Success"}
      ],
      "lstEnrollment": "2021-2-5"
    },
    "Message": {
      "Code": 1000,      
      "Description": "OK"
    }
  },
  "StringArray" : ["abc", "def", "ghi"]
}

Tags: , ,

.NET Framework

Json.NET の JToken をパース

by WebSurfer 7. February 2021 15:04

Json.NET (Newtonsoft.Json) の JsonConvert.DeserializeObject メソッドを使って JSON 文字列を JToken 型のオブジェクトにデシリアライズし、それを以下のようにパースして表示する方法を書きます。

JToken をパース

使用するのは Newtonsoft.Json.Linq 名前空間にある JToken, JObject, JArray, JValue クラスですので、まずそれらの簡単な説明を以下に書きます。詳しい説明は Newtonsoft.Json.Linq Namespace を見てください。

  • JToken: 下のクラスの継承元の抽象クラス
  • JObject: {"name1":"value1","name2":"value2"} のようなオブジェクト。IDictionary<string, JToken> を継承
  • JArray: [{"name":"value1"},{"name":"value2"}, ...] のような配列。IList<JToken> を継承
  • JValue: string, number, boolean, null などのプリミティブな JSON 値

一般的な方法は DeserializeObject<T>(String) メソッドの T に先の記事「JSON 文字列から C# のクラス定義生成」のような方法で求めたクラス定義を設定して、そのクラスのオブジェクトにデシリアライズするのだと思います。

なので、この記事に書くような JToken クラスにデシリアライズしてそれをパースする必要はなさそうですが、ひょっとしたら JSON 文字列を受け取るまでその内容が不明でクラス定義が生成できないとかいうケースがあるかもしれないということで考えてみました。

そのコードを以下にアップしておきます。

DeserializeObject<JToken>(jsonText) で JSON 文字列を JToken 型のオブジェクトにデシリアライズします。その後 Parse メソッドで JToken オブジェクトを再帰的に解析し結果をコンソールに出力したのが上の画像です。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;

namespace NewtonsoftJson
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:\Users\...\NewtonsoftJson\";
            string file = "TextFile5.txt";

            string jsonText = "";

            file = "TextFile5.txt";
            using (StreamReader sr = File.OpenText(path + file))
            {
                jsonText = sr.ReadToEnd();
            }

            JToken jtoken = JsonConvert.DeserializeObject<JToken>(jsonText);
            Parse(0, jtoken);

        }

        private static void Parse(int padding, JToken jtoken)
        {
            if (jtoken is JValue)
            {
                // プリミティブ型の場合 Value プロパティで値を取得
                JValue jvalue = (JValue)jtoken;
                string str = $"value = {jvalue.Value}";
                Console.WriteLine(str.PadLeft(str.Length + padding));
            }
            else if (jtoken is JObject)
            {
                // JObject は IDictionary<string, JToken> を継承しているので、
                // 以下のように foreach ループで {"name":"value"} を取得できる
                foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
                {
                    if (kvp.Value is JValue)
                    {
                        JValue jvalue = (JValue)kvp.Value;
                        string str = $"name = {kvp.Key}, value = {jvalue.Value}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                    }
                    else if (kvp.Value is JObject)
                    {
                        string str = $"name = {kvp.Key}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                        Parse(padding + 2, kvp.Value);
                    }
                    else if (kvp.Value is JArray)
                    {
                        string str = $"name = {kvp.Key}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                        JArray jarray = (JArray)kvp.Value;
                        int index = 1;

                        // JArray は IList<JToken> を継承しているので、以下の
                        // ように foreach で配列の各要素の JToken を取得できる
                        foreach (JToken token in jarray)
                        {
                            string idx = $"array index {index}";
                            Console.WriteLine(idx.PadLeft(idx.Length + padding + 1));
                            Parse(padding + 2, token);
                            index++;
                        }
                    }
                }
            }
            else if (jtoken is JArray)
            {
                JArray jarray = (JArray)jtoken;
                int index = 1;
                foreach (JToken token in jarray)
                {
                    string str = $"array index {index}";
                    Console.WriteLine(str.PadLeft(str.Length + padding + 1));
                    Parse(padding + 2, token);
                    index++;
                }
            }
            else
            {
                // ここには来ないはずだが念のため
                Console.WriteLine(jtoken.ToString());
            }
        }
    }
}

ちなみに、上のコードで使った JSON 文字列は以下の通りです。他にもいろいろ試してみましたが、JSON 文字列としてはかなり無理目でクラス定義が生成できない場合も上のコードでパースは可能でした。ダメなケースもあるかもしれませんが・・・

{
  "Title": "This is my title",
  "Response": {
    "Version": 1,
    "StatusCode": "OK",
    "Result": {
      "Profile": {
        "UserName": "SampleUser",  
        "IsMobileNumberVerified": false,
        "MobilePhoneNumber": null
      },
      "ObjectArray" : [
          {"Code": 2000,"Description": "Fail"},
          {"Code": 3000,"Description": "Success"}
      ],
      "lstEnrollment": "2021-2-5"
    },
    "Message": {
      "Code": 1000,      
      "Description": "OK"
    }
  },
  "StringArray" : ["abc", "def", "ghi"]
}

Tags: , , , ,

.NET Framework

タスク並列ライブラリ (TPL)

by WebSurfer 27. December 2020 14:53

タスク並列ライブラリ (TPL) は Windows Forms のような GUI アプリでは使い難い点があるということを今更ながらですが学びましたので、備忘録として書いておきます。

タスク並列ライブラリの検証

最近の PC の CPU はマルチコアが当たり前になっているようですが、PC のマルチコアを有効に使うプログラミングをサポートするため、.NET Framework 4.0 で導入されたタスク並列ライブラリ (TPL・・・Parallel.For とか Parallel.Invoke とか) を利用するという話を巷でよく耳にします。

マルチスレッドの基本的な話ですが、@IT の記事「第1回 マルチスレッドはこんなときに使う (1/2)」の「マルチスレッドの動作原理」セクションを見てください。リンク切れになると困るので画像だけ借用して以下に表示しておきます。

マルチスレッドの比較

② のようになると OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅くなるということになります。なので、マルチスレッドアプリで処置時間の短縮を図るなら ③ のようにマルチコアを利用できる環境が必要で、さらにマルチコアを有効に利用するプログラミングを行うという話になると思います。

その際に自分がよく聞くのがタスク並列ライブラリ (TPL) や Parallel LINQ (PLINQ) を使うという話です。Microsoft のドキュメント「.NET での並列プログラミング」や、ググるとヒットする記事例えば @IT の記事「ループをParallelクラスで並列処理にするには?」を読むと TPL, PLINQ は並列処理には万能のような気がしてました。

Microsoft のドキュメント「タスク並列ライブラリ (TPL)」には、

"TPL は、使用可能なすべてのプロセッサを最も効率的に使用するように、コンカレンシーの程度を動的に拡大します。The TPL scales the degree of concurrency dynamically to most efficiently use all the processors that are available."

・・・と言う記述がありますし、PC のマルチコアを有効に使うという局面に限れば最強のように思えます。

しかし、Windows Forms のような GUI アプリではそうでもなさそうな感じです。自分が気が付いた限りですが、以下の 2 点が問題だと思いました。(自分が回避策を知らないだけという可能性は否定できませんが)

  1. 並列に実行される複数のメソッドがすべて完了するまで UI がブロックされる。
  2. 並列実行するメソッドには非同期版は使えない。

以下のサンプルコードの ParallelInvoke_Click メソッドは Parallel.Invoke を使って 5 つの同期版メソッド Work を並列に実行するものですが、5 つのメソッドがすべて完了するまで UI がブロックされるので、アプリはフリーズしたようになります。その他の TPL, PLINK を使った xxxxx_Click メソッドも同様で、完了するまで UI がブロックされます。それを回避する手段は、自分が探した限りですが、なさそうです。

さらに、非同期版メソッド WorkAsync は使えません。詳しくはサンプルコードのコメントに書きましたので見てください。なので、ライブラリなどで非同期版メソッドしか提供されてない場合は何ともならないと思われます。

というわけで、下のサンプルコードの WhenAll1_Click, WhenAll2_Click メソッドのように、Parallel.Invoke など使わないで、await Task.WhenAll(...) で待機するようにし、並列化については OS に任せるのが良さそうと思いました。(実際にサンプルを動かしてみると並列化してくれているような感じはしました。② のようになっている可能性は否定しきれませんが)

非同期版メソッド WorkAsync も使えます。下のサンプルコードの WhenAll2_Click メソッドを見てください。上の画像がその実行結果です。(ThreadID が 1 で UI スレッドと同じなのは、WorkAsync メソッドの中で ManagedThreadId を取得するのが UI スレッドだからです)

using System;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace WinFormsApp1
{
    public partial class TaskParallelLibrary : Form
    {
        public TaskParallelLibrary()
        {
            InitializeComponent();
        }

        // 同期版メソッド
        private string Work(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start:{DateTime.Now:ss.fff}, ";

            Thread.Sleep(3000);

            retunVlaue += $"end:{DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド
        private async Task<string> WorkAsync(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start: {DateTime.Now:ss.fff}, ";

            await Task.Delay(3000);

            retunVlaue += $"end: {DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド 5 つを for ループで逐次実行
        private async void InOrder_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            for (int n = 0; n < 5; n++)
            {
                this.label1.Text += await WorkAsync(n) + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを Parallel.Invoke で実行
        // 終了まで UI はブロックされる
        private void ParallelInvoke_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.Invoke(
                () => results[0] = Work(0),
                () => results[1] = Work(1),
                () => results[2] = Work(2),
                () => results[3] = Work(3),
                () => results[4] = Work(4));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";

            // 非同期版メソッド WorkAsync は使えない

            // 以下のようにすると Task.Result でデッドロックになる
            //Task<string> result1 = null, result2 = null;
            //Parallel.Invoke(
            //    () => result1 = WorkAsync(0),
            //    () => result2 = WorkAsync(1));
            //this.label1.Text += result1.Result + "\r\n";
            //this.label1.Text += result2.Result + "\r\n";

            // 以下のようにすると await で待つことなく終わってしまう
            //string result1 = "", result2 = "";
            //Parallel.Invoke(
            //    async () => result1 = await WorkAsync(0),
            //    async () => result2 = await WorkAsync(1));
            //this.label1.Text += result1 + "\r\n";
            //this.label1.Text += result2 + "\r\n";

        }

        // 非同期版メソッド 5 つを Parallel.For で実行
        // 終了まで UI はブロックされる
        private void ParallelFor_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.For(0, 5, n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを Parallel.ForEach で実行
        // 終了まで UI はブロックされる
        private void ParallelForEach_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.ForEach(Enumerable.Range(0, 5), 
                             n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを PLINK で実行
        // 終了まで UI はブロックされる
        private void PLINK_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            var results = Enumerable.Range(0, 5).AsParallel().
                          Select(n => Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // ***** 以下 TPL, PLINK に代えて Task.WhenAll を使用 *****

        // 同期版メソッド 5 つを Task.Run で実行、
        // await Task.WhenAll で待機        
        private async void WhenAll1_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(Task.Run(() => results[i] = Work(i)));
            }

            // WaitAll は await で待機できないので注意
            await Task.WhenAll(taskList.ToArray());

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを実行、
        // await Task.WhenAll で待機
        private async void WhenAll2_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            Task<string>[] results = new Task<string>[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(results[i] = WorkAsync(i));
            }

            await Task.WhenAll(taskList.ToArray());

            foreach (Task<string> result in results)
            {
                this.label1.Text += result.Result + "\r\n";
            }

            this.label1.Text += "完了";
        }

    }
}

Tags: , , , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  February 2021  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
1234567

View posts in large calendar