LoginSignup
14
16

More than 3 years have passed since last update.

[C#][.NET] CSVファイルの書き出し(列とプロパティをバインディング、属性を使って)

Last updated at Posted at 2017-02-24

CSVの読み込みができるようになったら、書き込みもできないといけないよね。

作りました。

CSV化は、例によって「Perl正規表現雑技」からいただきました。
感謝 :smile:

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; }
        }

    }
14
16
1

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
14
16