【C#】CSVに型情報を追加する【現場の備忘録】

プログラム

CSVは手軽で便利なんだけど、読み込むと中身が全部string(文字列)で入ってくる。

だから金額を合計しようとすると毎回int.Parse()、日付でソートしたいのに文字列…みたいに、使う側で毎回型変換する羽目になる

だったらCSV自体に型を持たせてしまえばいい。やり方は簡単で、1行目に列名、2行目にその型、3行目からデータ、という並びにしておくだけ。

型情報つきCSVの例
TOSHI_NO,HINMEI,JYURYO,IRISU
int,string,decimal,int
1,りんご,1.1,3
2,マスカット,1.7,6
3,いちご,0.1,2

こうしておけば、読み込むときに2行目の型を見て列を定義できるので、そのまま型付きのDataTableが作れる。

書き出し(型行つきCSVを作る)

まずはDataTableを上のフォーマットで書き出す処理。列名の行の下に、型名の行を1行差し込んでいるだけ。

    /// <summary>
    /// DataTableを「型情報つきCSV」として書き出す
    /// (1行目=列名/2行目=型/3行目以降=データ)
    /// </summary>
    public static void WriteTypedCsv(DataTable dt, string path)
    {
        var sb = new StringBuilder();
 
        var names = new string[dt.Columns.Count]; // 1行目:列名
        var types = new string[dt.Columns.Count]; // 2行目:型名
        for (int i = 0; i < dt.Columns.Count; i++)
        {
            names[i] = dt.Columns[i].ColumnName;
            types[i] = ToTypeName(dt.Columns[i].DataType);
        }
        sb.AppendLine(string.Join(",", names));
        sb.AppendLine(string.Join(",", types));
 
        // 3行目以降:データ本体
        foreach (DataRow dr in dt.Rows)
        {
            var values = new string[dt.Columns.Count];
            for (int i = 0; i < dt.Columns.Count; i++)
            {
                values[i] = dr[i]?.ToString() ?? string.Empty;
            }
            sb.AppendLine(string.Join(",", values));
        }
 
        File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
    }
 
    /// <summary>
    /// C#の型 -> CSVに書き込む型名
    /// </summary>
    private static string ToTypeName(Type type)
    {
        if (type == typeof(int)) return "int";
        if (type == typeof(long)) return "long";
        if (type == typeof(decimal)) return "decimal";
        if (type == typeof(double)) return "double";
        if (type == typeof(bool)) return "bool";
        if (type == typeof(DateTime)) return "datetime";
        return "string";
    }

読み込み(型付きDataTableにする)

読み込み側。2行目の型名を見て列を作り、データはConvert.ChangeTypeで列の型に変換して格納している。

    /// <summary>
    /// 「型情報つきCSV」を、型付きのDataTableとして読み込む
    /// </summary>
    public static DataTable ReadTypedCsv(string path)
    {
        DataTable dt = new DataTable();
 
        string[] lines = File.ReadAllLines(path, Encoding.UTF8);
        // 列名行+型行の最低2行が無ければ何もできない
        if (lines.Length < 2) return dt;
 
        string[] names = lines[0].Split(',');  // 1行目:列名
        string[] types = lines[1].Split(',');  // 2行目:型名
 
        // 列名と型から、型付きの列を定義していく
        for (int i = 0; i < names.Length; i++)
        {
            dt.Columns.Add(names[i], ToClrType(types[i]));
        }
 
        // 3行目以降がデータ本体
        for (int r = 2; r < lines.Length; r++)
        {
            if (string.IsNullOrEmpty(lines[r])) continue; // 空行はスキップ
 
            string[] cells = lines[r].Split(',');
            DataRow row = dt.NewRow();
            for (int c = 0; c < names.Length; c++)
            {
                // 空文字はDBNull、それ以外は列の型に変換して格納
                // 環境(カルチャ)による小数点の解釈ズレを防ぐためInvariantCultureを指定
                row[c] = string.IsNullOrEmpty(cells[c])
                    ? (object)DBNull.Value
                    : Convert.ChangeType(cells[c], dt.Columns[c].DataType, CultureInfo.InvariantCulture);
            }
            dt.Rows.Add(row);
        }
 
        return dt;
    }
 
    /// <summary>
    /// CSVの型名 -> C#の型
    /// </summary>
    private static Type ToClrType(string typeName)
    {
        switch (typeName.Trim().ToLower())
        {
            case "int": return typeof(int);
            case "long": return typeof(long);
            case "decimal": return typeof(decimal);
            case "double": return typeof(double);
            case "bool": return typeof(bool);
            case "datetime": return typeof(DateTime);
            default: return typeof(string);
        }
    }

ポイントはConvert.ChangeTypeCultureInfo.InvariantCultureを渡しているところ。これを省くと環境によっては小数点や日付の解釈がズレることがあるので、付けておくと安全。空文字はDBNullとして入れている。

実装例

下記の例はVisualStudio2022.NET8.0、作成プロジェクトはコンソールアプリ、OSはWindows11で実装。Program.csにコピペすれば、書き出し→読み込みまで一気に確認できる。

Program.cs
using System;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text;
 
class Program
{
    static void Main()
    {
        string path = "sample.csv";
 
        // 型情報つきCSVを書き出す
        DataTable src = CreateTable();
        WriteTypedCsv(src, path);
 
        Console.WriteLine("=== 書き出したCSV ===");
        Console.WriteLine(File.ReadAllText(path, Encoding.UTF8));
 
        // 型情報つきCSVを型付きDataTableとして読み込む
        DataTable dt = ReadTypedCsv(path);
 
        Console.WriteLine("=== 読み込んだ列と型 ===");
        foreach (DataColumn col in dt.Columns)
        {
            Console.WriteLine($"{col.ColumnName} : {col.DataType.Name}");
        }
 
        Console.WriteLine("=== データ ===");
        foreach (DataRow dr in dt.Rows)
        {
            Console.WriteLine($"{dr["TOSHI_NO"]} / {dr["HINMEI"]} / {dr["JYURYO"]} / {dr["IRISU"]}");
        }
    }
 
    /// <summary>
    /// 確認用のデータテーブルを作成
    /// </summary>
    static DataTable CreateTable()
    {
        DataTable dt = new DataTable();
        dt.Columns.Add("TOSHI_NO", typeof(int));
        dt.Columns.Add("HINMEI", typeof(string));
        dt.Columns.Add("JYURYO", typeof(decimal));
        dt.Columns.Add("IRISU", typeof(int));
 
        dt.Rows.Add(1, "りんご", 1.1m, 3);
        dt.Rows.Add(2, "マスカット", 1.7m, 6);
        dt.Rows.Add(3, "いちご", 0.1m, 2);
        return dt;
    }
 
    // ▼▼ ここから下の4メソッドがコピペ用の本体 ▼▼
 
    public static void WriteTypedCsv(DataTable dt, string path)
    {
        var sb = new StringBuilder();
 
        var names = new string[dt.Columns.Count];
        var types = new string[dt.Columns.Count];
        for (int i = 0; i < dt.Columns.Count; i++)
        {
            names[i] = dt.Columns[i].ColumnName;
            types[i] = ToTypeName(dt.Columns[i].DataType);
        }
        sb.AppendLine(string.Join(",", names));
        sb.AppendLine(string.Join(",", types));
 
        foreach (DataRow dr in dt.Rows)
        {
            var values = new string[dt.Columns.Count];
            for (int i = 0; i < dt.Columns.Count; i++)
            {
                values[i] = dr[i]?.ToString() ?? string.Empty;
            }
            sb.AppendLine(string.Join(",", values));
        }
 
        File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
    }
 
    public static DataTable ReadTypedCsv(string path)
    {
        DataTable dt = new DataTable();
 
        string[] lines = File.ReadAllLines(path, Encoding.UTF8);
        if (lines.Length < 2) return dt;
 
        string[] names = lines[0].Split(',');
        string[] types = lines[1].Split(',');
 
        for (int i = 0; i < names.Length; i++)
        {
            dt.Columns.Add(names[i], ToClrType(types[i]));
        }
 
        for (int r = 2; r < lines.Length; r++)
        {
            if (string.IsNullOrEmpty(lines[r])) continue;
 
            string[] cells = lines[r].Split(',');
            DataRow row = dt.NewRow();
            for (int c = 0; c < names.Length; c++)
            {
                row[c] = string.IsNullOrEmpty(cells[c])
                    ? (object)DBNull.Value
                    : Convert.ChangeType(cells[c], dt.Columns[c].DataType, CultureInfo.InvariantCulture);
            }
            dt.Rows.Add(row);
        }
 
        return dt;
    }
 
    private static string ToTypeName(Type type)
    {
        if (type == typeof(int)) return "int";
        if (type == typeof(long)) return "long";
        if (type == typeof(decimal)) return "decimal";
        if (type == typeof(double)) return "double";
        if (type == typeof(bool)) return "bool";
        if (type == typeof(DateTime)) return "datetime";
        return "string";
    }
 
    private static Type ToClrType(string typeName)
    {
        switch (typeName.Trim().ToLower())
        {
            case "int": return typeof(int);
            case "long": return typeof(long);
            case "decimal": return typeof(decimal);
            case "double": return typeof(double);
            case "bool": return typeof(bool);
            case "datetime": return typeof(DateTime);
            default: return typeof(string);
        }
    }
}
実行結果
=== 書き出したCSV ===
TOSHI_NO,HINMEI,JYURYO,IRISU
int,string,decimal,int
1,りんご,1.1,3
2,マスカット,1.7,6
3,いちご,0.1,2
 
=== 読み込んだ列と型 ===
TOSHI_NO : Int32
HINMEI : String
JYURYO : Decimal
IRISU : Int32
=== データ ===
1 / りんご / 1.1 / 3
2 / マスカット / 1.7 / 6
3 / いちご / 0.1 / 2

読み込んだあとの型を見ると、JYURYOがちゃんとDecimalTOSHI_NOやIRISUはInt32になっている。文字列じゃなく、ちゃんと数値の型で取れているのがわかる。

さいごに

1点だけ注意。今回のパーサはSplit(",")で切っているだけの簡易版なので、値の中にカンマや改行を含むCSVには対応していない。そういうデータを扱うならCsvHelperのようなライブラリを使うのが無難。

とはいえ、自分たちで作って自分たちで読むCSVなら、型の行を1行足しておくだけで扱いがかなり楽になる。ちょっとした受け渡しには十分だと思います。

以前紹介したDataTable→List変換と組み合わせれば、さらに便利になるのでよければどうぞ。

コメント

タイトルとURLをコピーしました