C#でプログラミングの練習がてらCSVリーダを書いた話
はじめに
TextFieldParserやCSVHelperを追加で入れるのが色々面倒な環境で作業をしている際に、テスト時のデータチェック用 兼 勉強用 兼 練習 兼 遊び目的で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;
}
使ってみた
// 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行目)はスキップして読み取る
などの特徴があって、使っていたデータだと不都合がありました。結果として組んでおいてよかったのかもしれない。
Author And Source
この問題について(C#でプログラミングの練習がてらCSVリーダを書いた話), 我々は、より多くの情報をここで見つけました https://qiita.com/kurunin52/items/f71c0043ef913630e2f7著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .