C#を使ったCSVの読み込み方法に関する検索が多いようなので、今回はそれを取り上げたいと思います。
このブログで紹介した以前の記事では、Microsoft.VisualBasic.dll を参照設定することで使える「TextFieldParser」を用いたサンプルを紹介しました。
今回はそのような参照設定は必要なく、C#のコードだけでCSV読み込みを行っています。
勿論、ダブルクォーテーションで括られている項目の中に、カンマや改行キーが入っていても正しく分解できるようにしています。
また、関数化していますので、そのままコピー&ペーストでお使いいただけます。
中身の理解を助けるためのフローチャートも載せていますので、是非参考にしてください。
アルゴリズムの解説
CSVの仕様を把握しておく必要がありますが、詳しくは こちら の記事に委ねるとして、それを踏まえると基本的な考え方は次様になります。
- ファイルから1文字づつ読み込む
- カンマが見つかったら、そこまでを1項目として認識する
- 改行が見つかったら、そこまでを1行として認識する
- ダブルクォーテーションが見つかれば、次のダブルクォーテーションが見つかるまでの間は、カンマや改行は単なる文字として扱う
- ダブルクォーテーションが項目内に2個連続して登場する場合、ダブルクォーテーション1つに置き換える
項目内にカンマや改行が無い場合
項目内にカンマや改行が無い場合は非常に簡単です。
もし、行数が数百万行くらいで、項目の中にカンマや改行が無く、ダブルクォーテーションで括られていなければ、一気にメモリに読み込んでカンマで分割するという方法が使えます。
実際にはこのような形式のCSVも多いので、簡易的ではありますが実用的です。
ちなみに、ファイルを読み込むためのクラスを使っているので、ソースコードの冒頭に次の参照設定を記述しておいて下さい。
using System.IO;
private List<string[]> LoadCsv(string filename)
{
List<string[]> result = new List<string[]>();
var lines = File.ReadAllLines(uxPath.Text, Encoding.GetEncoding("shift-jis"));
foreach(string line in lines)
{
result.Add(line.Split(','));
}
return result;
}
ファイル名を指定すると、CSVを読み込んで、文字列配列のリスト(List<string[])で返すようになっています。
参考までに、foreach ループを linq に換えたサンプルも掲載しておきます。
private List<string[]> LoadCsv(string filename)
{
List<string[]> result = new List<string[]>();
var lines = File.ReadAllLines(uxPath.Text, Encoding.GetEncoding("shift-jis"));
lines.Select(i => { result.Add(i.Split(',')); return i; }).ToArray();
return result;
}
項目内にカンマや改行がある場合
こちらが今回の記事の本命です。
ファイル名と文字のエンコード名を渡すと、CSVを分割して List<string[]>で返す仕様なのですが、ダブルクォーテーションが見つかると、次のダブルクォーテーションが見つかるまでの間、カンマがあっても分割せず、改行があっても改行しないようにする必要があります。
そのため、次の3つの変数を用意しています。
- ファイルを1文字づつ読み込んでカンマが見つかると、そこまでを1つの項目として切り出して格納する value 変数
- 改行が見つかるまで、切り出した項目を1行分だけ蓄えておく line 変数
- 改行が見つかったら、line変数を次々と追加していく result 変数
ファイルから取り出した1文字が、ダブルクォーテーションとダブルクォーテーションの間にあるものか否かを区別するため、bool 型の dq_flg というフラグを用意しました。
ダブルクォーテーションが見つかると 、フラグの状態を反転させるという動作をさせることで、ダブルクォーテーション内か否かを区別しています。
//ダブルクオーテーションが見つかるとフラグを反転する
dq_flg = (ch == '\"') ? !dq_flg : dq_flg;
これを前提にしたフローチャートは次の様になります。
以上で前置きは終了です。
では、実際のソースコードを見ていきましょう。
ソースコード
少し長くなりましたが、全体のソースコードは次の様になります。
これもコピー&ペーストで張り付けて使えるようにしています。
StreamReader クラスを使っていますので、 冒頭の参照設定に using System.IO; を追加しておいて下さい。
private List<string[]> LoadCsv(string fileName,char delimiter = ",",string encodeName = "shift-jis")
{
//結果を格納するリスト
List<string[]> result = new List<string[]>();
//カンマで分割した1行分を格納するリスト
List<string> line = new List<string>();
//1カラム分の値を格納する変数
StringBuilder value = new StringBuilder();
//ダブルクォーテーションの中であることを現わすフラグ
bool dq_flg = false;
//ファイルをオープンする
using (StreamReader sr = new StreamReader(fileName, Encoding.GetEncoding(encodeName)))
{
//ファイルの最後になるまでループする
while (!sr.EndOfStream)
{
//1文字読み込む
var ch = (char)sr.Read();
//ダブルクオーテーションが見つかるとフラグを反転する
dq_flg = (ch == '\"') ? !dq_flg : dq_flg;
//ダブルクォーテーション中ではないキャリッジリターンは破棄する
if (ch == '\r' && dq_flg == false)
{
continue;
}
//ダブルクォーテーション中ではない時にカンマが見つかったら、
//それまでに読み取った文字列を1つのかたまりとしてline に追加する
if (ch == delimiter && dq_flg == false)
{
line.Add(to_str(value));
value.Clear();
continue;
}
//ダブルクォーテーション中ではない時にラインフィードが見つかったら
//line(1行分) を result に追加する
if (ch == '\n' && dq_flg == false)
{
line.Add(to_str(value));
result.Add(line.ToArray());
line.Clear();
value.Clear();
continue;
}
value.Append(ch);
}
}
//ファイル末尾が改行コードでない場合、ループを抜けてしまうので、
//未処理の項目がある場合は、ここでline に追加
if(value.Length > 0)
{
line.Add(to_str(value));
result.Add(line.ToArray());
}
return result;
//前後のダブルクォーテーションを削除し、2個連続するダブルクォーテーションを1個に置換する
string to_str(StringBuilder p_str)
{
string l_val = p_str.ToString().Replace("\"\"", "\"");
int l_start = (l_val.StartsWith("\"")) ? 1 : 0;
int l_end = l_val.EndsWith("\"") ? 1 : 0;
return l_val.Substring(l_start, l_val.Length - l_start - l_end);
}
}
まとめ
今回はCSV読み込みをC#のロジックだけで実現する方法を、図式で解説しました。
サンプルソースはそのまま使えますが、中身を理解していれば好きなように改造できますので、例えばカンマではなくタブ区切り対応するとか、色々と手を加えても面白いと思います。
コメント