毎週の Excel レポートを自動化しようとして、Python などのライブラリでファイルを開き、値を書き込んで保存する。ところが保存したファイルを Excel で開くと、修復を求めるメッセージが出る、書式が崩れている、図や集計表が消えている。Excel 自動化で「ファイルが壊れる」のは、よくある失敗です。
frectas は、物流・運輸の会社で週次の債権レポートを自動化したとき、まさにこの問題に当たりました。既存の Excel ライブラリで開いて保存すると、ファイルが壊れたのです。そこで、xlsx を zip として開き、変更が必要な XML だけを書き換え、それ以外の部品は1バイトも変えずに戻す方式を取りました。この記事では、その方式と、壊れていないことの確かめ方を、一般的な技術の説明とあわせて書きます。
この記事の数字は、frectas の案件の記録(顧客への報告など)によるもので、区分を添えています。
導入前の作業と、導入後
| 項目 | 導入前 | 導入後 | 区分 |
|---|---|---|---|
| ファイルを開く | 開くだけで長い時間がかかる | — | 顧客報告記載 |
| 作業全体 | 毎週数時間 | シート追加から検算までの全工程が数秒で完了 | 顧客報告記載(検証中・実ファイルで確認中) |
| 結果の正しさ | — | 手作業の完成品と全項目が一致 | 顧客報告記載(検証中・実ファイルで確認中) |
「数秒」は仕組みが処理する時間、「毎週数時間」は人が作業していた時間です。測っているものが違うので、削減率には換算していません。数秒の処理の前後には、入力ファイルをそろえる、結果を確かめる、といった人の作業が残るからです。
作る前に、AI が要るかを決める
着手したとき、最初にしたのは業務の理解です。作業の指示書、作業の動画、作業前と作業後のファイル一式を分析し、毎週どこがどう変わっているかを調べました。
その結果、毎週の変更は、入力ファイルの値と、前週からの機械的な延長だけで決まると分かりました。人の判断が入る所がないのです。
結果が入力で決まる作業に、生成 AI の推論は要りません。入れると、次の問題が出ます。
- 同じ入力でも、結果が毎回少しずつ変わることがある
- 分からない所を推測で埋め、それが誤りだと気づけない
- 正しく動いたことを、毎回人が確かめ直す必要がある
そこで、AI の推論を使わない確定的な処理にしました。AI を使うかどうかの判断の基準は「物流・運輸の定型業務を AI 化した記録」に詳しく書いています。
なぜライブラリで開いて保存すると壊れるのか
xlsx は XML を zip にまとめたもの
xlsx の拡張子を .zip に変えて展開すると、中身が見えます。主な部品は次のとおりです。
| 部品 | 中身 |
|---|---|
[Content_Types].xml |
zip の中のどのファイルが何の部品かの一覧 |
_rels/.rels、xl/_rels/workbook.xml.rels |
部品どうしのつながり |
xl/workbook.xml |
シートの一覧と順番、名前の定義、計算の設定 |
xl/worksheets/sheet1.xml など |
シートごとのセルの値と数式 |
xl/sharedStrings.xml |
セルの文字列の一覧(セルは番号で参照する) |
xl/styles.xml |
書式の一覧(セルは番号で参照する) |
xl/calcChain.xml |
数式を計算する順番 |
xl/externalLinks/、xl/pivotTables/、xl/drawings/ など |
外部参照、ピボット、図やグラフ |
ライブラリは「読み込んで、一から書き出す」
多くの Excel ライブラリは、これらの部品を読み込んで自分のデータ構造に変え、保存のときに新しいファイルとして書き出します。値を1つ変えただけでも、ファイル全体が作り直されます。ライブラリが扱えない部品や設定は、書き出すときに落ちたり、形が変わったりします。
一般に、壊れやすいのは次のような所です。
| 壊れやすい所 | 起きやすいこと |
|---|---|
| 数式と計算結果 | 数式は残っても、保存されていた計算結果が消え、Excel 以外の画面で空に見える |
| 共有数式(同じ形の数式をまとめて持つ仕組み) | 元になるセルを書き換えると、同じ数式を共有していた他のセルの数式が消える |
| 書式・条件付き書式 | 書式の番号が振り直され、色や表示形式がずれる |
| 外部参照 | 他のブックへのリンクが外れる、参照先が変わる |
| ピボットテーブル | キャッシュが落ちて、更新できなくなる |
| 図・グラフ・コメント | 消える、位置がずれる |
| マクロ(xlsm) | 保存の形式によって消える |
小さな表なら問題が出ないこともあります。長年人の手で育ってきた業務の Excel ほど、こうした部品が多く入っていて、壊れやすくなります。
必要な XML だけを書き換える
考え方は単純です。zip の中の部品を一つずつ取り出し、変える必要がある部品だけを書き換え、残りはそのまま新しい zip に入れます。
考え方を示すための簡単な例です(実案件のコードとは違います)。
import zipfile
def rewrite_xlsx(src: str, dst: str, edits: dict) -> None:
"""edits: {部品の名前: 元の中身を受け取って新しい中身を返す関数}"""
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w") as zout:
for info in zin.infolist(): # 元の並び順を保つ
data = zin.read(info.filename)
if info.filename in edits:
data = edits[info.filename](data)
zout.writestr(info, data, compress_type=info.compress_type)
シートの XML の中でも、変えるのは対象のセルだけにします。XML のライブラリで読み込んで書き出すと、名前空間の書き方や空白が変わり、比べるときの差分が増えるため、ここでは文字列として必要な所だけを置き換えています。
import re
def set_number(sheet_xml: bytes, ref: str, value: float) -> bytes:
"""数値のセル ref の値だけを書き換える。書式の番号などの属性には触れない"""
text = sheet_xml.decode("utf-8")
m = re.search(rf'<c r="{ref}"([^>]*?)(?<!/)>(.*?)</c>', text, re.S)
if not m or "<v>" not in m.group(2):
raise ValueError(f"{ref} に値がない") # 想定と違うときは止める
if re.search(r'\st="(?!n")', m.group(1)):
raise ValueError(f"{ref} は数値のセルではない") # 文字列などのセルは別の扱い
inner = re.sub(r"<v>.*?</v>", f"<v>{value}</v>", m.group(2), count=1)
return (text[: m.start(2)] + inner + text[m.end(2):]).encode("utf-8")
実際の業務では、ほかに次のことを考えます。
- 文字列のセル: 値は sharedStrings.xml の番号で持っているので、一覧に文字列を足して番号を書く
- シートの追加: シートの XML を足すだけでなく、workbook.xml のシート一覧、workbook.xml.rels のつながり、[Content_Types].xml の一覧にも書き足す
- 数式の計算結果: 入力の値を変えると、保存されている計算結果は古くなる。仕組みの側で計算して書き込むか、開いたときに Excel に再計算させる設定にする。数式を足したり消したりした場合は、計算順の calcChain.xml を消して Excel に作り直させるのが、一般に行われるやり方です。消すときは、[Content_Types].xml と workbook.xml.rels からも calcChain.xml の記載を外します
想定と違う形のセルに当たったら、推測で書き換えずに止めるのが大事です。黙って書き換えると、壊れたファイルが毎週できあがります。
壊れていないことを確かめる
方式を変えたら、次の順で確かめます。
1. 変わってよい部品だけが変わったか
処理の前と後のファイルで、部品ごとにハッシュ値を比べます。
import hashlib
import zipfile
def part_hashes(path: str) -> dict:
with zipfile.ZipFile(path) as z:
return {n: hashlib.sha256(z.read(n)).hexdigest() for n in z.namelist()}
before, after = part_hashes("input.xlsx"), part_hashes("output.xlsx")
for name in sorted(before.keys() | after.keys()):
if before.get(name) != after.get(name):
print("変更あり:", name)
書き換えるつもりの部品以外が「変更あり」に出たら、どこかで余計な変更をしています。変わった部品は、XML を整形してから差分を取ると、どのセルが変わったかを目で追えます。
2. 正解と全部突き合わせる
人が手で作った過去の完成品を正解にして、同じ週の入力から仕組みで作ったファイルと、全シートの全セルを比べます。数式と値の両方を見ます。比べるためにライブラリで読み込むのは問題ありません。読むだけで、保存はしないからです。
from itertools import zip_longest
from openpyxl import load_workbook
def diff_cells(made: str, correct: str, data_only: bool) -> list:
"""data_only=False で数式、True で保存されている計算結果を比べる"""
a = load_workbook(made, data_only=data_only, read_only=True)
b = load_workbook(correct, data_only=data_only, read_only=True)
diffs = []
for name in b.sheetnames:
rows_a = a[name].iter_rows(values_only=True)
rows_b = b[name].iter_rows(values_only=True)
for r, (ra, rb) in enumerate(zip_longest(rows_a, rows_b, fillvalue=()), 1):
for c, (va, vb) in enumerate(zip_longest(ra, rb), 1):
if va != vb:
diffs.append((name, r, c, va, vb))
a.close() # read_only で開いたブックは閉じる
b.close()
return diffs
この案件でも、手作業の完成品と全項目が一致することを確かめています(顧客報告記載)。実ファイルでの確認は続けている段階です。
3. Excel で開く
最後に、実際に Excel で開きます。修復を求めるメッセージが出ないこと、書式・図・ピボットが元のとおりであること、再計算した後の値が正解と同じであることを確かめます。
全部の週を点検すると、過去の参照ずれが見つかることがある
自動化の仕組みができると、過去の全部の週のファイルを機械で点検できるようになります。手作業でシートを複製して数式を直す作りの Excel では、別の週のシートを参照したままの数式が、気づかれずに残ることがあります。人の目では全部を見きれない所も、機械なら全部の数式の参照先を比べられます。自動化の副産物として、既存の資料の点検に役立つことがあります。
こんな Excel 業務に向いています
- 毎週・毎月、同じ形のレポートを、前回のファイルを複製して作っている
- 変わるのは入力の値と日付の延長だけで、人の判断はほとんど入らない
- ファイルが重く、開くだけで時間がかかる
- 図・ピボット・外部参照など、作り直すと困る部品が入っている
- 以前にライブラリで自動化を試して、ファイルが壊れたことがある
案件の全体像は事例「物流・運輸の会社の定型業務を、現場に入って一つずつ AI 化」に、こうした1業務単位の自動化は「小さな業務の自動化」にまとめています。部署ごとの業務を順に進めたい場合は「FDE 型の伴走支援」をご覧ください。
毎週の Excel レポートを、ファイルを壊さずに自動化したい方は、お問い合わせからご相談ください。作業の手順と、作業前後のファイルの違い(どこが毎週変わるか)を教えていただければ、AI が要る作業か、決まった処理で済む作業かの見立てをお伝えします。