[C#][.NET] CSVファイルの書き出し(列とプロパティをバインディング、属性を使って)
CSVの読み込みができるようになったら、書き込みもできないといけないよね。
作りました。
CSV化は、例によって「Perl正規表現雑技」からいただきました。
感謝
2018/2/1 追記
いくらか手直しして、こちらにソースを置きました。
https://github.com/sengokyu/csvreader-csvwriter-for-dot-net
概要
列と行がある一般的なCSVを書き出します。
列数は固定です。
特長
- CSV列とクラスのプロパティを属性でバインディングします。
- 値中にあるカンマに対応しています。
- 値中にある改行に対応しています。
- 値中にあるダブルクォートに対応しています。
使い方
準備
CSVとして書き出したいクラスのプロパティに属性CSVColumn
を付与します。
SampleBean.cs
public class SampleBean
{
public string Ignored { get; set; }
[CSVColumn(1)]
public string Column1 { get; set; }
[CSVColumn(2, Name = "Original, \"Name")]
public string Column2 { get; set; }
[CSVColumn(3)]
public int MyNumber { get; set; }
}
属性の引数は2つです。
引数 | 必須 | 説明 |
---|---|---|
Order | 〇 | CSVとして書き出すときの列順を指定します。 |
Name | CSVファイルのヘッダに出力する名前を指定します。未指定の時はプロパティ名から自動生成します。 |
書き出し
あらかじめStream
を用意しておきます。
CSVに出力するクラスをジェネリクスで渡します。
あとはWriteHeaderLine
を呼べばヘッダが書き出され、WriteLine
を呼べば行が書き出されます。
IEnumebable<SampleBean> sampleBeans;
// 出力したいクラス群があるとするじゃろ。
// ...
using (var writer = new CSVWriter<SampleBean>(stream, Encoding.UTF8))
{
writer.WriteHeaderLine(); // ヘッダ行を書き出します。
foreach (var i in sampleBeans) {
writer.WriteLine(i); // 書き出し
}
writer.Close();
}
ソース
属性
CSVColumnAttribute.cs
/// <summary>
/// CSV column definition
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false)]
public class CSVColumnAttribute : System.Attribute
{
private readonly int _order;
public CSVColumnAttribute(int order)
{
_order = order;
}
public int Order { get { return _order; } }
public string Name { get; set; }
}
インターフェース
ICSVWriter.cs
/// <summary>
/// Write object to the stream as CSV
/// </summary>
public interface ICSVWriter<T> : IDisposable
{
/// <summary>
/// Write a header line
/// </summary>
void WriteHeaderLine();
/// <summary>
/// Write a line
/// </summary>
/// <param name="record"></param>
void WriteLine(T record);
}
コンクリートクラス
CSVWriter.cs
public class CSVWriter<T> : ICSVWriter<T>
{
private static readonly string DELIMITER = ",";
private readonly StreamWriter _writer;
private List<BindingProperty> _bindingProperties;
public CSVWriter(Stream stream, Encoding encoding)
{
_writer = new StreamWriter(stream, encoding);
_bindingProperties = extractBindingProperties();
}
private List<BindingProperty> extractBindingProperties()
{
var targetType = GetType().GetGenericArguments()[0];
return targetType
.GetProperties()
.Select(i => new BindingProperty()
{
Property = i,
CSVColumn = i.GetCustomAttributes(typeof(CSVColumnAttribute), false).FirstOrDefault() as CSVColumnAttribute
})
.Where(i => i.CSVColumn != null)
.OrderBy(i => i.CSVColumn.Order)
.ToList();
}
public void WriteLine(T record)
{
var values = _bindingProperties
.Select(i => i.Property.GetValue(record))
.Select(i => Quote(i))
.ToArray();
_writer.WriteLine(string.Join(DELIMITER, values));
}
public void WriteHeaderLine()
{
var headers = _bindingProperties
.Select(i => i.CSVColumn.Name ?? CamelCase2Title(i.Property.Name))
.Select(i => Quote(i))
.ToArray();
_writer.WriteLine(string.Join(DELIMITER, headers));
}
public void Flush()
{
_writer.Flush();
}
public void Close()
{
_writer.Close();
}
private string CamelCase2Title(string src)
{
return Regex.Replace(src, "(?<!^)([A-Z])(?![A-Z])", " ${1}");
}
private string Quote(object src)
{
string ssrc = src != null ? src.ToString() : "";
// via http://www.din.or.jp/~ohzaki/perl.htm#CSVfromValues
// join ',', map {(s/"/""/g or /[\r\n,]/) ? qq("$_") : $_} @values;
if (Regex.Match(ssrc, "[\"\\r\\n,]").Success)
{
return "\"" + ssrc.Replace("\"", "\"\"") + "\"";
}
else
{
return ssrc;
}
}
private class BindingProperty
{
internal PropertyInfo Property;
internal CSVColumnAttribute CSVColumn;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_writer.Dispose();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~CSVWriter() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
}
テスト
CSVWriterTests.cs
[TestClass]
public class CSVWriterTests
{
[TestMethod]
public void TestWriterHeaderLine()
{
using (var stream = new MemoryStream())
using (var instance = new CSVWriter<SampleBean>(stream, Encoding.UTF8))
{
instance.WriteHeaderLine();
instance.Flush();
stream.Seek(0, SeekOrigin.Begin);
var result = Encoding.UTF8.GetString(stream.ToArray());
Check.That(result).IsEqualTo("Column1,\"Original, \"\"Name\",My Number\r\n");
}
}
[TestMethod]
public void TestWriteLine()
{
var bean = new SampleBean()
{
Column1 ="value1",
Column2 ="value\nvalue,value\"",
MyNumber= 1234
};
using (var stream = new MemoryStream())
using (var instance = new CSVWriter<SampleBean>(stream, Encoding.UTF8))
{
instance.WriteLine(bean);
instance.Flush();
stream.Seek(0, SeekOrigin.Begin);
var result = Encoding.UTF8.GetString(stream.ToArray());
Check.That(result).IsEqualTo("value1,\"value\nvalue,value\"\"\",1234\r\n");
}
}
private class SampleBean
{
public string Ignored { get; set; }
[CSVColumn(1)]
public string Column1 { get; set; }
[CSVColumn(2, Name = "Original, \"Name")]
public string Column2 { get; set; }
[CSVColumn(3)]
public int MyNumber { get; set; }
}
}