C# で CSVファイル を List データ に 読み込む 方法

2 件のコメント

CSVファイル または 文字列 を List 配列 に読み取るコードを作成する必要があったので、サンプルコードを作成しました。 ちまたのサイトでは カンマ(,) でスプリットするものが多く、ダブルクォート(")、改行(\r\n)、エスケープ文字もきちんと判別、読み取るものがみあたらなかったので、細かいとこまでできるものを実装してみました。

ファイルの読み込みもすべて最初に一括実行するのではなく、逐次実行するよう実装しました。 おそらく大きなファイルでも止まらずに実行できる ハズ …と、信じています(試していないのでわからない…)。 最悪、非同期メソッドを実装してあるので、そちらを利用するとなんとか回避できると思います。

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

 

  1. 120行目の処理、おかしいのではないでしょうか。
    if ((i + 1 >= line.Length) ||                <ここ
    (field.Length == 0 && !this.isQuotedField))   <ここ
    {
    this.isQuotedField = true;
    continue;
    }
    csvファイルが例えば、and,or,"test1"で終わるような場合、"test1"が誤認でreturn nullへ落ちます。
    解決策として、
    if (field.Length == 0 && !this.isQuotedField))
    {
    this.isQuotedField = true;
    continue;
    }
    if (i + 1 >= line.Length && this.isQuotedField)
    {
    this.isQuotedField = false;
    continue;
    }
    以上を提案します。いかがでしょうか。

    返信削除
    返信
    1. zoh nakaさん

      ご指摘ありがとうございます!
      こうした指摘をいただけると、個人的には嬉しいです。
      (返信が遅くなったのは申し訳ありません・・・)

      ご指摘いただいた「最後が " で囲まれたフィールドの場合、正しく読み取れない」不具合ですが、
      不具合を確認でき、ご提案いただいた解決策も、私の気づいた範囲では大丈夫だと思ったので、
      ちょっと手を加えさせていただきましたが、そのまま使わせていただきます。
      →そのまま記事へ反映させていただきました。ありがとうございます!!

      CSVの読み込みはパターンがいろいろありそうなので、
      少し単体テストを考えて、チェックしてみました。
      単体テスト含めたソリューション全体は github の garafu/samplecode_CsvReadWrite へ公開しました。
      https://github.com/garafu/samplecode_CsvReadWrite
      なお、単体テストのパターンは以下のURLにあります。
      https://github.com/garafu/samplecode_CsvReadWrite/blob/master/UnitTestProject/TestSpecification.txt

      もし、他にもお気づきの点等ございましたらご連絡いただけると幸いです。

      削除