EPPlus/NPOIで既存ファイルを開き、保存する方法

安易に保存することはできないので、いろいろメモ。

■【EPPlus】ファイルパス or FileInfo

□ファイルパスの場合Saveで保存

using OfficeOpenXml;
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");

            using (var package = new ExcelPackage(filePath))
            {
                // 何か書き込み
                package.Workbook.Worksheets[0].Cells["A1"].Value = DateTime.Now.ToString("HH:mm:ss");

                package.Save();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

□FileInfoも同様にSaveで保存

using OfficeOpenXml;
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");
            var fileInfo = new FileInfo(filePath); // 何かしらのファイル情報を必要としている場合

            using (var package = new ExcelPackage(fileInfo))
            {
                // 何か書き込み
                package.Workbook.Worksheets[0].Cells["A1"].Value = DateTime.Now.ToString("HH:mm:ss");

                package.Save();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

ファイルパスや、FileInfoの場合、別プロセスからのアクセスを許容してしまうため、
同時に変更された場合、エラーになる
他からアクセスが想定される場合、Streamを使わざるを得ない
そこでStreamを使った書き込みをおさらいしてみる

[NG]StreamではSave

ClosedXMLはStreamで開いてもSaveで書き込み可能
同じように記載しても保存はされない

using Microsoft.Extensions.FileProviders;
using OfficeOpenXml;
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");

            // ブックを開く、読み書きモード、共有なし(排他)
            using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
            using (var package = new ExcelPackage(fileStream))
            {
                // 何か書き込み
                package.Workbook.Worksheets[0].Cells["A1"].Value = DateTime.Now.ToString("HH:mm:ss");

                // ClosedXMLなら、Saveで書き込みできるが・・・
                package.Save(); //エラーも起きず、ファイルの更新日時が変わっていないことから保存はできていない
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

[NG]StreamでSaveAs

using Microsoft.Extensions.FileProviders;
using OfficeOpenXml;
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");

            // ブックを開く、読み書きモード、共有なし(排他)
            using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
            using (var package = new ExcelPackage(fileStream))
            {
                // 何か書き込み
                package.Workbook.Worksheets[0].Cells["A1"].Value = DateTime.Now.ToString("HH:mm:ss");

                // エラーなく処理は終了する。更新日時は変わっているが・・・
                package.SaveAs(fileStream);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

開くとエラー

そもそも、同一stream上に書き込みを行うと、現在の位置から書き込み始めるので、おかしなことになる・・・

[NG]ファイル位置・内容を初期化

Seekの位置を先頭に戻し、内容を空っぽにしてから書き込みを行ってみる

using Microsoft.Extensions.FileProviders;
using OfficeOpenXml;
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");

            // 読み取り専用で開く(Excelのデータを取り込むためのStream)
            using (var readStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            // メモリ上にデータを取り込む
            using (var package = new ExcelPackage(readStream))
            {
                // 何か書き込み
                package.Workbook.Worksheets[0].Cells["A1"].Value = DateTime.Now.ToString("HH:mm:ss");

                // Excelデータを取り込んだら閉じる
                readStream.Close();

                // 書き込み用のStreamを開く
                using (var writeStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    // Seekの位置を先頭にし、データを空にする
                    writeStream.Seek(0, SeekOrigin.Begin);
                    writeStream.SetLength(0);

                    // 書き込み
                    package.SaveAs(writeStream);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

開いてみるとうまく書き込みている感じがします。

生成AIに確認すると・・・

「一見正しく見えるが、実は非推奨(危険)」なパターンです。

理由は次のとおりです。

🔍 問題の本質

💥 ExcelPackage の内部構造はストリームに依存

using (var readStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))

using (var package = new ExcelPackage(readStream))

{

readStream.Close(); // ← ここで物理的に閉じてしまう

...

package.SaveAs(writeStream); // ← 実はまだ読み取りストリームを使おうとする可能性あり

}

・ExcelPackage.Load(Stream) は ZIP アーカイブを内部的に展開して使いますが、

・一部の構成ファイルを「遅延読み込み」するため、ストリームを後からも参照しようとすることがあるんです。

・したがって、readStream.Close() したあとに SaveAs() を呼ぶと、未展開データにアクセスしようとして例外が起きる/壊れるリスクがあります。

🧪 実際に起きるエラー例

・IOException: Cannot access a closed stream

・System.InvalidOperationException: Cannot access Zip archive after stream is closed

・Excel ファイルが壊れて開けなくなる

[OK]メモリにExcelデータをすべて取り込んでから編集して保存する方法

using Microsoft.Extensions.FileProviders;
using OfficeOpenXml;
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;

namespace SampleCode; // C#10~

internal class SampleEPPlus
{
    static void Main(string[] args)
    {
        // Ver8.0のソースです
        // 非商用個人利用の場合 名前を設定
        ExcelPackage.License.SetNonCommercialPersonal("SampleTarou");

        try
        {
            // 実行ファイルのあるフォルダパス取得
            var folderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
            if (folderPath == null) { return; }

            // Excelファイルパスを作成
            var filePath = Path.Combine(folderPath, "sample.xlsx");

            // メモリにすべて取り込む。これなら遅延でStreamにアクセスするような問題が発生しない
            byte[] fileBytes = File.ReadAllBytes(filePath);

            using( var readStream = new MemoryStream(fileBytes))
            using (var package = new ExcelPackage(readStream))
            {
                package.Workbook.Worksheets[0].Cells["A1"].Value = DateTime.Now.ToString("HH:mm:ss");

                // あらためて書き込み用としてStreamを開く
                using (var writeStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    // 書き込み
                    package.SaveAs(writeStream);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

NPOIはそもそもStreamのみなので、同じ対応が必要

投稿日時: 2025-05-25 12:13:25

内部リンク

Comment

最近の投稿

最近のコメント

タグ

アーカイブ

その他