C# の Excel ファイル操作ライブラリー

この記事を書いてかなり時間が経過したので修正をしました(2017-9-19)。Excel Data Readerが、Ver 3.0 から .NET Standard 1.3 に対応し、.NET Core アプリで利用できるようになりました。また、XLS SST parser が書き直されたので(Rewrite the XLS SST parser)ので、日本語の問題が解消しました。Excel Data Reader は、Excel のファイルからデータを取得したいという場合には、処理速度が早いのでベストの選択肢です。

Excel Data Reader の使い方について若干メモをしておきます。使い方としては、reader を使う方法と AsDataSet 拡張メソッドを使う方法がありますが、AsDataSet 拡張メソッドの方は、まだ、.NET Standard に対応していません。reader を使うサンプルを以下に書いておきます。プログラムは、xls と xlsx 共通です。

public static void ExcelDataReader(string inFile, string outFile)
{
    using (var stream = File.Open(inFile, FileMode.Open, FileAccess.Read))           
    using (var excelReader = ExcelReaderFactory.CreateReader(stream))
    {
        var sb = new StringBuilder();
        do
        {
            while (excelReader.Read())
            {
                for (int n = 0; n < excelReader.FieldCount; n++)
                {
                    var value = excelReader.GetValue(n);
                    if(value == null)
                        sb.Append("\t");
                    else if (value is double)
                    {
                        sb.Append(((double)value).ToString(CultureInfo.InvariantCulture) + "\t");
                    }
                    else if (value is string)
                    {
                        sb.Append(((string)value).ToString(CultureInfo.InvariantCulture) + "\t");
                    }
                    else if (value is DateTime)
                    {
                        sb.Append(((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss") + "\t");
                    }
                    else if (value is bool)
                    {
                        sb.Append(((bool)value).ToString(CultureInfo.InvariantCulture) + "\t");
                    }
                    else if (value is int)
                    {
                        sb.Append(((int)value).ToString(CultureInfo.InvariantCulture) + "\t");
                    }
                    else
                    {
                        sb.Append("\t");
                    }
                }
                sb.Append("\r\n");
            }
        } while (excelReader.NextResult());
        File.WriteAllText(outFile, sb.ToString());
    }
}

数値はすべて double で返ってきます。1 や 2 と入力しても内部的には数値はすべて double になっているそうです。空白は、null で返ります。int で返ってくるケースは、エラーがあったセルかと思ったのですが実際に試してみると、xls では null が、xlsx では エラーが文字で返ってきます。

他のライブラリーの .NET Core への対応状況については。NPOI は、.NET Statndard 2.0 に対応する DotNetCore.NPOI が公開されています。EPPlus には、非公式ですが .NET Standard 1.3 以上に対応する EPPlus.Coreが公開されています。また、ClosedXML は、次の ver0.9.0 で、.NET Standard 2.0 に対応する予定です。

こうしてみると、ライブラリーの .NETStandard が進み始めたというのがよくわかります。それから、これらのライブラリーをホスティングしてきたウェブサイト Codeplex がとうとう2017年末で閉鎖ということで、時代の流れを感じます。以下の記事は、古くなっているんですが、.NET Core への対応が落ち着いたら書き直します。

C# で Excel ファイルの読み込み及び書き出しの操作が可能な OSS のライブラリーをテストしてみました。テストしたライブラリーは以下の4つです。

1. NPOI

Java の Apacche POI を .NET に移植したものです。OOXML の xlsx 及び 97-2003 の xls ファイルの双方に対応しており、読み込みも書き出しもできる便利なライブラリーです。

問題点としては、xlsx ファイルを作成した場合、Excelでファイルを開くと、「'○○.xlsx'には、読み取れない内容が含まれています。このブックの内容を回復しますか?」というファイルが破損しているエラーが発生します。修復ボタンを押すと問題なく使えますが、あまり気分のいいものではないです。ライセンスは、Apache License 2.0 です。

2. EPPlus

2007/2010形式の xlsx ファイルの読み込み及び書き込みに対応しています。チャートや VBA を操作できる API もあり、Excel に関して言えば NPOI よりも機能は豊富です。処理速度については、ホームページで 50,000セルを1秒で読み込めるとありますが、今回のテストではその程度の性能は問題なくでていました。ライセンスは、LGPL ライセンスです。

3. ClosedXML

Microsoft 純正の Open XML SDK をそのままで使うのはあまりにも面倒なので、それを使いやすくしたライブラリーです。ベースとしてOpen XML SDK を使うので、対応する Excel のファイル形式は、2007/2010 形式になります。Open XML SDK がベースなので機能は豊富ですが、処理は少し重いです。ライセンスは、MIT ライセンスです。

4. Excel Data Reader

読み込み専用のソフトで、高速に処理できるのが特徴で、海外では人気があるライブラリーです。日本では、97-2003 形式の場合には、日本語がよく文字化けし、2007/2010 の xlsx ファイルの場合は、ふりがなつきで返ってきます。処理が速いだけに残念なのですが、日本語の場合はかなり使いづらいです。ライセンスは、MIT ライセンスです。

テストの方法

Excel ファイルの読み込みと書き出しの簡単なプログラムを書いてテストをしました。テストに使ったプログラムは一番最後においておきます。配列のインデックスが NPOI では 0 からですが、EPPlus と ClosedXML では 1 から始まるのが面白いです。

データのファイルについては、統計データをダウンロードして利用しました。6380行×17列、6160行×17列の2個のファイルで、合計約21万セルで、ファイル容量は xls で合計 3,171KB、xlsx で 合計 1,744KB です。

テスト結果

読み込みだけであれば、Excel Data Reader が抜群に速いです。サーバーで利用する場合には軽いというのは重要な要素ですが、残念なことに日本語の処理に問題があります。現状では、97-2003 形式の日本語の xls ファイルを扱う場合の選択肢は、NPOI しかないようです。2007/2010 形式の xlsx ファイルを扱う場合は、それぞれの特徴を踏まえて利用するライブラリーを選択するようにしたらいいと思います。もう少し処理が速いほうがいいとは思うのですが、それでもサーバーの処理で十分実用になる処理速度だと思います。(単位: ミリ秒)

読込(xls) 読込(xlsx) 書込(xls) 書込(xlsx)
NPOI 925 1,887 1,289 4,000
EPPlus - 3,084 - 4,987
ClosedXML - 38,174 - 45,813
Excel Data Reader 398 737 - -

ソース

private void ExcelDataXlsReader(string inFile, string outFile)
{
    FileStream stream = File.OpenRead(inFile);
    Excel.IExcelDataReader excelReader = Excel.ExcelReaderFactory.CreateBinaryReader(stream);
    DataSet result = excelReader.AsDataSet();
    stream.Close();
    var sb = new StringBuilder();
    foreach (DataRow datarow in result.Tables[0].Rows)
    {
        for (int c = 0; c < datarow.ItemArray.Length; c++)
            sb.Append(datarow[c].ToString().Replace("\n","") + "\t");
        sb.Append("\r\n");
    }
    File.WriteAllText(outFile, sb.ToString());
}
private void ExcelDataReader(string inFile, string outFile)
{
    FileStream stream = File.OpenRead(inFile);
    Excel.IExcelDataReader excelReader = Excel.ExcelReaderFactory.CreateOpenXmlReader(stream);
    DataSet result = excelReader.AsDataSet();
    stream.Close();
    var sb = new StringBuilder();
    foreach (DataRow datarow in result.Tables[0].Rows)
    {
        for(int c = 0; c < datarow.ItemArray.Length; c++)
            sb.Append(datarow[c].ToString().Replace("\n", "") + "\t");
        sb.Append("\r\n");
    }
    File.WriteAllText(outFile, sb.ToString());
}
private void NpoiXlsReader(string inFile, string outFile)
{
    FileStream stream = File.OpenRead(inFile);
    var book = new NPOI.HSSF.UserModel.HSSFWorkbook(stream);
    stream.Close();
    NPOI.SS.UserModel.ISheet sheet = book.GetSheetAt(0);
    int lastRowNum = sheet.LastRowNum;
    var sb = new StringBuilder();
    for (int r = 0; r < sheet.LastRowNum; r++)
    {
        var datarow = sheet.GetRow(r);
        {
            for (int c = 0; c < datarow.Cells.Count; c++)
                sb.Append(datarow.Cells[c].StringCellValue.Replace("\n", "") + "\t");
            sb.Append("\r\n");
        }
    }
    File.WriteAllText(outFile, sb.ToString());
}
private void NpoiReader(string inFile, string outFile)
{
    FileStream stream = File.OpenRead(inFile);
    var book = new NPOI.XSSF.UserModel.XSSFWorkbook(stream);
    stream.Close();
    NPOI.SS.UserModel.ISheet sheet = book.GetSheetAt(0);
    int lastRowNum = sheet.LastRowNum;
            
    var sb = new StringBuilder();
    for (int r = 0; r < sheet.LastRowNum; r++)
    {
        var datarow = sheet.GetRow(r);
        {
            foreach (NPOI.SS.UserModel.ICell t in datarow.Cells)
                sb.Append(t.StringCellValue.Replace("\n", "") + "\t");
            sb.Append("\r\n");
        }
    }
    File.WriteAllText(outFile, sb.ToString());
}
private void EpPlusReader(string infile, string outFile)
{
    FileInfo newFile = new FileInfo(infile);
    OfficeOpenXml.ExcelPackage pck = new OfficeOpenXml.ExcelPackage(newFile);
    OfficeOpenXml.ExcelWorksheet sheet = pck.Workbook.Worksheets[1];
                    
    var sb = new StringBuilder();
    int rows = sheet.Dimension.Rows;
    for (int r = 1; r <= rows; r++)
    {
        {
            for (int c = 1; c <= sheet.Dimension.Columns; c++)
                sb.Append(sheet.Cells[r, c].Text.Replace("\n", "") + "\t");
            sb.Append("\r\n");
        }
    }
    File.WriteAllText(outFile, sb.ToString());
}
private void ClosedXmlReader(string inFile, string outFile)
{
    var workbook = new ClosedXML.Excel.XLWorkbook(inFile);
    ClosedXML.Excel.IXLWorksheet sheet = workbook.Worksheets.Worksheet(1);
    var sb = new StringBuilder();
    var rows = sheet.LastRowUsed().RangeAddress.FirstAddress.RowNumber;
    for (int r = 1; r <= rows; r++)
    {
        {
            for (int c = 1; c <= sheet.LastColumnUsed().RangeAddress.FirstAddress.ColumnNumber; c++)
                sb.Append(sheet.Cell(r, c).GetString().Replace("\n", "") + "\t");
            sb.Append("\r\n");
        }
    }
    File.WriteAllText(outFile, sb.ToString());
}
private void NpoiXlsWriter(string inFile, string outFile)
{
    FileStream stream = File.OpenRead(inFile);
    var book = new NPOI.HSSF.UserModel.HSSFWorkbook(stream);
    //var book = new XSSFWorkbook(stream);
    stream.Close();
    NPOI.SS.UserModel.ISheet sheet = book.GetSheetAt(0);
    for (int r = 0; r < sheet.LastRowNum; r++)
    {
        var datarow = sheet.GetRow(r);
        {
            for (int c = 0; c < datarow.Cells.Count; c++)
                datarow.Cells[c].SetCellValue(r + c);
        }
    }
    FileStream streamw = File.OpenWrite(outFile);
    book.Write(streamw);
}
private void NpoiWriter(string inFile, string outFile)
{
    FileStream stream = File.OpenRead(inFile);
    var book = new NPOI.XSSF.UserModel.XSSFWorkbook(stream);
    stream.Close();
    NPOI.SS.UserModel.ISheet sheet = book.GetSheetAt(0);
    for (int r = 0; r < sheet.LastRowNum; r++)
    {
        var datarow = sheet.GetRow(r);
        {
            for (int c = 0; c < datarow.Cells.Count; c++)
                datarow.Cells[c].SetCellValue(r + c);
        }
    }
    FileStream streamw = File.OpenWrite(outFile);
    book.Write(streamw);
}
private void EpPlusWriter(string inFile, string outFile)
{
    FileInfo inFileInfo = new FileInfo(inFile);
    OfficeOpenXml.ExcelPackage pck = new OfficeOpenXml.ExcelPackage(inFileInfo);
    OfficeOpenXml.ExcelWorksheet sheet = pck.Workbook.Worksheets[1];
    for (int r = 1; r <= sheet.Dimension.Rows; r++)
    {
        {
            for (int c = 1; c <= sheet.Dimension.Columns; c++)
                sheet.Cells[r, c].Value = r + c;
        }
    }
    FileInfo outFileInfo = new FileInfo(outFile);
    pck.SaveAs(outFileInfo);
}
private void ClosedXmlWriter(string inFile, string outFile)
{
    var workbook = new ClosedXML.Excel.XLWorkbook(inFile);
    ClosedXML.Excel.IXLWorksheet sheet = workbook.Worksheets.Worksheet(1);
    for (int r = 1; r <= sheet.LastRowUsed().RangeAddress.FirstAddress.RowNumber; r++)
    {
        {
            for (int c = 1; c <= sheet.LastColumnUsed().RangeAddress.FirstAddress.ColumnNumber; c++)
                sheet.Cell(r, c).SetValue(c + r);;
        }
    }
    workbook.SaveAs(outFile);
}