C# から Excel ファイルを読みだすために利用できるライブラリに ClosedXML というライブラリがあります。ここでは、このライブラリを使用して、さらに簡便に Excel ファイルを読み書きするためのクラスを紹介します。
ここで紹介するクラスの特徴は、以下になります。
ClosedXML ライブラリを使用するには、 Visual Studio の NuGet パッケージの機能を利用して、必要なライブラリをダウンロード・組み込んでください。私が利用したライブラリのバージョンは 0.94.2.0 となっていました。
ここで紹介しているライブラリの読み書きには、Csv ファイルの読み書きと同様に、標準クラスの DataTable を使用しています。これは、さらに大きなライブラリから、これらのクラスを利用しているため、 メソッドのインターフェースを共通にしてあります。いずれ、そのさらに大きなライブラリの紹介も行っていこうと思います。
それでは、 CSV ファイルを読み書きするためのクラス ExcelFile のソースを以下に示します。かなり長いプログラムですが、多くのコメントを残してあります。おおよその意味は読み取れるものと思います。
1 public class ExcelFile
2 {
/// <summary>
/// DataRow の列名と Excel のセルとの関係を定義したリスト
/// DataRow が5項目からなり、その列名が "One","Two","Three","Four","Five"であり
/// 対応する Excel のセルが、A10, B10, C10, D10, E10 である場合には、次のリストとなる
/// 読込み時のリスト
/// {{ "One", "A10" }, { "Two", "B10" }, { "Three", "C10" }, { "Four", "D10" }, { "Five", "E10" }}
/// この意味は、"A10" のセルの値を "One" の列名の項目に格納する、以下同様
/// 書出し時のリスト
/// {{ "A10", "One" }, { "B10", "Two" }, { "C10", "Three" }, { "D10", "Four" }, { "E10", "Five" }}
/// この意味は、"One" の列名の値を "A10" のセルに格納する、以下同様
///
/// 読込み時の処理
/// 上記のセルの値を読出して、DataRow に格納する
/// 以下 A11, B11, C11, D11, E11 の様に行番号を1つ増やして処理を繰り返す
/// AXX が空欄の場合に、読込み処理を中止する
/// 中止の判定方法を変更したい場合は、 IsEndCell() メソッドをオーバーライドする
/// (例:AXX が文字列"END"の場合や、全てのセルが空欄の場合など)
///
/// 書出し時の処理
/// 先頭の DataRow の値を対応するセルに書込む
/// 次の DataRow の値を、セルの行番号を1つ増やして処理を繰り返す
/// 最後の DataRow を書き出して処理を中止する
///
/// デフォルトのリストを作成するメソッドが用意されている
/// 読込み用のメソッド
/// MakeReadColumns()
/// 使用方法は、メソッドのコメントを参照せよ
///
/// 書出し用のメソッド
/// MakeWriteColumns()
/// 使用方法は、メソッドのコメントを参照せよ
/// </summary>
3 public List<KeyValuePair<String, String>> Columns
4 {
5 get; set;
6 }
/// <summary>
/// 読み取り時に何番目のシートを読み取るか(書出し時には使用しない)
/// デフォルトは1(最初のシート)
/// </summary>
7 public int Sheet
8 {
9 get; set;
10 }
/// <summary>
/// Excel のファイル名
/// </summary>
11 public String Path
12 {
13 get; set;
14 }
/// <summary>
/// 書出し時の書込みモードを指定する(読み取り時には使用しない)
/// 上書きモードの場合(IsAppend == false)こちらがデフォルトの動作
/// Path が指定する Excel ファイルを新規に作成し、先頭のシートに書込む
/// シートの総数は1増える
/// 追加モードの場合(IsAppend == true)
/// Path が指定する Excel ファイルが存在していること
/// 新規のシートを IsFirst に応じて追加し、そのシートに書込む
/// シートの総数は1増える
/// </summary>
15 public bool IsAppend
16 {
17 get; set;
18 }
/// <summary>
/// 書込みモードが追加モードの場合のみ有効となる
/// IsFirst == true の場合、先頭にシートを追加する
/// IsFirst == false の場合、最後にシートを追加する(デフォルト値)
/// 途中に挿入するモードはサポートしていない
/// </summary>
19 public bool IsFirst
20 {
21 get; set;
22 }
/// <summary>
/// 書込み時に新規に作成するシートの名前
/// デフォルトは「出力結果」
/// 同じ名前が存在する場合、「出力結果-(x)」の書式で x が 1 からの連番でユニークとなる名前を探す
/// </summary>
23 public string SheetName
24 {
25 get; set;
26 }
/// <summary>
/// 処理結果
/// </summary>
27 public String Result
28 {
29 get; set;
30 }
31 public ExcelFile(String name = "")
32 {
33 internalClear();
34 Path = name;
35 }
36 public virtual void Clear()
37 {
38 internalClear();
39 }
/// <summary>
/// Excel ファイルからデータテーブルに読み取る
/// </summary>
40 public virtual bool Read(DataTable table)
41 {
42 try
43 {
44 if (Path.Length == 0)
45 throw new FileNotFoundException();
46 if (table.Columns.Count < 1)
47 throw new Exception("テーブルのスキーマが指定されていません。");
48 List<KeyValuePair<String, String>> columns = Columns;
49 if (columns == null || columns.Count == 0)
50 throw new Exception("テーブルのコラムとExcelファイルの関連が指定されていません。");
51 return Read(columns, table);
52 }
53 catch (Exception ex)
54 {
55 Result = ex.Message;
56 return false;
57 }
58 }
/// <summary>
/// データテーブルの内容を Excel ファイルに書き出す
/// </summary>
59 public virtual bool Write(DataTable table)
60 {
61 try
62 {
63 if (Path.Length == 0)
64 throw new FileNotFoundException();
65 if (IsAppend == true && !File.Exists(Path))
66 throw new FileNotFoundException(Path);
67 if (table.Columns.Count < 1)
68 throw new Exception("テーブルのスキーマが指定されていません。");
69 List<KeyValuePair<String, String>> columns = Columns;
70 if (columns == null || columns.Count == 0)
71 throw new Exception("テーブルのコラムとExcelファイルの関連が指定されていません。");
72 return Write(table, columns);
73 }
74 catch (Exception ex)
75 {
76 Result = ex.Message;
77 return false;
78 }
79 }
/// <summary>
/// データテーブルの内容を Excel ファイルに書き出す
/// テーブルと Excel のセルとの対応に従って書き出す
/// </summary>
/// <param name="columns">テーブルと Excel のセルとの関係</param>
80 protected virtual bool Write(DataTable table, List<KeyValuePair<String, String>> columns)
81 {
82 try
83 {
84 XLWorkbook book;
85 IXLWorksheet sheet;
86 if (!IsAppend) // 上書きモード
87 {
88 book = new XLWorkbook();
89 sheet = book.Worksheets.Add(SheetName);
90 }
91 else // 追加モード
92 {
93 book = new XLWorkbook(Path);
94 string name = uniqName(book, SheetName); // ユニークなシート名を探す
95 sheet = book.Worksheets.Add(name, IsFirst ? 1 : book.Worksheets.Count + 1);
96 }
97 int offset = 0;
98 foreach (DataRow row in table.Rows)
99 {
100 if (!Write(row, sheet, offset, columns))
101 return false;
102 ++offset;
103 }
104 if (!IsAppend)
105 {
106 book.SaveAs(Path);
107 }
108 else
109 {
110 book.Save();
111 }
112 return true;
113 }
114 catch (Exception ex)
115 {
116 Result = ex.Message;
117 return false;
118 }
119 }
/// <summary>
/// データロウの内容を Excel シートに書き出す
/// セルの名前が A10 である場合、列名は A , 行番号は (10 + offset) となる
/// </summary>
/// <param name="sheet">Excel シート</param>
/// <param name="offset">基準となるセルの列番号からのオフセット</param>
/// <param name="columns">データロウの列名とセルとの関係</param>
/// <param name="row">データを格納しているデータロウ</param>
120 protected virtual bool Write(DataRow row, IXLWorksheet sheet, int offset, List<KeyValuePair<String, String>> columns)
121 {
122 try
123 {
124 int at;
125 String col, val;
126 foreach (var c in columns)
127 {
128 if (!ParseCell(c.Key, out at, out col))
129 throw new Exception(String.Format("セルの書式が不正です。[{0}]", c.Key));
130 val = row[c.Value] == DBNull.Value ? "" : (String)row[c.Value];
131 sheet.Cell(at + offset, col).Value = val;
132 }
133 return true;
134 }
135 catch (Exception ex)
136 {
137 Result = ex.Message;
138 return false;
139 }
140 }
/// <summary>
/// Excel のデータを DataTable に読み込む
/// セルとデータテーブルの対応は第一引数を使用する
/// </summary>
/// <param name="columns">対応リスト</param>
/// <param name="table">読込み用データテーブル</param>
141 protected virtual bool Read(List<KeyValuePair<String, String>> columns, DataTable table)
142 {
143 if (! File.Exists(Path))
144 {
145 Result = String.Format("ファイルが存在しません。[{0}]", Path);
146 return false;
147 }
148 using (var book = new XLWorkbook(Path))
149 {
150 var sheet = book.Worksheets.Worksheet(Sheet);
151 DataRow row;
152 int offset = 0;
153 while (true)
154 {
155 if (IsEndCell(sheet, offset, columns[0].Value))
156 break;
157 row = table.NewRow();
158 if (!Read(sheet, offset, columns, row))
159 return false;
160 table.Rows.Add(row);
161 ++offset;
162 }
163 }
164 return true;
165 }
/// <summary>
/// Excell のシートから DataRow に読込む
/// セルとデータテーブルの対応は第一引数(オフセット)と第二引数を使用する
/// </summary>
/// <param name="sheet">Excel のシート</param>
/// <param name="offset">第三引数に加えるオフセット</param>
/// <param name="columns">対応リスト</param>
/// <param name="row">格納用の DataRow</param>
166 protected virtual bool Read(IXLWorksheet sheet, int offset, List<KeyValuePair<String, String>> columns, DataRow row)
167 {
168 try
169 {
170 int at;
171 String cell, val;
172 foreach (var c in columns)
173 {
174 if (!ParseCell(c.Value, out at, out cell))
175 throw new Exception(String.Format("セルの書式が不正です。{0}", c.Key + ":" + c.Value));
176 val = getRowCellValue(sheet, at + offset, cell);
177 row[c.Key] = val;
178 }
179 return true;
180 }
181 catch (Exception ex)
182 {
183 Result = String.Format("Excel の読出しに失敗しました。\r\n[{0}] [{1}]", Path, ex.Message);
184 return false;
185 }
186 }
/// <summary>
/// 指定したセル以降には、有効なセルがないかどうかを返す
/// セルの名前が A10 である場合、セルの列名は A, 行番号は(10 + offset)となる
/// 該当するセルが空欄の場合、それ以降のセルは無効なセルと判断する
/// </summary>
/// <param name="sheet">Excel シート</param>
/// <param name="offset">セルの行番号に対するオフセット</param>
/// <param name="column">セルの名前</param>
187 protected virtual bool IsEndCell(IXLWorksheet sheet, int offset, String column)
188 {
189 String val = getCellValue(sheet, offset, column);
190 return String.IsNullOrEmpty(val);
191 }
/// <summary>
/// Excel の列名のリストを返す
/// count == 3, start == "A" の場合、{ "A", "B", "C" } を返す
/// count == 4, start == "AB" の場合、{ "AB", "AC", "AD", "AE" } を返す
/// </summary>
/// <param name="count">必要な列数</param>
/// <param name="start">最初の列名</param>
192 public static List MakeExcelColums(int count, string start = "")
193 {
194 string alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
195 int cnt = 0, all = 0;
196 string txt;
197 bool isOk = string.IsNullOrEmpty(start) ? true : false;
198 List<string> columns = new List<string>();
199 foreach (var ch in alpha)
200 {
201 ++all;
202 txt = new string(ch, 1);
203 if (!isOk)
204 {
205 if (txt.Equals(start))
206 isOk = true;
207 else
208 continue;
209 }
210 columns.Add(txt);
211 ++cnt;
212 if (cnt >= count)
213 return columns;
214 }
215 char[] char2 = new char[2];
216 foreach (var ch1 in alpha)
217 {
218 char2[0] = ch1;
219 foreach (var ch2 in alpha)
220 {
221 ++all;
222 char2[1] = ch2;
223 txt = new string(char2);
224 if (!isOk)
225 {
226 if (txt.Equals(start))
227 isOk = true;
228 else
229 continue;
230 }
231 columns.Add(txt);
232 ++cnt;
233 if (cnt >= count)
234 return columns;
235 }
236 }
237 int max = 16384;
238 char[] char3 = new char[3];
239 foreach (var ch1 in alpha)
240 {
241 char3[0] = ch1;
242 foreach (var ch2 in alpha)
243 {
244 char3[1] = ch2;
245 foreach (var ch3 in alpha)
246 {
247 ++all;
248 char3[2] = ch3;
249 txt = new string(char3);
250 if (!isOk)
251 {
252 if (txt.Equals(start))
253 isOk = true;
254 else
255 continue;
256 }
257 columns.Add(txt);
258 ++cnt;
259 if (cnt >= count)
260 return columns;
261 if (all >= max)
262 return null;
263 }
264 }
265 }
266 return columns;
267 }
/// <summary>
/// Excel のセル名を列名と行番号に分割する
/// </summary>
/// <param name="value">セル名</param>
/// <param name="at">行番号</param>
/// <param name="col">列名</param>
268 public static bool ParseCell(String value, out int at, out String col)
269 {
270 at = ParseRow(value);
271 col = ParseColumn(value);
272 return at >= 0 && !String.IsNullOrEmpty(col);
273 }
/// <summary>
/// Excel のセル名のうち、行番号に該当する部分の値を返す
/// セル名が A10 の場合、10 を返す
/// </summary>
/// <param name="cell"></param>
/// <returns>無効なセル名の場合、-1 を返す</returns>
274 public static int ParseRow(String cell)
275 {
276 String nums = "0123456789";
277 int index = cell.IndexOfAny(nums.ToCharArray());
278 if (index <= 0)
279 return -1;
280 int val = -1;
281 try
282 {
283 val = int.Parse(cell.Substring(index));
284 return val;
285 }
286 catch (Exception)
287 {
288 return -1;
289 }
290 }
/// <summary>
/// Excel のセル名のうち、列名に該当する部分の文字列を返す
/// セル名が A10 の場合、A を返す
/// </summary>
/// <param name="cell"></param>
/// <returns>無効なセル名の場合、空文字列を返す</returns>
291 public static string ParseColumn(String cell)
292 {
293 String nums = "0123456789";
294 int index = cell.IndexOfAny(nums.ToCharArray());
295 if (index <= 0)
296 return String.Empty;
297 return cell.Substring(0, index);
298 }
/// <summary>
/// 読込み用の DataRow と Excell のセルの対応リストを返す
/// リストの要素数は、table のコラム数分
/// 先頭のセルは、引数の cell
/// それ以降のセルは、次の列名と同じ行番号を自動的に作成する
/// table のコラム名が { "One", "Two", "Three" } の3つで、 cell が "A1" の場合、
/// {{ "One", "A1" }, { "Two", "B1" }, { "Three", "C1" }} となるリストを返す
/// </summary>
/// <param name="table"></param>
/// <param name="cell"></param>
299 public static List<KeyValuePair<string, string>> MakeReadColumns(DataTable table, string cell)
300 {
301 if (table.Columns.Count == 0 || string.IsNullOrEmpty(cell))
302 return null;
303 return MakePairColumns(table, cell, true);
304 }
/// <summary>
/// 書出し用の DataRow と Excell のセルの対応リストを返す
/// リストの要素数は、table のコラム数分
/// 先頭のセルは、引数の cell
/// それ以降のセルは、次の列名と同じ行番号を自動的に作成する
/// table のコラム名が { "One", "Two", "Three" } の3つで、 cell が "A1" の場合、
/// {{ "A1", "One" }, { "B1", "Two" }, { "C1", "Three" }} となるリストを返す
/// </summary>
/// <param name="table"></param>
/// <param name="cell"></param>
305 public static List<KeyValuePair<string, string>> MakeWriteColumns(DataTable table, string cell)
306 {
307 if (table.Columns.Count == 0 || string.IsNullOrEmpty(cell))
308 return null;
309 return MakePairColumns(table, cell, false);
310 }
311 private static List<KeyValuePair<string, string>> MakePairColumns(DataTable table, string cell, bool isRead)
312 {
313 int at = 0;
314 string alpha = string.Empty;
315 if (!ExcelFile.ParseCell(cell, out at, out alpha))
316 return null;
317 List<string> names = ExcelFile.MakeExcelColums(table.Columns.Count, alpha);
318 if (names == null)
319 return null;
320 int index = 0;
321 string atTxt = at.ToString();
322 List<KeyValuePair<string, string>> columns = new List<KeyValuePair>string, string>>();
323 foreach (DataColumn col in table.Columns)
324 {
325 columns.Add(isRead ?
326 new KeyValuePair<string, string>(col.ColumnName, names[index] + atTxt) : // 読込み用
327 new KeyValuePair<string, string>(names[index] + atTxt, col.ColumnName)); // 書出し用
328 ++index;
329 }
330 return columns;
331 }
/// <summary>
/// Excel シートとオフセットとセル名(例:A10)を指定して、セルの値を文字列として返す
/// 上記のセル名の場合、セルの列名は A であり、行番号は、(10 + offset)となる
/// </summary>
332 private String getCellValue(IXLWorksheet sheet, int offset, String column)
333 {
334 int at;
335 String col;
// セル名を列名と行番号に分解する
336 if (!ExcelFile.ParseCell(column, out at, out col))
337 return null;
338 at += offset;
339 return getRowCellValue(sheet, at, col);
340 }
// 行番号(1~)と列名(A~)を指定して、セルの値を文字列として返す
341 private String getRowCellValue(IXLWorksheet sheet, int at, String value)
342 {
343 var cell = sheet.Cell(at, value);
345 return cell.Value.ToString();
346 }
347 protected String uniqName(XLWorkbook book, String name)
348 {
349 bool same = book.Worksheets.Any((s) => (s.Name).Equals(name));
350 if (!same)
351 return name;
353 String newName;
354 for (int i = 1; ; ++i)
355 {
356 newName = name + "(" + i + ")";
357 same = book.Worksheets.Any((s) => (s.Name).Equals(newName));
358 if (!same)
359 return newName;
360 }
361 }
362 private void internalClear()
363 {
364 Columns = null;
365 Path = string.Empty;
366 IsAppend = false;
367 IsFirst = false;
368 Sheet = 1;
368 SheetName = "出力結果";
369 Path = string.Empty;
370 Result = String.Empty;
371 }
372 }
次のような内容の Excel ファイルを読み込むとします。
B2, C2, D2 の各セルに1つ目のデータがあり、B3, C3, D3 の各セルに2つ目のデータがあります。
読み込む先の DataRow には、次のようなコラム名が付けてあります。
この状態で、以下のようなプログラムにより、Excel の内容を読み取ることができます。
1 DataTable table = new DataTable();
2 ExcelFile excel = new ExcelFile("ファイルのパス名");
3 excel.Columns = new List<KeyValuePair<String, String>> {
4 new KeyValuePair<string, string>("1", "B2"),
5 new KeyValuePair<string, string>("2", "C2"),
6 new KeyValuePair<string, string>("3", "D2")};
7 excel.Read(table);
1行目 | 読み込む DataTable のインスタンスを作成します コラムの作成部分は省略していますが、3つのコラムを作成してください |
4行目 | Excel の B2 セルの内容を、 "1" のコラムに読みこみます |
5~6行目 | C2 セルの内容を "2" のコラムに、D2 セルの内容を "3" のコラムに読みこみます |
7行目 | Columns のリストに従って、順次セルを読みだします |
読みだした結果は次のようになります。
最初の DataRow には、 Excel の1つ目のデータが、次の DataRow には、 Excel の2つ目のデータが読みだされます。
Excel の2つ目のデータは、列名が同じで行番号が1つ増えたセルから読みだします。この場合は B3, C3, D3 のセルから読みだしますし、3つ目のデータは B4, C4, D4 となります。読みだしを終了するのはいつでしょうか?
それは、1行の先頭のセル Bxx (ここで、xx は数字です)セルが空欄だった時になります。この例題では、B4 のセルが空欄ですので、この時点で終了します。
この終了条件は変更することができます。このクラスから派生したクラスを定義してください。そのクラスで、 IsEndCell() メソッドを再定義することで、終了条件を変更することができます。読みだしを終了したい場合に、 true を返すようにしてください。
このサンプルでは、 Excel のセルと DataTable のコラムの関係を1行分すべて定義しています。通常の使い方では、読みだしを開始するセルを1つだけ定義すれば、読みだすセルの列数は DataTable の列と同じ数だけ読みだし、 次は先頭の次のセルを読みだすと思います。そして、先頭の行が空欄になった場合に読みだしを終了するのが普通でしょう。そのための方法も用意されています。以下のようにしてください。
1 DataTable table = new DataTable();
2 ExcelFile excel = new ExcelFile("ファイルのパス名");
3 excel.Columns = ExcelFile.MakeReadColumns(table, "B2");
4 excel.Read(table);
MakeReadCoulmns() メソッドを呼び出すことで、先頭のセルの番号を指定するだけで、必要なリストを作成することができます。
こんどは、書き込みのサンプルです。DataTable に格納されているデータを Excel ファイルに書き出すことになります。
次のような DataRow を想定してください。
次のようなプログラムで Excel に書き込むことができます。
1 DataTable table = new DataTable();
2 ExcelFile excel = new ExcelFile("ファイルのパス名");
3 excel.Columns = new List<KeyValuePair<String, String>> {
4 new KeyValuePair<string, string>("B2", "1"),
5 new KeyValuePair<string, string>("C2", "2"),
6 new KeyValuePair<string, string>("C2", "3")};
7 excel.Write(table);
1行目 | 書き込み用の DataTable のインスタンスを作成します DataRow の作成は省略しています |
4~6行目 | DataRow のコラムとセルの対応を定義しています.キーと値の順番が読み込み時とは逆になっています |
7行目 | Excel に書き出します |
書き込み時には、読みだし時とは違う考慮が必要になります。
こうした動作を指定することができます。Sheet, IsAppend, IsFirst, SheetName の各プロパティを確認してください。