はじめに (対象読者・この記事でわかること)

この記事は、JavaでExcelファイルを扱う業務システムの開発に携わっている方、またはこれから携わろうとしている方を対象としています。特に、Apache POIを使ってExcelファイルを読み込む処理を実装しているが、「読み取り専用のはずなのにファイルが更新されてしまう」という不可解な現象に悩まされている方におすすめです。

この記事を読むことで、Apache POIでExcelファイルを読み込む際にファイルが更新されてしまう原因が理解でき、読み取り専用で安全に処理するための具体的な実装方法を習得できます。また、ファイルのタイムスタンプが変わらないようにするためのベストプラクティスも紹介します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法とファイル入出力の知識 - Apache POIの基本的な使い方(Workbook、Sheet、Cellなどの基本概念)

Excelが更新される謎:なぜ読み取り専用でファイルが変わってしまうのか

Apache POIでExcelファイルを読み込むだけの処理を実装しているのに、ファイルの更新日時が変わってしまう、またはファイルサイズが変化してしまうという現象に遭遇したことはありませんか?これは非常に混乱を招く問題で、「自分は書き込み処理をしていないのに、なぜファイルが更新されるのだろう?」と疑問に思う開発者が多くいます。

この現象の背後には、Apache POIの内部的な動作メカニズムと、Excelファイルの構造に関する深い関係があります。実は、Apache POIはExcelファイルを単純に「読み込む」のではなく、内部的に一時的な処理を行っていることが原因なのです。

読み取り専用でExcelを扱う:問題の詳細と解決方法

問題の再現:実際に起きている現象

まず、実際にどのような現象が起きているのか、具体的なコード例を見てみましょう。

Java
// 読み取り専用と思われる処理 public class ExcelReader { public void readExcel(String filePath) { try (FileInputStream fis = new FileInputStream(filePath)) { Workbook workbook = WorkbookFactory.create(fis); // データの読み取り処理 Sheet sheet = workbook.getSheetAt(0); Row row = sheet.getRow(0); Cell cell = row.getCell(0); System.out.println("セルの値: " + cell.getStringCellValue()); // workbook.close() - この時点でファイルが更新される! } catch (Exception e) { e.printStackTrace(); } } }

上記のコードは、一見したところ、Excelファイルを読み取ってその内容を表示するだけの処理のように見えます。しかし、実際にはworkbook.close()を実行したタイミングで、元のExcelファイルが更新されてしまうことがあります。

原因の解明:なぜファイルが更新されるのか

この現象が起きる主な原因は以下の3つです:

1. 一時ファイルの作成と自動的な上書き保存

Apache POIは、大きなExcelファイルを扱う際にメモリ効率を良くするため、一時ファイルを作成することがあります。この一時ファイルは、元のファイルと同じディレクトリに作成され、処理終了時に元のファイルを上書き保存してしまいます。

2. 個人設定情報の更新

Excelファイルには、個人設定情報(最後に編集したユーザー、印刷設定、ウィンドウの状態など)が含まれています。Apache POIはこれらの情報を読み込む際に、内部的に更新してしまうことがあります。

3. 計算式の再計算

Excelファイル内に計算式が含まれている場合、Apache POIはそれらの計算式を再計算し、結果を保存しようとします。

解決策:読み取り専用モードでの正しい実装方法

それでは、実際にファイルを更新せずにExcelファイルを読み込むには、どうすればよいのでしょうか。以下に、信頼性の高い実装方法を紹介します。

解決策1:読み取り専用モードの使用

Java
public class SafeExcelReader { public void readExcelSafely(String filePath) { // 読み取り専用モードでファイルを開く try (InputStream fis = new FileInputStream(filePath)) { // 読み取り専用ワークブックを作成 Workbook workbook = WorkbookFactory.create(fis, null, true); // データの読み取り処理 Sheet sheet = workbook.getSheetAt(0); processSheet(sheet); // 読み取り専用なので、close()でファイルは更新されない workbook.close(); } catch (Exception e) { e.printStackTrace(); } } private void processSheet(Sheet sheet) { for (Row row : sheet) { for (Cell cell : row) { // セルの内容を読み取る処理 System.out.print(cell.toString() + "\t"); } System.out.println(); } } }

解決策2:ファイルのコピーを使用する

Java
public class ExcelReaderWithCopy { public void readExcelWithCopy(String originalFilePath) { // 一時ファイルを作成 Path tempFile = Files.createTempFile("excel_read_", ".xlsx"); try { // 元ファイルを一時ファイルにコピー Files.copy(Paths.get(originalFilePath), tempFile, StandardCopyOption.REPLACE_EXISTING); // 一時ファイルから読み込む try (InputStream fis = Files.newInputStream(tempFile)) { Workbook workbook = WorkbookFactory.create(fis); // データ処理 processWorkbook(workbook); workbook.close(); } } finally { // 一時ファイルを削除 Files.deleteIfExists(tempFile); } } private void processWorkbook(Workbook workbook) { // Excelファイルの処理 Sheet sheet = workbook.getSheetAt(0); // ... 処理 ... } }

解決策3:SAX APIを使用する(大容量ファイル向け)

大容量のExcelファイルを扱う場合は、SAX APIを使用することで、メモリ効率も良く、ファイルの更新も防げます。

Java
import org.apache.poi.xssf.eventusermodel.*; import org.apache.poi.xssf.model.SharedStringsTable; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class LargeExcelReader { public void readLargeExcel(String filePath) { try { OPCPackage pkg = OPCPackage.open(filePath, PackageAccess.READ); XSSFReader reader = new XSSFReader(pkg); SharedStringsTable sst = reader.getSharedStringsTable(); XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) reader.getSheetsData(); while (iter.hasNext()) { InputStream stream = iter.next(); processSheet(stream, sst); stream.close(); } pkg.close(); } catch (Exception e) { e.printStackTrace(); } } private void processSheet(InputStream stream, SharedStringsTable sst) { // SAXパーサーを使用してシートを処理 // この方法はファイルを更新しない } }

ベストプラクティス:ファイルのタイムスタンプを保持する

読み取り専用でExcelファイルを処理する際には、元のファイルのタイムスタンプも保持しておくと、後から「本当に更新されていないか」を確認できます。

Java
public class TimestampPreservingReader { public void readExcelPreservingTimestamp(String filePath) { File originalFile = new File(filePath); long originalModified = originalFile.lastModified(); // Excelファイルの読み取り処理 readExcelSafely(filePath); // タイムスタンプが変わっていないか確認 if (originalFile.lastModified() != originalModified) { System.err.println("警告: ファイルのタイムスタンプが変更されています"); // 必要に応じて、元のタイムスタンプに戻す originalFile.setLastModified(originalModified); } } }

ハマった点と回避策:実装時の注意点

実際の開発現場でよくある問題とその解決方法をいくつか紹介します。

問題1:ファイルがロックされる

Java
// 悪い例:ファイルがロックされて他のアプリケーションが開けない Workbook workbook = WorkbookFactory.create(new File(filePath));
Java
// 良い例:InputStreamを使用してファイルロックを回避 try (InputStream is = Files.newInputStream(Paths.get(filePath))) { Workbook workbook = WorkbookFactory.create(is); // 処理... }

問題2:メモリリーク

Java
// 悪い例:リソースの解放が不十分 Workbook workbook = WorkbookFactory.create(inputStream); // workbook.close()を忘れるとメモリリークが発生
Java
// 良い例:try-with-resourcesを使用 try (Workbook workbook = WorkbookFactory.create(inputStream)) { // 処理... } // 自動的にクローズされる

まとめ

本記事では、Apache POIでExcelファイルを読み込む際に、なぜファイルが更新されてしまうのか、その原因と解決方法について詳しく解説しました。

  • Excelファイルが更新されてしまう3つの主な原因(一時ファイルの作成、個人設定情報の更新、計算式の再計算)
  • 読み取り専用モードを使用した安全な実装方法
  • ファイルのコピーを使用する方法
  • 大容量ファイル向けのSAX APIの活用
  • タイムスタンプを保持するベストプラクティス

この記事を通して、Excelファイルを扱う業務システムを開発する際に、予期せぬファイルの更新を防ぎ、より安全で信頼性の高いアプリケーションを構築できるようになりました。

今後は、Apache POIの最新バージョンでの新機能や、PDFやCSVなど他のフォーマットとの連携方法についても記事にする予定です。

参考資料