C# その2 Advent Calendar 2019 の 11 日目の記事です。
はじめに
今まで、記事とか書いたこともないし、他の方々と違いレベルの低い話題なので、そこのところご承知おきください。
経緯
業務上、Excel ファイル にエクスポートされたデータを取り扱うことが多く、VBA でマクロを書くのもVBE がアレすぎて面倒なため、C#で処理したいなーと
今回作るにあたっては、読み込んで値を変える程度ですが、業務によっては読み込んだものを加工して転記したり、DB に登録したりすると思うので自分用にメモ
なお、今回ClosedXMLについては、2019/12/01時点の最新版を使ってます
サンプルデータ
こんな感じでサンプルデータを XLSX ファイルとして作成
Id | Name | Affiliated | Age | Position |
---|---|---|---|---|
1 | A1 | 総務部 | 28 | 部長 |
2 | B2 | 人事部 | 19 | 係長 |
3 | C3 | 開発部 | 34 | 課長 |
4 | D4 | 開発部 | 23 | 係長 |
5 | E5 | 製造部 | 18 | 部長 |
6 | F6 | 製造部 | 69 | 一般 |
サンプルに使えそうな人な名前が思いつかない
早速作成
ということで、DynamicObject を継承する形で作成
基本的には、TryGetMember と TrySetMember を override する
なお、ベースは IDictionaryとしてフィールド名にあたるものとその値を用意 (状況によっては、TrySetIndex、TrySetIndex も override してもいいのかも)
private readonly IDictionary<string, object> dictionary;
public DataRecord(IDictionary<string, object> dictionary) => this.dictionary = dictionary;
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (!IsTypeCheck(binder.Name, value)) return false;
dictionary[binder.Name] = value;
return true;
}
private bool IsTypeCheck(string key, object value)
{
// キーがないならNG
if (!dictionary.TryGetValue(key, out var result)) return false;
// 型が一致しない場合はNG
return IsTypeMatch(result.GetType(), value.GetType());
}
private bool IsTypeMatch(Type baseType, Type valueType)
=> valueType.Equals(baseType) || valueType.IsSubclassOf(baseType);
public override bool TryGetMember(GetMemberBinder binder, out object result)
=> dictionary.TryGetValue(binder.Name, out result);
読み取りとか行うものを
XLSX を ClosedXML で読み込み
項目名称の取得やデータ取得の仕方は Excel ファイルのデータ次第なので、そこは適宜に
今回は RangeUsed メソッドからテーブル変換し、 Fields プロパティから項目名称を、データについては DataRange プロパティを用いて取得します。
public IEnumerable<dynamic> ReadExcelData()
{
using (IXLWorkbook workbook = new XLWorkbook(Path))
{
IXLWorksheet worksheet = workbook.Worksheet(1);
// 項目名称の取得
var tables = worksheet.RangeUsed().AsTable();
var columnNames = tables.Fields.Select(field => field.Name);
var values = tables.DataRange.Rows();
// 生成開始
var generator = new DataRecordGenerator(columnNames, values);
return generator.Generate();
}
}
public IEnumerable<dynamic> Generate()
{
foreach (var row in rows)
{
var dic = columnNames.Select((name, index) => (name, index))
.Select(x => (x.name, row.Cell(x.index + 1).Value))
.ToDictionary(k => k.name, v => v.Value);
yield return new DataRecord(dic);
}
}
んで、テストコード
[TestMethod]
public void TestMethod1()
{
var excelControl = new ExcelControl(path);
var result = excelControl.ReadExcelData().ToArray();
foreach (var item in result)
{
Console.WriteLine($"{item.Id},{item.Name},{item.Affiliated},{item.Age},{item.Position}");
}
// 数値に関してはClosedXMLの読み取るとdoubleで取得される
result[0].Age = (double)50;
result[0].Affiliated = "役員";
result[0].Position = "執行役員";
Console.WriteLine($"{result[0].Id},{result[0].Name},{result[0].Affiliated},{result[0].Age},{result[0].Position}");
}
実行結果
1,A1,総務部,28,部長
2,B2,人事部,19,係長
3,C3,システム開発部,34,課長
4,D4,システム開発部,23,係長
5,E5,製造部,18,部長
6,F6,製造部,69,一般
1,A1,役員,50,執行役員
Excel の内容を読み込みすることに成功し、また書き換えた後の出力も問題ありません。
感想
今回はテキストにしただけですが、読み込みして書き換えできるようにすることができました。
そもそも、ClosedXML だと自動マッピングできるかといわれるとそこらへんは勉強不足です。(できるんだったらそっち使ったほうがいいかもです)
リフレクション使ってのマッピングということで自分で実装するのがベターなのかもしれません、というより実際にテストした際も実行速度については、普通に型マッピングしたほうが早かったですし
ただ、業務で使うとなると、データ都合上Excel 中に 50 列くらい普通に並ぶものもあると思うので、マッピングしたい項目数が多い場合だったり、とりあえず意識しとうないときとかは dynamic を使うこともありなのかもしれません。
(そもそも、項目が多い時点で「孟徳!なぜ俺がこんなものを見なきゃならん!」的な作業なので)
サンプルは以下
https://github.com/exactead/ExcelReaderDynamic
参考ソース・記事
・ClosedXML (https://github.com/ClosedXML/ClosedXML )
・「C# DynamicObjectの基本と細かい部分について」
(http://neue.cc/2010/05/06_257.html )
・「【C#】ClosedXML で Excel テーブルを IEnumerableオブジェクトに変換」
(https://qiita.com/penguinshunya/items/dd586b1e42b7a66e552e )