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 の フォロー」 お願いします!!