DIYプログラミングにおいて、C#を使ったCSVファイルの読み込み、書き込みは結構需要があります。
Google検索すると、CSVファイルの解説や、C#による読み書きのプログラムのサンプル例はたくさん目にしますが、実際に使おうとすると、色々な事を考慮してプログラムを書き足す必要があります。
面倒なことは置いといて、「コピペして使えるソースコードがほしい!」という方のために、実用的なCSV読み込み、書き込みのサンプルをご紹介します。
サンプルと言っても、私が実際にDIYプログラミングで使っているものなので、必要と思われる一通りの機能は揃っています。
ちなみに、WindowsFormとWPFのどちらからでも利用可能です。
CSVファイルはDataTableと相性抜群
CSVファイルをプログラムから読み込みたい場合とは、読み込んだ結果に対して何らかの加工を加えたいからですよね。
例えば別のアプリに読み込ませられるようフォーマット変換するとか、特定のデータだけを除外したいとかです。
時には、加工した結果を再びCSVファイルとして保存することもあります。
この様なニーズに対応する上で一番扱いやすいのは、何といってもDataTable です。
CSVをDataTableに入れてあげると、特定の条件でフィルタリングしたり、特定の条件に一致する項目を加工したりする行為が格段にやり易くなります。
ですから、今回紹介するソースコードは、CSVを読み込んでDataTableとして返したり、DataTableの内容をCSVファイルとして出力できるようになっています。
プログラムの仕様
仕様は次の通りです。
CSV読み込み | CSV書き込み |
---|---|
出力ファイル名の指定 ヘッダの有無の指定 最大読み込み行数の指定 文字列エンコードの指定 区切り文字の指定(自動判定機能付き) | 出力ファイル名の指定 ヘッダの出力有無の指定 区切り文字の指定 文字列エンコードの指定 既存ファイルへの追加指定 |
最初に参照設定が必要
CSVファイルは、項目がカンマやタブで区切られているテキストファイルですが、項目をダブルクォートで囲むことにより、項目の中に改行やタブを含ませることが可能です。
また、改行やタブの有無に関係なく、ダブルクォートで囲ったり囲わなかったりと、色々なバリエーションが存在します。
1文字づつ自力で解析しても良いのですが、せっかくマイクロソフトが用意してくれているので、その機能を使わせて頂きます。
その為には、Microsoft.VisualBasic というアセンブリの参照設定が必要となります。
リファレンス
ネームスペース | CommonClass |
---|---|
クラス名 | CsvUtil |
機能 | CSVファイルの読み込み |
---|---|
メソッド名 | DataTable Read(filename, isHeader [ ,limit, encodeName,delimiter ]) |
戻り値の型 | DataTable |
型 | 引数名 | 内容 | 省略時の初期値 |
---|---|---|---|
string | filename | 読み込みたいファイル名 | 省略不可 |
bool | isHeader | 1行目をヘッダとして解釈するか否かの指定 | 省略不可 |
long | limit | 読み込み行数の制限値 | longの最大値 |
string | encodeName | エンコード文字の指定 | shift-jis |
string | delimiter | 区切り文字(1文字のみ指定可能) | 自動判定 (1行目に含まれるカンマとタブの数が多い方を採用) |
機能 | CSVファイルの書き込み |
---|---|
メソッド名 | Write(dt, filename, writeHeader [ ,delimiter, encodeName ,isAppend ]) |
戻り値の型 | DataTable |
型 | 引数名 | 内容 | 省略時の初期値 |
---|---|---|---|
DataTable | dt | ファイルに保存したDataTable | 省略不可 |
string | filename | 書き込みたいファイル名ファイル名 | 省略不可 |
bool | isHeader | ヘッダを出力するか否かの指定 | 省略不可 |
string | delimiter | 区切り文字(1文字のみ指定可能) | カンマ |
string | encodeName | エンコード文字の指定 | shift-jis |
bool | isAppend | 既にファイルが存在する場合、追加書き込みするか否かの指定 | 追加しない (上書する) |
ソースコード
では、実際のソースコードを掲載します。
とりあえず説明は良いからコピペして使いたいという方は、どうぞ。
説明が必要な方は、ソースコードの後に解説します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualBasic.FileIO;
using System.Windows.Forms;
namespace CommonClass
{
public class CsvUtil
{
/// <summary>
/// CSVを読み込みDataTableとして返す
/// </summary>
/// <param name="filename"></param>
/// <param name="isHeader"></param>
/// <param name="limit"></param>
/// <param name="enccodeName"></param>
/// <param name="delimiter"></param>
/// <returns></returns>
public DataTable Read(string filename, bool isHeader, long limit = long.MaxValue, string encodeName = "shift-jis", string delimiter = "")
{
limit += (isHeader == true && limit < long.MaxValue) ? 1 : 0;
long count = 0;
var enc = Encoding.GetEncoding(encodeName);
delimiter = (delimiter == "") ? GetDelimiter(filename, enc) : delimiter;
DataTable dt = new DataTable();
using (TextFieldParser parser = new TextFieldParser(filename, enc) { TextFieldType = FieldType.Delimited })
{
parser.Delimiters = new string[] { delimiter };
while (!parser.EndOfData)
{
var fields = parser.ReadFields();
if (count++ == 0)
{
//ヘッダがある場合、1行目のデータで列を追加
dt.Columns.AddRange(fields.Select(i => (isHeader) ? new DataColumn(i) : new DataColumn()).ToArray());
if (isHeader)
{
continue;
}
}
if (fields.Length > dt.Columns.Count)
{
dt.Columns.AddRange(Enumerable.Range(0, fields.Length - dt.Columns.Count).Select(i => new DataColumn()).ToArray());
}
if (count > limit)
{
break;
}
DataRow dr = dt.NewRow();
Enumerable.Range(0, fields.Length).Select(i => dr[i] = fields[i]).ToArray();
dt.Rows.Add(dr);
}
}
return dt;
}
/// <summary>
/// DataTableの内容をCSV形式でファイルに出力する
/// </summary>
/// <param name="dt"></param>
/// <param name="filename"></param>
/// <param name="writeHeader"></param>
/// <param name="delimiter"></param>
/// <param name="encodeName"></param>
/// <param name="isAppend"></param>
public void Write(DataTable dt, string filename, bool writeHeader, string delimiter = ",", string encodeName = "shift-jis", bool isAppend = false)
{
bool header = (writeHeader && (isAppend == false || (isAppend == true && File.Exists(filename) == false)));
//書き込むファイルを開く
using (StreamWriter sw = new StreamWriter(filename, isAppend, Encoding.GetEncoding(encodeName)))
{
//ヘッダを書き込む
if (header)
{
string[] headers = dt.Columns.Cast<DataColumn>().Select(i => enclose_ifneed(i.ColumnName)).ToArray();
sw.WriteLine(String.Join(delimiter, headers));
}
//レコードを書き込む
foreach (DataRow dr in dt.Rows)
{
string[] fields = Enumerable.Range(0, dt.Columns.Count).Select(i => enclose_ifneed(dr[i].ToString())).ToArray();
sw.WriteLine(String.Join(delimiter, fields));
}
}
return;
/// 必要ならば、文字列をダブルクォートで囲む
string enclose_ifneed(string p_field)
{
//ダブルクォートで括る必要があるかを確認
if (p_field.Contains('"') || p_field.Contains(',') || p_field.Contains('\r') || p_field.Contains('\n') ||
p_field.StartsWith(" ") || p_field.StartsWith("\t") || p_field.EndsWith(" ") || p_field.EndsWith("\t"))
{
//ダブルクォートが含まれていたら2つ重ねて、前後にダブルクォートを付加
return (p_field.Contains('"')) ? ("\"" + p_field.Replace("\"", "\"\"") + "\"") : ("\"" + p_field + "\"");
}
else
{
//何もせずそのまま返す
return p_field;
}
}
}
/// <summary>
/// 区切り文字の自動判定
/// </summary>
/// <param name="filename"></param>
/// <param name="encodeName"></param>
/// <returns></returns>
public string GetDelimiter(string filename, Encoding encodeName)
{
using (StreamReader sr = new StreamReader(filename, encodeName))
{
string line = sr.ReadLine();
return (line.Split(',').Length > line.Split('\t').Length) ? "," : "\t";
}
}
}
}
解説
コメントも出来るだけ入れるようにしていますので、ソースコードを見て頂ければ内容はほぼ理解できると思います。
用途によって必要な機能が違ってきますし、プログラムの書き方についても好みがあると思いますので、そこはどんどん自分の使いやすい様に追加・修正して頂ければ幸いです。
念のため、要点だけ簡単に解説しておきたいと思います。
TextFieldParser を使ってCSVを項目に分解
CSVを読み込んで区切り文字で分割する作業を、TextFieldParserというマイクロソフト標準のクラスで実現しています。
標準と言っても初めから組み込まれている訳では無いので、Microsoft.VisualBasic というアセンブリを参照し、またソースコード冒頭にも次の1行を入れています。
using Microsoft.VisualBasic.FileIO;
もし自力でやろうとするなら、1文字づつ解析しながら、ダブルクォートが見つかったら次に見つかるまでの間は、「改行」「タブ」「カンマ」を区切り文字として扱わないような工夫が必要です。
GetDelimiterで区切り文字を判定
引数の delimiter が省略されるか 空文字( "" )の場合、1行目を読み込んで区切り文字を判定しています。
方法は、カンマ(,) とタブコード(\t)の数を数えて、多い方を区切り文字として判断しているだけです。
1行目を使ってヘッダを作成
引数の isHeader が true の場合、1行目をヘッダと解釈し、DataColumnを作成しています。
この時のDataColumnの型は object 型になります。
もし厳密に型を合わせたいなら全データをスキャンしなければなりませんが、面倒なのでそこまではしていません。
isHeader が false の場合、列名を付けずに DataColumn を作成していますので、”Column1” のような列名が自動で作られます。
動的に列を追加
途中から列の数が変わるというケースは殆ど無いとは思いますが、私が扱うデータだとたまにあるので対応しました。
if (fields.Length > dt.Columns.Count)
{
dt.Columns.AddRange(Enumerable.Range(0, fields.Length - dt.Columns.Count).Select(i => new DataColumn()).ToArray());
}
これは結構便利で、とりあえずカンマかタブで区切られていれば、少々いい加減なデータであっても、DataTableに収めることができます。
DataTableにさえ入ってしまえば、あとのデータ加工は楽ですからね。
CSV書き込みはダブルクォートの扱いに注意
書き込みの方は読み込みに比べて簡単です。
DataTableの内容を取り出し、セルの中にカンマやタブが含まれていたらダブルクォートで前後を括り、ファイル出力するだけです。
そのため、ダブルクォートで括る必要があるかを判断するローカル関数として enclose_ifneed というのを作っています。
/// 必要ならば、文字列をダブルクォートで囲む
string enclose_ifneed(string p_field)
{
//ダブルクォートで括る必要があるかを確認
if (p_field.Contains('"') || p_field.Contains(',') || p_field.Contains('\r') || p_field.Contains('\n') ||
p_field.StartsWith(" ") || p_field.StartsWith("\t") || p_field.EndsWith(" ") || p_field.EndsWith("\t"))
{
//ダブルクォートが含まれていたら2つ重ねて、前後にダブルクォートを付加
return (p_field.Contains('"')) ? ("\"" + p_field.Replace("\"", "\"\"") + "\"") : ("\"" + p_field + "\"");
}
else
{
//何もせずそのまま返す
return p_field;
}
}
まとめ
今回はCSVを読み込んでDataTableを返すメソッドと、DataTableの内容をCSVファイルに出力するメソッドを持った CsvUtil というクラスを作成し、そのソースコードを掲載しました。
ネームスペースが CommonClass としているのは、どのプロジェクトにも使える共通クラスとしての位置づけだからです。
今後とも、共通クラスはこのネームスペースで作っていきたいと思います。
今回の記事が皆さんのお役に立てれば光栄です。
コメント