2021年2月8日月曜日

C#で階層構造をもつJSONとCSVを相互変換する

階層構造をもつJSONのデータをCSVファイルの1行として保存するプログラムのメモ. ネットでは配列と同じような構造のJSONデータをCSVファイルに変換する例はよく見るけど, 入れ子になってるデータでもCSVファイルに保存したい.

  • 階層構造をもつJSONのデータをCSVファイルの1行として保存する
  • 保存したCSVファイルから逆にJSONに戻せるようにする
  • 構造は未知としてライブラリ化する

作戦は,クラスオブジェクトをJSON化して,JSONのkeyはCSVのヘッダに格納して,valueは対応する列のセルに格納する. 階層構造を保つ場合は,key1:key2のように階層構造の親子関係を「:」で接続して表現する.

想定するJSONの階層構造例

想定するJSONはこれ.中括弧が3重になっていることから分かるように3階層になっている.

{"a":"aho","b":{"a":1,"b":2,"c":{"c":2,"d":3}}}

これをこんな感じのCSVに変換したい.ここではオブジェクト1個分だからCSVファイルではヘッダ1行とデータ1行.

#a,b:a,b:b,b:c:c,b:c:d
aho,1,2,2,3

使い方

// JSONデータ(入力方法は省略)
// {"a":"aho","b":{"a":1,"b":2,"c":{"c":2,"d":3}}}
string json_txt = .... ;

// JSON -> CSV 

// JSONからCSVのヘッダとデータ1行に変換
string csv_header = JsonCsv.JsonToCsvHeader(json_txt);
string csv_row = JsonCsv.JsonToCsvRow(json_txt);
Console.WriteLine(csv_header+csv_row);

// CSV -> JSON 

// CSVヘッダからDictionary<string, object>に変換(key情報のみ格納)
var dict = JsonCsv.CsvHeaderToDict(csv_header+'\n');

// Dictionary<string, object>からJSONに変換(途中結果の確認用)
string json_dict = JsonCsv.DictToJson(dict);

// key情報とvalue情報をあわせてJSONに変換
string json_out = JsonCsv.CsvRowToJson(dict, csv_row+'\n');

Console.WriteLine(json_dict);
Console.WriteLine(json_out);
{"a":"aho","b":{"a":1,"b":2,"c":{"c":2,"d":3}}}

JSONからCSV

JSONからCSVヘッダ

階層構造を処理するためにDictionary<string, object>を用いて, objectにDictionary<string, object>を代入することでツリー構造を作る. ツリー構造を巡回するために再帰関数extractHeaderをJsonToCsvHeaderから呼び出す仕組み.

public static string JsonToCsvHeader(string json_txt){
    Dictionary<string, object> dic = Json.Deserialize(json_txt) as Dictionary<string, object>;
    string csv_txt = "#";
    csv_txt += extractHeader("", (Dictionary<string, object>)dic);
    return csv_txt + '\n';
}
// #a,b:a,b:b,b:b:c:c,b:c:d
static string extractHeader(string parent, Dictionary<string, object> dic){
    string csv_txt = "";
    bool is_first = true;
    foreach(KeyValuePair<string, object> pair in dic)
    {
        //最初以外はカンマで区切る
        if(!is_first){
            csv_txt += ",";
        }
        is_first = false;

        Type type = pair.Value.GetType();
        if( type == typeof(Dictionary<string, object>) )
        {
            csv_txt += extractHeader(parent + pair.Key + ':', (Dictionary<string, object>)pair.Value);
        }
        else{
            csv_txt += (parent + pair.Key);
            Debug.Log(parent + pair.Key);
        }
    }
    return csv_txt;
}

JSONからCSVデータ

public static string JsonToCsvRow(string json_txt){
    Dictionary<string, object> dic = Json.Deserialize(json_txt) as Dictionary<string, object>;
    string csv_txt  = extractRow((Dictionary<string, object>)dic);
    return csv_txt + '\n';
}
static string extractRow(Dictionary<string, object> dic){
    string csv_txt = "";
    bool is_first = true;
    foreach(KeyValuePair<string, object> pair in dic)
    {
        //最初以外はカンマで区切る
        if(!is_first){
            csv_txt += ",";
        }
        is_first = false;

        Type type = pair.Value.GetType();
        if( type == typeof(Dictionary<string, object>) )
        {
            csv_txt += extractRow((Dictionary<string, object>)pair.Value);
        }
        else{
            csv_txt +=  pair.Value;
        }
    }
    return csv_txt;
}

CSVからJSON

CSVのヘッダをDictionaryに格納

ここでもDictionaryを使ってツリー構造を構築する. CSVのヘッダからDictionary<string, object>に一旦変換する. addDictItem関数は再帰処理の関数.

public static Dictionary<string, object> CsvHeaderToDict(string csv_txt)
{
    var rows = csv_txt.Split('\n');
    string[] header = rows[0].Split(',');
    header[0] = header[0].Trim('#');
    Dictionary<string, object> dict = new Dictionary<string, object>();

    for(int i = 0; i<header.Length; i++)
    {
        var keys = header[i].Split(':');
        addDictItem(keys, 0, ref dict);
    }
    return dict;
}
static void addDictItem(string [] keys, int idx, ref Dictionary<string, object> dict){
    //2つ以上のkey
    if(keys.Length > idx+1)
    {
        //追加
        if(dict.ContainsKey(keys[idx])){
            var child = (Dictionary<string, object>) dict[ keys[idx] ];
            addDictItem(keys, idx+1, ref child);
        }
        //新規
        else{
            var child = new Dictionary<string, object>();
            dict[ keys[idx] ]= child;
            addDictItem(keys, idx+1, ref child);
        }
    }
    //1つのkey
    else{
        dict[ keys[idx] ]="";
    }
}

CSVのデータとDictionaryからJSONに変換

一旦Dictionaryが出来てしまえば簡単. CSVのデータ1行(csv_row)とヘッダ情報を格納したDictionaryとでJSON形式のデータを復元する.

public static string CsvRowToJson(Dictionary<string, object> dic, string csv_row)
{
    csv_row = csv_row.Trim();
    var cols = csv_row.Split(',');
    int i_col = 0;

    string json_txt = "{";
    bool is_first = true;
    foreach(KeyValuePair<string, object> pair in dic)
    {
        //最初以外はカンマで区切る
        if(!is_first){
            json_txt += ",";
        }
        is_first = false;
        
        //子ノードを持っているとき
        Type type = pair.Value.GetType();
        if( type == typeof(Dictionary<string, object>) )
        {        
            string pattern = "[^,]*,";
            Regex regex = new Regex(pattern);
            int max_count = i_col;
            string row_next = regex.Replace(csv_row, "", max_count);
            json_txt += "\"" + pair.Key + "\":" + CsvRowToJson((Dictionary<string, object>)pair.Value, row_next);
        }
        //子ノードが無いとき
        else{
            json_txt += "\"" + pair.Key + "\":\"" + cols[i_col] + "\"";
        }
        i_col++;
    }
    return json_txt + '}';
}