CSVファイル または 文字列 を List
ファイルの読み込みもすべて最初に一括実行するのではなく、逐次実行するよう実装しました。 おそらく大きなファイルでも止まらずに実行できる ハズ …と、信じています(試していないのでわからない…)。 最悪、非同期メソッドを実装してあるので、そちらを利用するとなんとか回避できると思います。
CSVフォーマットの仕様に関してはざっくりと CSVフォーマット の 仕様 に記載しました。 単体テスト含めた全体コードは Github garafu/samplecode_CsvReadWrite へ記載しました。
サンプルコード
CSV形式 の ファイル または 文字列 を読み込むサンプルコードを以下に掲載します。 このサンプルコードでは、ダブルクォート(")、改行(\r\n)、エスケープ文字もきちんと判別、読み取りするよう実装されています。 ヘッダー行の有無は判定しない点だけご注意ください。
以下にあるコードをダブルクリックして選択、コピペすれば使える ハズ です。
CsvReader.cs
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; /// <summary> /// CSV形式のストリームを読み込む CsvReader を実装します。 /// </summary> public class CsvReader : IDisposable { /// <summary> /// CSVを読み込むストリーム /// </summary> private StreamReader stream = null; /// <summary> /// 現在読み込んでいるフィールドがダブルクォートで囲まれたフィールドかどうか /// </summary> private bool isQuotedField = false; /// <summary> /// ファイル名を指定して、 <see cref="CsvReader">CsvReader</see> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="path">読み込まれる完全なファイルパス。</param> public CsvReader(string path) : this(path, Encoding.Default) { } /// <summary> /// ファイル名、文字エンコーディングを指定して、 <see cref="CsvReader">CsvReader</see> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="path">読み込まれる完全なファイルパス。</param> /// <param name="encoding">使用する文字エンコーディング。</param> public CsvReader(string path, Encoding encoding) { this.stream = new StreamReader(path, encoding); } /// <summary> /// ストリームを指定して、 <see cref="CsvReader">CsvReader</see> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="stream">読み込まれるストリーム。</param> public CsvReader(Stream stream) { this.stream = new StreamReader(stream); } /// <summary> /// 文字列データを指定して、 <see cref="CsvReader">CsvReader</see> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="data">文字列データ。</param> public CsvReader(StringBuilder data) { var buffer = Encoding.Unicode.GetBytes(data.ToString()); var memory = new MemoryStream(buffer); this.stream = new StreamReader(memory); } /// <summary> /// すべての文字の現在位置から末尾までを読み込みます。 /// </summary> /// <returns> /// ストリームの現在位置から末尾までのストリームの残り部分。 /// 現在の位置がストリームの末尾である場合は、空の配列が返されます。 /// </returns> public List<List<string>> ReadToEnd() { var data = new List<List<string>>(); var record = new List<string>(); while ((record = this.ReadRow()) != null) { data.Add(record); } return data; } /// <summary> /// すべての文字の現在位置から末尾までを非同期的に読み込みます。 /// </summary> /// <returns> /// ストリームの現在位置から末尾までのストリームの残り部分。 /// 現在の位置がストリームの末尾である場合は、空の配列が返されます。 /// </returns> public Task<List<List<string>>> ReadToEndAsync() { return Task.Factory.StartNew(() => { return this.ReadToEnd(); }); } /// <summary> /// 現在のストリームから 1 レコード分の文字を読み取り、そのデータを文字配列として返します。 /// </summary> /// <returns>入力ストリームからの次のレコード。入力ストリームの末尾に到達した場合は null。</returns> public List<string> ReadRow() { var file = this.stream; var line = string.Empty; var record = new List<string>(); var field = new StringBuilder(); while ((line = file.ReadLine()) != null) { for (var i = 0; i < line.Length; i++) { var item = line[i]; if (item == ',' && !this.isQuotedField) { record.Add(field.ToString()); field.Clear(); } else if (item == '"') { if (!this.isQuotedField) { if (field.Length == 0) { this.isQuotedField = true; continue; } } else { if (i + 1 >= line.Length) { this.isQuotedField = false; continue; } } var peek = line[i + 1]; if (peek == '"') { field.Append('"'); i += 1; } else if (peek == ',' && this.isQuotedField) { this.isQuotedField = false; i += 1; record.Add(field.ToString()); field.Clear(); } } else { field.Append(item); } } if (this.isQuotedField) { field.Append(Environment.NewLine); } else { record.Add(field.ToString()); return record; } } return null; } /// <summary> /// 現在のストリームから非同期的に 1 レコード分の文字を読み取り、そのデータを文字配列として返します。 /// </summary> /// <returns>入力ストリームからの次のレコード。入力ストリームの末尾に到達した場合は null。</returns> public Task<List<string>> ReadRowAsync() { return Task.Factory.StartNew<List<string>>(() => { return this.ReadRow(); }); } /// <summary> /// CsvReader オブジェクトと、その基になるストリームを閉じ、 /// リーダーに関連付けられたすべてのシステムリソースを解放します。 /// </summary> public void Close() { if (this.stream == null) { return; } this.stream.Close(); } /// <summary> /// この CsvReader オブジェクトによって使用されているすべてのリソースを解放します。 /// </summary> public void Dispose() { if (this.stream == null) { return; } this.stream.Close(); this.stream.Dispose(); this.stream = null; } }
使用例
上記サンプルコードの利用例を以下に載せます。 サンプルコードには非同期メソッドも実装しましたが、ここでは同期メソッドの使用例だけあげます。
1 レコードずつ読み取る使用例
using System; using System.Collections.Generic; class Program { static void Main(string[] args) { List<string> row = null; using (var csv = new CsvReader(@"TestData.csv")) { while ((row = csv.ReadRow()) != null) { Console.WriteLine(row[0].ToString()); } } } }
すべてのデータを読み取る使用例
using System; using System.Collections.Generic; class Program { static void Main(string[] args) { List<List<string>> data = null; using (var csv = new CsvReader(@"TestData.csv")) { data = csv.ReadToEnd(); } } }
更新履歴
- 2014/12/29 : 「"」で囲まれたフィールドが最後の場合、正しく読み込めない不具合修正
関連記事
最後に… このブログに興味を持っていただけた方は、 ぜひ 「Facebookページ に いいね!」または 「Twitter の フォロー」 お願いします!!