LoginSignup
5
3

More than 5 years have passed since last update.

C#でプログラミングの練習がてらCSVリーダを書いた話

Posted at

はじめに

TextFieldParserCSVHelperを追加で入れるのが色々面倒な環境で作業をしている際に、テスト時のデータチェック用 兼 勉強用 兼 練習 兼 遊び目的でCSVファイルのパース処理を組んだ記録です。
初投稿なので、さらに投稿の練習も兼ねて。

環境

.Net Framework 4.5 (C# 5)

CSVの仕様

Comma-Separated Values - ウィキペディア

要約すると、

  • カンマ(,)でデータが区切られている
  • データ内に以下のどれかがあれば、データをダブルクォートで囲む
    • カンマ
    • 改行
    • ダブルクォート
  • データ内にダブルクォートがあれば、それを二重にする
  • 以上に関係なく、各データはダブルクォートで囲んでもいい

使うシステムで仕様が違う場合があるとのことですが、おそらくここまでが基本仕様とのこと。

読み取りの指針

以下の処理ができれば読み取りできるはず、という点を書く前に大まかに整理。

  • データの先頭がダブルクォートだったら、それ以降でペアになっていないダブルクォートを探して、そこまでを1データとする
  • データ自体にダブルクォートがあれば、二重になっているのを1つに変換する
  • データの先頭がダブルクォートでなければ、次のカンマまでを1データとする
  • 行の先頭、末尾にカンマがあれば、その前後に空文字データがあると考える
  • 2回以上連続でカンマがあれば、そこに空文字データがあると考える
  • 空行は、空文字データが1つある行と考える

実際のメソッド


// File.ReadLines(string path)辺りを引数で渡すつもりで作成
public IEnumerable<List<string>> ReadCsvData(IEnumerable<string> data)
{
    // 返却用リスト
    List<string> retLine = new List<string>();
    // データの改行有無フラグ
    bool hasNewLine = false;

    foreach(string line in data)
    {
        int iStart = 0;

        // 前の読み取り行に改行付きデータがあれば、ダブルクォートの終点探し
        if (hasNewLine)
        {
            retLine[retLine.Count - 1] += Environment.NewLine;
            int iEnd = SearchCloseQuot(line, iStart);
            if (iEnd == -1)
            {
                retLine[retLine.Count - 1] += line.Replace("\"\"", "\"");
                continue;
            }
            retLine[retLine.Count - 1] += line.Substring(iStart, iEnd).Replace("\"\"", "\""); 
            hasNewLine = false;

            iStart = line.IndexOf(',', ++iEnd);

            if(iStart == -1)
            {
                yield return retLine;
                retLine = new List<string>();
                continue;
            }

            iStart++;
        }

        // 行始まりか、前行からの改行データ終わりから読み取り開始
        for (; iStart < line.Length; iStart++)
        {
            //始点がダブルクォートなら、終点のダブルクォートの探索
            if(line[iStart] == '"')
            {
                int iEnd = SearchCloseQuot(line, iStart + 1);
                if (iEnd == -1)
                {
                    retLine.Add(line.Substring(iStart));
                    hasNewLine = true;
                    break;
                }
                retLine.Add(line.Substring(iStart + 1, iEnd - iStart - 1).Replace("\"\"", "\""));
                if(iEnd == line.Length - 1)
                {
                    break;
                }
                else
                {
                    iStart = line.IndexOf(',', iEnd + 1);
                    continue;
                }
            }
            // 始点がカンマなら、空文字データがあるとして処理
            else if (line[iStart] == ',')
            {
                retLine.Add("");
                continue;
            }
            // 始点がそれ以外なら、次のカンマを探索
            else
            {
                int iEnd = line.IndexOf(',', iStart);
                if(iEnd == -1)
                {
                    retLine.Add(line.Substring(iStart).Trim());
                    break;
                }
                else
                {
                    retLine.Add(line.Substring(iStart, iEnd - iStart));
                    iStart = iEnd;
                    continue;
                }
            }
        }

        // 行内のデータに改行がなければ、リストを返却してリセット
        if (!hasNewLine)
        {
            if (line.Length == 0 || line[line.Length - 1] == ',')
            {
                retLine.Add("");
            }
            yield return retLine;
            retLine = new List<string>();
        }
    }

    // 読み取りが終わった時に改行があれば異常処理
    if (hasNewLine)
        throw new InvalidDataException();
}

// データ終端のダブルクォートを探すサブ関数
private int SearchCloseQuot(string data, int startIndex)
{
    for (int i = startIndex; i < data.Length; i += 2)
    {
        i = data.IndexOf('"', i);
        if (i == data.Length - 1 || data[i + 1] != '"' || i == -1)
        {
            return i;
        }
    }
    return -1;
}

使ってみた

テストデータ:

a,b,cd,e
,f,,g,h,

"i,",j,k
"lm"",n",op,"qrs",
"tu","vw""
""x",yz

実行結果:
(データのない箇所はハイフン)

0 1 2 3 4 5
a b cd e - -
f g h
- - - - -
i, j k - - -
lm",n op qrs - -
tu vw"
"x
yz - - -

補記

いったん例は割愛しますが、前述のTextFieldParserクラスでの読み取りには

  • データの先頭、末尾にスペースがあると、ダブルクォートで囲っていてもTrimされる
  • 空白行(サンプルデータの3行目)はスキップして読み取る

などの特徴があって、使っていたデータだと不都合がありました。結果として組んでおいてよかったのかもしれない。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3