背景
業務用データを AWS Lambda 上で Excel ファイルとして生成し、他社のExcelファイルを読み込んで処理するデスクトップアプリケーション にアップロードするという処理を開発していました。
当初、PHPSpreadsheet を使って生成した Excel ファイルをアップロードしたところ、問題なく読み込まれました。
その後、Lambda によるサーバーレス化を進める中で、Python を使って同等の機能を実現する必要が生じました。 そこで openpyxl
を用いて同じ構造の Excel ファイルを生成し、同アプリにアップロードしたところ、
❌ ファイル形式が不正です
というエラーが表示されました。
Excel自体では正常に開くことができ、レイアウトも PHPSpreadsheet 版とほぼ同一でした。 にもかかわらず、アプリ側では受け付けてもらえません。
この現象について調査を行い、Excelファイル内部の構造に違いがあることが判明しました。
問題の再現コード
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.append(["商品名", "カテゴリ", "個数"])
ws.append(["いい感じの本", "実用書", "10"])
wb.save("output.xlsx")
このExcelファイルをアップロードしたところ、
❌ ファイル形式が不正です
さらに不可解なことに、Excel上で最後の行を削除して保存し直したファイルは問題なく読み込まれるという現象も確認されました。
XMLレベルで調査
.xlsx
ファイルを .zip
に変換し、sheet1.xml
を直接調査したところ、openpyxl が生成したセルの構造は次のようになっていました:
<c r="B2" t="inlineStr">
<is>
<t>1</t>
</is>
</c>
この t="inlineStr"
属性は「セルの文字列がインラインで直接書かれている」ことを意味します。
なぜこれが問題なのか?
一部の読み込み専用アプリケーションは、インライン文字列(inlineStr) に対応しておらず、 sharedString(t="s")形式のみを想定して実装されていることがあります。
実際に PHPSpreadsheet や Excel GUI で保存したファイルでは、次のような記述になります:
<c r="B2" t="s">
<v>1</v>
</c>
この場合、文字列の中身は sharedStrings.xml
に格納され、セルにはそのインデックスだけが入ります。
解決策: xlsxwriter
を使う
Python で sharedString 形式を出力するためには、xlsxwriter
を使うのが確実です。
import xlsxwriter
from io import BytesIO
wb_buffer = BytesIO()
wb = xlsxwriter.Workbook(wb_buffer, {'in_memory': True})
ws = wb.add_worksheet()
rows = [
["商品名", "カテゴリ", "個数"],
["いい感じの本", "実用書", "10"]
]
for i, row in enumerate(rows):
for j, val in enumerate(row):
ws.write(i, j, val)
wb.close()
このように xlsxwriter
を使えば:
- すべてのセルが sharedString 形式で書き込まれる
- UsedRange も過不足なく出力される
- 不要な空行や空セルも書き込まれない
結果、アプリ側でも正常に取り込まれるExcelファイルになります。
教訓
-
openpyxl
は便利だが、セル出力形式(inlineStr vs sharedString)に関する仕様が暗黙的なため注意が必要。 - Excel GUI や PHPSpreadsheet と見た目が同じでも、内部構造が違うことがある。
- 相手側アプリが仕様通りに動かないことを前提に設計する覚悟も必要。
-
.xlsx
を.zip
にしてsheet1.xml
やsharedStrings.xml
を確認するのがトラブル調査の第一歩。 - 特に読み込み側が古い・特殊・閉じたアプリケーションである場合、xlsxwriter の利用が無難な選択肢。
以上、「意外と落とし穴の多いExcelファイル生成とその仕様」についての一例でした。