C#には標準でCSVファイルの読込みを行うクラスはありません。
ストリームから読み込んで自分で解析するか、ライブラリを使用する事になるかと思います。
案件によってはOSSライブラリの使用がNGだったりすることもあると思いますので、独自にCSVの読込み・出力を行うクラスを作成しましたので共有します。
リフレクションを使用していますので、処理速度は速くないですが、数千行程度であれば特に気になりません。(環境にもよりますが)
機能
- CSVの区切り文字を指定可能とする。
- ヘッダー行があってもなくても動作可能とする。
- 指定の型にマッピング出来るものとする。
- 指定の型をCSVに出力できるものとする。
- 値の前後の空白を取り除くかどうかを指定可能とする。
- 値を組み込み型に変換可能とする。
ソースコード
CsvParser.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
/// <summary> /// 指定の型でCSVを入力・出力する機能を実装します。 /// </summary> public class CsvParser { /// <summary> /// CSVファイルにヘッダー行があるかどうかを取得または設定します。 /// </summary> public bool HasHeader { get; set; } = true; /// <summary> /// 区切り文字を取得または設定します。 /// </summary> public string Delimiter { get; set; } = ","; /// <summary> /// 要素の前後の空白をトリムするかどうかを取得または設定します。 /// </summary> public bool TrimWhiteSpace { get; set; } = true; /// <summary> /// CSVファイルの文字コードを取得または設定します。 /// </summary> public Encoding Encoding { get; set; } = Encoding.UTF8; /// <summary> /// <see cref="CsvParser"/>クラスのインスタンスを初期化します。 /// </summary> public CsvParser() { } /// <summary> /// CSVファイルから全行を読込み、指定の型にマッピングしたコレクションを返却します。 /// </summary> /// <param name="filePath">CSVファイルパス。</param> /// <typeparam name="T">プロパティにCsvFieldAttributeを実装したマッピング対象の型。</typeparam> /// <returns>指定した型のコレクション。</returns> public IEnumerable<T> ReadAllRecords<T>(string filePath) where T : class, new() { // ファイルパスが指定されていない if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); // CsvFieldAttributeがマッピングするクラスメンバに実装されていない if (!IsCsvFieldAttributeImplemented<T>()) throw new Exception("CsvFieldAttribute not implemented"); // CSVファイル読み込み var lines = new List<string[]>(); using (var sr = new StreamReader(filePath, Encoding)) { // ヘッダー行有りの場合は先頭行を読み飛ばす if (HasHeader) { _ = sr.ReadLine(); } var delimiter = new string[] { Delimiter }; while (!sr.EndOfStream) { lines.Add(sr.ReadLine().Split(delimiter, StringSplitOptions.None)); } } // 指定したクラスにマッピングする var properties = typeof(T).GetProperties(); foreach (var line in lines) { T record = new T(); for (var i = 0; i < line.Length; i++) { var pInfo = properties.FirstOrDefault(pi => (pi.GetCustomAttributes(typeof(CsvFieldAttribute), false)[0] as CsvFieldAttribute).Index == i + 1); if (pInfo != null) { object value = TrimWhiteSpace ? line[i].Trim() : line[i]; // Nullable型の場合 if (pInfo.PropertyType.IsGenericType && pInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { // 値が空の場合はnullを設定、nullではない場合は基となる型に変換する var t = Nullable.GetUnderlyingType(pInfo.PropertyType) ?? pInfo.PropertyType; value = string.IsNullOrEmpty(value.ToString()) ? null : Convert(t, value.ToString()); } pInfo.SetValue(record, value, null); } } yield return record; } } /// <summary> /// 指定した型のコレクションをCSVファイルに出力します。 /// </summary> /// <typeparam name="T">プロパティにCsvFieldAttributeを実装したレコードの型。</typeparam> /// <param name="filePath">CSVファイルパス。</param> /// <param name="headers">ヘッダー文字列コレクション。</param> /// <param name="records">指定した型のコレクション。</param> /// <returns></returns> public bool SaveRecords<T>(string filePath, IEnumerable<string> headers, IEnumerable<T> records) where T : class, new() { // ファイルパスが指定されていない if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); // CsvFieldAttributeがマッピングするクラスメンバに実装されていない if (!IsCsvFieldAttributeImplemented<T>()) throw new Exception("CsvFieldAttribute not implemented"); using (var sw = new StreamWriter(filePath, false, Encoding)) { // ヘッダーが指定されている場合 if (headers != null && headers.Count() > 0) { // ヘッダー行出力 sw.WriteLine(string.Join(Delimiter, headers)); } // レコード行出力 foreach (var record in records) { var properties = record.GetType() .GetProperties() .OrderBy(pi => (pi.GetCustomAttribute(typeof(CsvFieldAttribute), false) as CsvFieldAttribute).Index); var values = properties.Select(p => {<br> var value = p.GetValue(record, null);<br> if (value == null) value = "";<br> return value.ToString();<br> });<br> sw.WriteLine(string.Join(Delimiter, values));<br> } } return true; } /// <summary> /// 指定した型のプロパティに<see cref="CsvFieldAttribute"/>が実装されているか判定します。 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> private bool IsCsvFieldAttributeImplemented<T>() { var members = typeof(T).GetMembers().Where(m => m.MemberType == MemberTypes.Property); foreach (var member in members) { if (!member.IsDefined(typeof(CsvFieldAttribute), false)) { return false; } } return true; } /// <summary> /// 文字列を指定の型に変換します。 /// </summary> /// <param name="type"></param> /// <param name="value"></param> /// <returns></returns> private object Convert(Type type, string value) { if (type.IsEnum) { return Enum.Parse(type, value); } return System.Convert.ChangeType(value, type); } } |
CsvFileAttribute.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// <summary> /// CSVフィールドインデックス属性。 /// </summary> [AttributeUsage(AttributeTargets.Property)] public class CsvFieldAttribute : Attribute { public int Index { get; } public CsvFieldAttribute(int index) { if (index < 1) throw new ArgumentOutOfRangeException(nameof(index), $"You must specify a value of 1 or greater for index."); Index = index; } } |
使用方法
CSVファイルの読込み
以下のCSVファイル読込みを例として解説します。

ユーザー定義型(マッピングする型)を定義し、CSVのカラムに対応したプロパティを定義します。
定義した型に「CsvFileAttribute」属性を指定し、カラムのインデックスを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class Test { [CsvField(1)] public string 文字列 { get; set; } [CsvField(2)] public int? 整数_int { get; set; } [CsvField(3)] public long? 整数_long { get; set; } [CsvField(4)] public float? 浮動小数点数_float { get; set; } [CsvField(5)] public double? 浮動小数点数_double { get; set; } [CsvField(6)] public Environment.SpecialFolder? 列挙値 { get; set; } [CsvField(7)] public DateTime? 日付 { get; set; } public override string ToString() { var str = ""; foreach (var pi in this.GetType().GetProperties()) { str += $"{pi.Name}:{pi.GetValue(this)}\r\n"; } return str; } } |
CSVの値は空の場合があるため、Null許容型(Nullable)として定義します。
上記CSVファイルを定義した型にマッピングするには以下の様に使用します。
1 2 3 4 5 6 |
var csvFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Book1.csv"); var csv = new CsvParser(); foreach (var record in csv.ReadAllRecords<Test>(csvFile)) { Console.WriteLine(record); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
// 出力結果 文字列:ごりへい1 整数_int:0 整数_long:10000000000 浮動小数点数_float:1.123 浮動小数点数_double:1.123456 列挙値:PrinterShortcuts 日付:2022/04/15 0:00:00 文字列:ごりへい2 整数_int:0 整数_long:10000000001 浮動小数点数_float:1.124 浮動小数点数_double:1.123457 列挙値:Desktop 日付:2022/04/16 0:00:00 文字列:ごりへい3 整数_int:0 整数_long:10000000002 浮動小数点数_float:1.125 浮動小数点数_double:1.123458 列挙値:MyDocuments 日付:2022/04/17 0:00:00 文字列:ごりへい4 整数_int:0 整数_long:10000000003 浮動小数点数_float:1.126 浮動小数点数_double:1.123459 列挙値:AdminTools 日付:2022/04/18 0:00:00 文字列:ごりへい5 整数_int:0 整数_long:10000000004 浮動小数点数_float:1.127 浮動小数点数_double:1.12346 列挙値:Templates 日付:2022/04/19 0:00:00 文字列:ごりへい6 整数_int:0 整数_long:10000000005 浮動小数点数_float:1.128 浮動小数点数_double:1.123461 列挙値:Cookies 日付:2022/04/20 0:00:00 文字列:ごりへい7 整数_int:0 整数_long:10000000006 浮動小数点数_float:1.129 浮動小数点数_double:1.123462 列挙値:ApplicationData 日付:2022/04/21 0:00:00 文字列:ごりへい8 整数_int:0 整数_long:10000000007 浮動小数点数_float:1.13 浮動小数点数_double:1.123463 列挙値:SendTo 日付:2022/04/22 0:00:00 文字列:ごりへい9 整数_int:0 整数_long:10000000008 浮動小数点数_float:1.131 浮動小数点数_double:1.123464 列挙値:Recent 日付:2022/04/23 0:00:00 文字列:ごりへい10 整数_int:0 整数_long:10000000009 浮動小数点数_float:1.132 浮動小数点数_double:1.123465 列挙値:Startup 日付:2022/04/24 0:00:00 文字列:ごりへい11 整数_int:0 整数_long:10000000010 浮動小数点数_float:1.133 浮動小数点数_double:1.123466 列挙値:System 日付:2022/04/25 0:00:00 文字列:ごりへい12 整数_int:0 整数_long:10000000011 浮動小数点数_float:1.134 浮動小数点数_double:1.123467 列挙値:DesktopDirectory 日付:2022/04/26 0:00:00 文字列:ごりへい13 整数_int:0 整数_long:10000000012 浮動小数点数_float:1.135 浮動小数点数_double:1.123468 列挙値:MyDocuments 日付:2022/04/27 0:00:00 |
CSVファイルの出力
CSVファイルの出力をするには以下の様に使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var records = new Test[10]; for (var i = 0; i < records.Length; i++) { records[i] = new Test { 文字列 = $"ごりへい{i}", 整数_int = i, 整数_long = i * 10000000000, 浮動小数点数_float = (float)(i * 1.123), 浮動小数点数_double = (double)(i * 0.00000123), 列挙値 = (Environment.SpecialFolder)Enum.Parse(typeof(Environment.SpecialFolder), Enum.GetNames(typeof(Environment.SpecialFolder))[i]), 日付 = DateTime.Now + TimeSpan.FromDays(i), }; } var csvFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Book2.csv"); var csv = new CsvParser(); csv.SaveRecords(csvFile, new string[] { "string", "int", "long", "float", "double", "enum", "date" }, records); |

オプション
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var csv = new CsvParser(); // 区切り文字を変更 csv.Delimiter = ";"; // 値の前後の空白を除外する csv.TrimWhiteSpace = true; // 入力・出力文字コードを変更する csv.Encoding = Encoding.ASCII; // ヘッダー行の無いCSVファイルを読込む csv.HasHeader = false; |
おまけ
実行速度を計測してみました。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 1000行 LineCount:1000 seconds:0.0528583 // 10000行 LineCount:10000 seconds:0.4109035 // 100000行 LineCount:100000 seconds:3.6153662 // 1000000行 LineCount:1000000 seconds:34.0428918 |
列数7で1万行の出力で0.4秒程となっています。
10万行で約3.5秒。
さすがに100万行は35秒と遅いですが、CSVファイルを100万行パースして使用する事はまずないと思いますので、通常の使用であれば特に問題ない処理速度だと思います。
動作環境
・CPU i7 7700K
・メモリ 16GB
・ストレージ SSD
コメント