はじめに
こんにちは。grnishiです。もう11月ですね。皆さん年末調整は提出しましたか?めんどくさいですけど人事に急かされる前に提出してしまいましょう。その気になればすぐ終わります。
本題
何かゲームを運用していると、データを集計して報告書を作るなんて仕事がありますよね。
例えばイベント期間の売上がどうだったか?とかユーザの状況はどうだったのか?とか。
サーバ側でMySQL叩いて集計する際などはCSVやTSVでデータを吐き出す事が多いと思います。
ただ報告書としてまとめるにはCSVやTSVだとちょっと寂しい。エクセルでそれなりに綺麗にしたいという欲求に駆られる事も多いかと思います。
そんな時は集計結果のデータをエクセルにペタっと貼り付けて体裁を整える。みたいな作業をします。
でもめんどくさいんですよね。いちいち色変えたりとか。罫線付けたりとか。
そんな時はエクセルを生成するプログラムを書けば良いじゃないかという事で、C#でCSVからエクセルを生成するプログラムを書いたりします。
そこで、いくつかあるエクセルそ操作するライブラリのベンチマークを測定しました。
世の中そんな事している人はたくさんいるのですが、2020年11月版としてまとめたいと思います。
比較ライブラリ
すべてNuGetでインストール
ライブラリ名 | バージョン |
---|---|
NPOI | v2.5.1 |
EPPlus | v5.4.1 |
OpenXML | v2.11.3 |
ClosedXML | v0.95.3 |
ExcelDataReader | v3.6.0 |
検証環境
- CPU: Core i7-9700K @3.60GHz
- RAM: 32GB
- SSD: 500GB
- Windows 10 Pro
- Visual Studio 2019
- .net framework 4.7.2
検証項目
読み込み
このサイトからもらったデータを使用しました。
ファイル名 | 件数 | 備考 |
---|---|---|
001.xlsx | 3件 | 適当に抜き出した |
002.xlsx | 8,816件 | 東京のデータ |
003.xlsx | 36,453件 | 関東のデータ |
004.xlsx | 149,703件 | 全国のデータ |
書き出し
A、B、C列に乱数、D列に「テスト文字列」と書き込み。
件数 |
---|
1000件 |
10000件 |
100,000件 |
1,000,000件 |
の4パターン
プログラム
using System; using System.IO; using System.Linq; using System.Text; using System.Diagnostics; namespace ExcelBench { class Program { public static void Main(string[] args) { if (args.Length != 3) { Console.WriteLine("引数が不正です"); Environment.Exit(0); } string rw = args[0]; string lib = args[1]; string input_filename = ""; int num = 0; if (rw == "read") { input_filename = args[2]; } else { num = int.Parse(args[2]); } ExcelLibInterface excel = GetExcelLibrary(lib); var sw = new Stopwatch(); sw.Start(); if (rw == "read") { if (!File.Exists(input_filename)) { throw new FileNotFoundException(); } var data = excel.Read(input_filename); } else if (rw == "write") { string output_filename = $"{lib}_{num}.xlsx"; excel.Write(output_filename, num); } else { throw new InvalidDataException(); } sw.Stop(); Console.WriteLine("{0} msec", sw.ElapsedMilliseconds.ToString()); Process currentProcess = Process.GetCurrentProcess(); currentProcess.Refresh(); Console.WriteLine("{0}", FormatSize(currentProcess.PeakWorkingSet64, 3)); } private static ExcelLibInterface GetExcelLibrary(string lib) { if (lib == "ExcelDataReader") { return new ExcelDataReaderTest(); } else if (lib == "Npoi") { return new NpoiTest(); } else if (lib == "EPPlus") { return new EPPlusTest(); } else if (lib == "OpenXML") { return new OpenXMLTest(); } else if (lib == "ClosedXML") { return new ClosedXMLTest(); } else { throw new InvalidOperationException("invalid library"); } } public static string FormatSize(long byte_number, int rounding) { if (byte_number >= Math.Pow(2, 80)) return Math.Round(byte_number / Math.Pow(2, 70), rounding).ToString() + " YB"; //yettabyte if (byte_number >= Math.Pow(2, 70)) return Math.Round(byte_number / Math.Pow(2, 70), rounding).ToString() + " ZB"; //zettabyte if (byte_number >= Math.Pow(2, 60)) return Math.Round(byte_number / Math.Pow(2, 60), rounding).ToString() + " EB"; //exabyte if (byte_number >= Math.Pow(2, 50)) return Math.Round(byte_number / Math.Pow(2, 50), rounding).ToString() + " PB"; //petabyte if (byte_number >= Math.Pow(2, 40)) return Math.Round(byte_number / Math.Pow(2, 40), rounding).ToString() + " TB"; //terabyte if (byte_number >= Math.Pow(2, 30)) return Math.Round(byte_number / Math.Pow(2, 30), rounding).ToString() + " GB"; //gigabyte if (byte_number >= Math.Pow(2, 20)) return Math.Round(byte_number / Math.Pow(2, 20), rounding).ToString() + " MB"; //megabyte if (byte_number >= Math.Pow(2, 10)) return Math.Round(byte_number / Math.Pow(2, 10), rounding).ToString() + " KB"; //kilobyte return byte_number.ToString() + " Bytes"; //byte } } }
using System; using System.IO; using System.Text; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; namespace ExcelBench { class NpoiTest : ExcelLibInterface { public string Read(string input_filename) { var st = File.OpenRead(input_filename); var wb = new XSSFWorkbook(st); st.Close(); var sb = new StringBuilder(); var ws = wb.GetSheetAt(0); int row_num = ws.LastRowNum; for (int i = 0; i <= row_num; i++) { var line = ws.GetRow(i); { foreach (var cell in line.Cells) { switch (cell.CellType) { case CellType.Numeric: sb.Append(cell.NumericCellValue.ToString() + "\t"); break; case CellType.String: sb.Append(cell.StringCellValue.Replace("\n", "") + "\t"); break; default: throw new Exception("?"); } } sb.Append("\n"); } } return sb.ToString(); } public void Write(string output_filename, int num) { var wb = new XSSFWorkbook(); var sh= wb.CreateSheet("Sheet1"); Random r = new Random(); // 指定行数書き込む for (int i = 0; i < num; i++) { var row = sh.CreateRow(i); for (int j = 0; j < 4; j++) { // ABCは数値。Dは文字列 if (j == 3) { var cellType = CellType.String; var cell = row.CreateCell(j); cell.SetCellType(cellType); cell.SetCellValue("テスト文字列"); } else { var cellType = CellType.Numeric; var cell = row.CreateCell(j); cell.SetCellType(cellType); cell.SetCellValue(r.Next(0, 100000)); } } } using (FileStream streamw = File.Open(output_filename, FileMode.Create)) { wb.Write(streamw); } } } }
using System; using System.IO; using System.Text; using OfficeOpenXml; using OfficeOpenXml.Style; using OfficeOpenXml.ConditionalFormatting; namespace ExcelBench { class EPPlusTest : ExcelLibInterface { public string Read(string input_filename) { ExcelPackage.LicenseContext = LicenseContext.NonCommercial; var new_file = new FileInfo(input_filename); var pck = new OfficeOpenXml.ExcelPackage(new_file); var ws = pck.Workbook.Worksheets[0]; var sb = new StringBuilder(); int rows = ws.Dimension.Rows; int cols = ws.Dimension.Columns; for (int r = 1; r <= rows; r++) { for (int c = 1; c <= cols; c++) { sb.Append(ws.Cells[r, c].Text.Replace("\n", "") + "\t"); } sb.Append("\n"); } return sb.ToString(); } public void Write(string output_filename, int num) { var pck = new ExcelPackage(); var sh = pck.Workbook.Worksheets.Add("Sheet1"); Random r = new Random(); // 指定行数書き込む for (int i = 1; i <= num; i++) { for (int j = 1; j <= 4; j++) { // ABCは数値。Dは文字列 if (j == 4) { ExcelRange cell = sh.Cells[i, j]; cell.Value = "テスト文字列"; } else { ExcelRange cell = sh.Cells[i, j]; cell.Value = r.Next(0, 100000); } } } FileInfo fi = new FileInfo(output_filename); pck.SaveAs(fi); } } }
using System; using System.Linq; using System.Text; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; namespace ExcelBench { class OpenXMLTest : ExcelLibInterface { public string Read(string input_filename) { SpreadsheetDocument document = SpreadsheetDocument.Open(input_filename, false); var sb = new StringBuilder(); var wb_part = document.WorkbookPart; var string_table = wb_part.GetPartsOfType<SharedStringTablePart>().FirstOrDefault(); var sheet = wb_part.Workbook.Sheets.Elements<Sheet>().First(); var ws_part = wb_part.GetPartById(sheet.Id) as WorksheetPart; var ws = ws_part.Worksheet; foreach (var row in ws.Descendants<Row>()) { foreach (Cell cell in row) { string value = cell.InnerText; if (cell.DataType != null) { switch (cell.DataType.Value) { case CellValues.Number: case CellValues.String: sb.Append(cell.InnerText.Replace("\n", "") + "\t"); break; case CellValues.SharedString: if (string_table != null) { sb.Append(string_table.SharedStringTable.ElementAt(int.Parse(value)).InnerText.Replace("\n", "") + "\t"); } break; default: break; } } } sb.Append("\n"); } return sb.ToString(); } public void Write(string output_filename, int num) { SpreadsheetDocument doc = SpreadsheetDocument.Create(output_filename, SpreadsheetDocumentType.Workbook, true); WorkbookPart wbpart = doc.AddWorkbookPart(); wbpart.Workbook = new Workbook(); WorksheetPart wspart = wbpart.AddNewPart<WorksheetPart>(); SheetData sheetData = new SheetData(); wspart.Worksheet = new Worksheet(sheetData); Sheets sheets = wbpart.Workbook.AppendChild<Sheets>(new Sheets()); // シートを1つ追加 Sheet sheet = new Sheet() { Id = wbpart.GetIdOfPart(wspart), SheetId = 1, Name = "Sheet1" }; sheets.Append(sheet); Random r = new Random(); // 指定行数書き込む for (int i = 1; i <= num; i++) { Row row = new Row(); for (int j = 1; j <= 4; j++) { // ABCは数値。Dは文字列 if (j == 4) { Cell cell = new Cell(); cell.DataType = CellValues.String; cell.CellReference = convertColumnAlphabet(j) + i.ToString(); cell.CellValue = new CellValue("テスト文字列"); row.Append(cell); } else { Cell cell = new Cell(); cell.DataType = CellValues.Number; cell.CellReference = convertColumnAlphabet(j) + i.ToString(); cell.CellValue = new CellValue(r.Next(0, 100000).ToString()); row.Append(cell); } } sheetData.Append(row); } doc.Close(); } public string convertColumnAlphabet(int column) { string alphabet = "ZABCDEFGHIJKLMNOPQRSTUVWXY"; string column_str = string.Empty; int m = 0; do { m = column % 26; column_str = alphabet[m] + column_str; column = column / 26; if (m == 0) { column -= 1; } } while (0 < column); return column_str; } } }
using System; using System.Text; using ClosedXML.Excel; namespace ExcelBench { class ClosedXMLTest : ExcelLibInterface { public string Read(string input_filename) { var wb = new XLWorkbook(input_filename); var sb = new StringBuilder(); foreach (var ws in wb.Worksheets) { foreach (var data_row in ws.RowsUsed()) { foreach (var cell in data_row.CellsUsed()) { sb.Append(cell.Value.ToString().Replace("\n", "") + "\t"); } sb.Append("\n"); } } return sb.ToString(); } public void Write(string output_filename, int num) { var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("Sheet1"); Random r = new Random(); // 指定行数書き込む for (int i = 1; i <= num; i++) { for (int j = 1; j <= 4; j++) { // ABCは数値。Dは文字列 if (j == 4) { var cell = ws.Cell(i, j); cell.Value = "テスト文字列"; } else { var cell = ws.Cell(i, j); cell.Value = r.Next(0, 100000); } } } wb.SaveAs(output_filename); } } }
using System; using System.Data; using System.IO; using System.Text; using ExcelDataReader; namespace ExcelBench { class ExcelDataReaderTest : ExcelLibInterface { public string Read(string input_filename) { var st = File.Open(input_filename, FileMode.Open, FileAccess.Read); var er = ExcelReaderFactory.CreateReader(st); var data_set = er.AsDataSet(); st.Close(); var sb = new StringBuilder(); foreach (DataRow data_row in data_set.Tables[0].Rows) { foreach (var obj in data_row.ItemArray) { sb.Append(obj.ToString().Replace("\n", "") + "\t"); } sb.Append("\r\n"); } er.Close(); return sb.ToString(); } public void Write(string output_file, int num) { } } }
using System; namespace ExcelBench { interface ExcelLibInterface { string Read(string input_filename); void Write(string ioutput_filename, int num); } }
検証結果
読み込みテスト(データ件数3件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 164ms | 23.973MB |
EPPlus | 113ms | 19.02MB |
OpenXML | 463ms | 27.996MB |
ClosedXML | 923ms | 37.23MB |
ExcelDataReader | 58ms | 17.551MB |
読み込みテスト(データ件数8,816件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 1039ms | 102.367MB |
EPPlus | 562ms | 43.008MB |
OpenXML | 8445ms | 67.043MB |
ClosedXML | 2331ms | 67.281MB |
ExcelDataReader | 431ms | 32.137MB |
読み込みテスト(データ件数36,453件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 3415ms | 287.543MB |
EPPlus | 1820ms | 92.695MB |
OpenXML | 155054ms | 162.34MB |
ClosedXML | 6285ms | 143.707MB |
ExcelDataReader | 1431ms | 67.742MB |
読み込みテスト(データ件数149,703件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 13097ms | 1.03GB |
EPPlus | 6789ms | 273.48MB |
OpenXML | 2669582ms | 555.484MB |
ClosedXML | 23869ms | 443.676MB |
ExcelDataReader | 5688ms | 183.871MB |
書き出しテスト(1,000件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 242ms | 26.844MB |
EPPlus | 229ms | 23.215MB |
OpenXML | 129ms | 22.539MB |
ClosedXML | 913ms | 39.637MB |
書き出しテスト(10,000件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 410ms | 40.613MB |
EPPlus | 374ms | 32.309MB |
OpenXML | 256ms | 34.059MB |
ClosedXML | 1292ms | 58.316MB |
書き出しテスト(100,000件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 2270ms | 157.801MB |
EPPlus | 1799ms | 102.012MB |
OpenXML | 2171ms | 126.289MB |
ClosedXML | 5585ms | 233.367MB |
書き出しテスト(1,000,000件)
ライブラリ | 実行時間 | 使用メモリ |
---|---|---|
NPOI | 19975ms | 1.284GB |
EPPlus | 16267ms | 245.676MB |
OpenXML | 20314ms | 1005.051MB |
ClosedXML | 47604ms | 1.868GB |
結果
読み込み最速はExcelDataReaderでした。とはいえ名前の通り読み込みでしか使えないので用途を選びます。
書き出し最速はEPPlusでした。読み込みもExcelDataReaderを除けばEPPlusが良さそうです。
しっかりと調べたわけじゃないですが、ライブラリによっては出来る事、出来ない事があるでしょうし、
罫線や色付け、数式を入れたりなど見栄えを調整しようとするとパフォーマンスは変わるかもしれませんし、
APIのインターフェース的に使いやすかったり使いづらかったりもあると思います。
この結果が全てでは無いです。
また、極力外的影響を受けないようにWindowsをクリーンインストールしたあと、VisualStudioしか入れてない状態で計測していますが、
たまたま余計なプロセスが動いていたという可能性も否定できません(一応10回ぐらい実行して最速、最遅の2つを除いた8回分の平均を取っていますが)
さいごに
最低限のプログラムを書いてベンチマークを取ってみました。プログラムを最適化していないのでもっと効率良くかける部分があるかもしれません。それによりベンチマーク結果が変わる事も十分にありえます。
実際は色を付けたり罫線を引いたり、数式を入れたりシートが複数あったりと異なる結果になる事もあるかと思います。
まずは手を動かす事が重要だなと思いました。
告知
弊社からゲームアプリがリリースされました。
簡単操作で楽しめるジャンプアクションゲームなっています。
かわいいニワトリが沢山登場しますよ。
apps.apple.com