人から受け取った xlsx のセルを 1 つ書き換えて返す、という作業を Python で自動化するとき、openpyxl で開いて保存し直すと、ファイルの中身が一部消えることがあります。Claude Code に Excel と Google スプレッドシートを同じ条件で操作させて比べる検証をしたときに見つけたので、確かめた結果と、消さずに書き換える方法をまとめます。比較の全体は元の記事に書きました。この記事では、保存し直しで何が消えるかと、xlsx の XML を直接書き換える方法に絞ります。
試したファイル
次の部品を入れた xlsx を作りました。
- テキストボックス 2 つ
- コメント 1 つ
- プルダウン(入力規則)1 つ
- フィルタの条件 1 つ
- 非表示の行 10 行
- カスタムプロパティ 1 つ
B2 のセル 1 つに名前を書き込み、4 通りの方法で保存しました。openpyxl は 3.1.5 です。
| 項目 | 元のファイル | openpyxl で保存し直し | XML を直接書き換え | スプレッドシートに変換して戻す |
|---|---|---|---|---|
| ファイル内の部品数 | 15 | 13 | 15 | 13 |
| テキストボックス | 2 | 0 | 2 | 2 |
| コメント | 1 | 1 | 1 | 1 |
| 入力規則 | 1 | 1 | 1 | 1 |
| フィルタの条件 | 1 | 1 | 1 | 0 |
| 非表示の行 | 10 | 10 | 10 | 20(全部) |
| カスタムプロパティ | 1 | 1 | 1 | 0 |
openpyxl で保存し直すと、テキストボックスが 2 つとも消えました。「ここに社印を押してください」のような案内が、エラーも出さずに消えます。シートに置いた画像は、別に試したところ、保存し直しても残りました。
スプレッドシートに変換して編集し、Excel に戻す方法も試しました。テキストボックスは残りましたが、フィルタの条件が消え、データの行がすべて非表示の状態で保存されました。
取引先から受け取ったヒアリングシートでも同じことが起きました。openpyxl で開いて保存し直しただけで、ファイル内の部品が 66 から 20 に減り、シート名も変わりました。
xlsx は XML を zip にまとめたもの
xlsx の中身は、XML のファイルを zip でまとめたものです。書き換えたいセルの XML だけを差し替えて、ほかのファイルは 1 バイトも変えずにコピーすれば、部品は何も失われません。上の表の「XML を直接書き換え」の列は、この方法の結果です。
import re, zipfile
from xml.sax.saxutils import escape
def set_cell(src, dst, sheet_xml, ref, text):
"""シートの XML の中で、セル 1 つだけを文字列で書き換える。ほかのファイルはそのままコピーする"""
row = re.sub(r"\D", "", ref)
value = escape(text) # & < > をエスケープしないと、ファイルが壊れる
def new_cell(attrs):
attrs = re.sub(r'\s+t="[^"]*"', "", attrs) # 型だけ差し替え、書式(s="…")は残す
return f'<c{attrs} t="inlineStr"><is><t xml:space="preserve">{value}</t></is></c>'
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename == sheet_xml:
s = data.decode()
if f'<c r="{ref}"' in s:
s = re.sub(rf'<c( r="{ref}"[^>]*?)(?:/>|>.*?</c>)',
lambda m: new_cell(m.group(1)), s, count=1, flags=re.S)
else: # セルが無ければ行の末尾に足す(右隣の列にセルが無い場合だけ使える)
s = re.sub(rf'(<row r="{row}"[^>]*>.*?)(</row>)',
lambda m: m.group(1) + new_cell(f' r="{ref}"') + m.group(2), s, count=1, flags=re.S)
data = s.encode()
zout.writestr(item, data)
set_cell("申込書.xlsx", "申込書_記入済み.xlsx", "xl/worksheets/sheet1.xml", "B2", "山田太郎")
気をつける点
書き込む値は、必ずエスケープしてから XML に入れます。「田中&鈴木」のように & や < を含む値をそのまま入れると、openpyxl では開けないファイルになりました。LibreOffice ではエラーにならず、その値だけが消えていました。壊れたことに気づきにくいので、エスケープは省かないでください。
元のセルに付いている書式の指定(s="…")も残します。セルを丸ごと差し替えると、記入欄の太字や背景色、罫線が消えます。上の関数で、書式付きのセルや & を含む値を書き込んでも、値と書式がそのまま残ることを確かめました。
この関数は、文字列を 1 か所書き込む最小限の例です。数値や日付を入れる場合や、シート名から XML のファイル名を調べる場合は、処理を足す必要があります。
AI エージェントに任せるとき
Claude Code などに記入を任せるときは、指示に次の 2 点を入れておくと安全です。
- openpyxl で開いて保存し直さない
- XML のセルだけを差し替える
人から受け取ったファイルを、AI が黙って作り直してしまうことがあるためです。
行や数式のずれ、画像の挿入、回数制限など、Excel とスプレッドシートの比較の全体は、Claude Code に Excel とスプレッドシート、どちらを作らせるか|15の操作と5つの観点で実測比較に書いています。
元記事
Claude Code に Excel とスプレッドシート、どちらを作らせるか|15の操作と5つの観点で実測比較
https://amanity.co.jp/news/claude-code-excel-vs-sheets
