gracetory’s blog

東池袋にある合同会社グレストリのエンジニアブログです

C#のエクセルライブラリのパフォーマンスを比較してみた

f:id:grnishi:20201118235716p:plain

はじめに

こんにちは。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

検証項目

読み込み

jusyo.jp

このサイトからもらったデータを使用しました。

ファイル名 件数 備考
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回分の平均を取っていますが)

さいごに

最低限のプログラムを書いてベンチマークを取ってみました。プログラムを最適化していないのでもっと効率良くかける部分があるかもしれません。それによりベンチマーク結果が変わる事も十分にありえます。

実際は色を付けたり罫線を引いたり、数式を入れたりシートが複数あったりと異なる結果になる事もあるかと思います。

まずは手を動かす事が重要だなと思いました。