openpyxl・python-pptxでOffice文書を自動編集して踏んだ落とし穴集——図形消失・行高崩れ・テーマ色・見た目の検証
Claude Code のような AI コーディングツールの普及で、「Excel の帳票を Python で編集する」「PowerPoint 資料をスクリプトで生成する」という作業が日常になりました。定番ライブラリの openpyxl / python-pptx は便利ですが、ライブラリがサポートしていない領域を踏むと、エラーではなく静かにファイルが壊れるのが怖いところです。
この記事では、業務の帳票類・プレゼン資料の自動生成で私が実際に踏んだ(あるいは踏む寸前で気づいた)落とし穴を4つまとめます。いずれも再発時にそのまま使える対処付きです。
前提: xlsx / pptx は zip コンテナ
先に1つだけ前提知識を。Office 文書(xlsx / pptx / docx)の実体は XML ファイル群を固めた zip アーカイブです。
book.xlsx(実体は zip)
├── [Content_Types].xml ← 部品の種類宣言
└── xl/
├── workbook.xml
├── worksheets/
│ ├── sheet1.xml ← セルの値・行高・結合
│ └── _rels/ ← シートと図形の関連付け
├── styles.xml ← 書式(塗り・罫線・フォント)
├── sharedStrings.xml ← 文字列の共有プール
├── drawings/ ← 図形(オートシェイプ)
├── media/ ← 画像
└── theme/theme1.xml ← テーマ色の定義
この構造を頭に入れておくと、以降の落とし穴と対処がすべてつながります。「ライブラリで直せないなら zip を開いて XML を直接直す」が最終手段として常に使えるからです。
落とし穴1: openpyxl の load→save で図形・画像が丸ごと消える
一番被害が大きかったのがこれです。openpyxl は DrawingML(図形)を完全サポートしておらず、図形入りの xlsx を load_workbook() → save() すると、図形・画像が丸ごと消えます。構成図シートを含むブックの編集で遭遇しました。
兆候はあります。読み込み時に出るこの警告です。
UserWarning: Shapes and drawings will be lost
警告を見落とすと、保存した時点で図形は消えています。編集前に図形の有無を確認するのが第一の防御です。
unzip -l 対象.xlsx | grep -E "drawing|media"
出力があれば図形・画像入りなので、必ずバックアップを取ってから作業します。
消してしまった場合の復元手順(zip 部分修復)
xlsx は zip なので、元ファイルから図形関連の部品だけ移植すれば復元できます。
- 編集後 xlsx を unzip し、元ファイルから
xl/drawings/一式・xl/media/・該当シートのxl/worksheets/_rels/sheetN.xml.relsをコピーして戻す - 該当
sheetN.xmlの</worksheet>直前に<drawing r:id="rId1"/>を復元(r:id は rels に合わせる) - ルート要素に
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"を追加する。ここが最重要のハマりどころで、openpyxl は r: を使う要素がないシートでは名前空間宣言を省略するため、drawing 参照だけ足すと未定義プレフィックスになり Excel で修復エラーになります。xmllint --nooutで検出できます [Content_Types].xmlに画像拡張子の Default と drawing の Override を追加[Content_Types].xmlを先頭エントリにして再 zip(Python の zipfile で可)
さらに深い罠: 図形を復元しても配色・体裁が崩れる
図形を復元して一件落着かと思ったら、今度は配色・行高・フォントの見た目が崩れていることに気づきました。openpyxl は保存時に styles.xml を全面再生成するため、Excel で人手が作り込んだブックでは書式が微妙に変わってしまうのです。
結論として、Excel で作られた既存ブックへの追記は、openpyxl の load→save を使わず zip レベルで XML を直接外科編集するのが正解でした。要点だけ列挙します。
- 値の変更: 既存セルの style 番号(
s=属性)を維持したままt="inlineStr"形式に置き換える(sharedStrings.xml に触れずに済む) - 同一文字列を参照する複数セルの一括変更: sharedStrings.xml の該当
<si>を直接書き換える(1箇所で全参照セルに反映される) - 新規行の書式: styles.xml には触れず、既存セルの
s=番号を流用する - 改行コードの罠: zip 内の XML は改行が CRLF で格納されていることがあります。部品を unzip してテキストモードの
open()で読むと LF に正規化されるため、そこで確認した文字列をアンカーにzipfile.read().decode()のデータをreplace()すると一致せず失敗します。調査も編集もzipfile.read()→decode()に統一し、アンカーはrepr()で改行を確認のうえcount == 1を assert してから置換します - スタイルコピーの罠: 行追加時に既存行からスタイルを写す際、コピー元が縦結合のメンバーセルだと塗り・罫線が入っておらず、コピー先で書式が欠落します。単独セルか結合の左上セルをコピー元に選びます
なお、openpyxl 自身で生成したブックの再編集(round-trip)は崩れません。危険なのは「Excel で人が作った凝ったブック」です。
落とし穴2: 生成した xlsx の行の高さが 10cm を超える
openpyxl でチェックシート類を生成したとき、一部の行高が 335〜857pt(1行 10〜30cm)に膨張して「崩れた」見た目になりました。原因は2つありました。
- 未結合の狭い列に長文を wrap=True で配置: 幅6〜12 の列に説明文を置くと、Excel はその列幅で折り返す前提で文字数比例の行高を計算し、数十行分の高さを設定してしまう
- 数式セルの行数計算:
=IF(...)のような数式はcell.valueが数式文字列のため、その文字数から行数を見積もると過大になる。表示されるのは短い結果文字列なので1行扱いが正しい
修正パターンはこうです。
- 説明行は表の幅いっぱいに
merge_cellsしてから wrap=True にする - 行高は「見積行数 × 18pt」基準で明示設定する(フォントにもよるが Meiryo 11pt の実測は 18.75pt/行。14.3pt/行で見積もると全部見切れる)
- 行数の見積は表示幅単位(半角1・全角2)で計算し、数式セルは無条件で1行扱いにする
- セル結合された行は Excel が行高を自動調整しないため、結合セルに長文を入れたら行高の明示設定が必須
もう1つ、関連して踏んだのが列幅のグループ定義の破損です。Excel で保存されたファイルは列幅が <col min="2" max="3" width="58"/> のような複数列スパンで保存されることがあり、そこに単独定義 <col min="3" max="3" width="13"/> が併存すると後者が勝って1列だけ潰れます。診断は ws.column_dimensions.items() で min / max / width を出力して重複レンジを探すこと(ws.column_dimensions['C'].width の単純参照ではグループ定義を見落とします)。修復はキーを全削除してから1列ずつ width を再設定します。
落とし穴3: 「theme=4, tint=0.8」の実際の色が分からない
既存ブックのデザインを別ファイルで再現しようとしたとき、塗りの色が theme=4, tint=0.7999... のようなテーマ参照になっていて、実際の RGB が分からない問題に当たりました。解決は3ステップです。
- テーマ定義の取得: zip として開いて
xl/theme/theme1.xmlの<a:clrScheme>から色を読む。定義順は dk1, lt1, dk2, lt2, accent1〜6, hlink, folHlink - インデックスの罠: スタイル側のインデックスは先頭2組が入れ替わります。0=lt1の白、1=dk1の黒、2=lt2、3=dk2、4=accent1。「theme=4」は accent1 のことです
- tint の適用: RGB 各チャンネルの単純補間ではなく、HLS の輝度に適用します
import colorsys
def tint_hex(base_hex, tint):
r, g, b = (int(base_hex[i:i+2], 16) / 255 for i in (0, 2, 4))
h, l, s = colorsys.rgb_to_hls(r, g, b)
l = l * (1 - tint) + tint # tint > 0 で明るく
return "".join(f"{round(v*255):02X}" for v in colorsys.hls_to_rgb(h, l, s))
# 実例: accent1 #156082 + tint 0.8 → C1E5F5(淡い水色)
print(tint_hex("156082", 0.7999799847602844))
この式で計算した色は Excel の表示と一致しました。テーマ色を含む帳票の「同じ見た目」再現には必須の知識です。
落とし穴4: python-pptx は「データは正しいのに見た目が違う」
PowerPoint 資料の自動生成では、データ構造は正しいのにレンダリング結果が意図と違う系の落とし穴が中心でした。
- 消えないシャドウ: 図形のシャドウを消すのに
a:effectLstを削除するだけでは、テーマ既定のシャドウを継承して角丸矩形がボケたままになります。shape.shadow.inherit = Falseで「効果なし」を明示的に上書きする必要があります - タイトルの折り返し: 40pt のタイトルは幅 19cm のボックスで全角約12文字/行が上限。自動折返しに任せると変な位置で切れるので、手動改行で分割します
見た目の検証を自動化する
「見た目」の問題は生成コードのテストでは捕まえられないので、スライドを画像化して目視確認するパイプラインを作りました。macOS では Keynote の AppleScript でスライドを PNG エクスポートできます。
- 事前に
open -a Keynoteで起動してから osascript を実行する(未起動だと -609 エラー) - Microsoft PowerPoint 側の AppleScript はサンドボックスの制約で
/private/tmpへ書き出せず失敗するため、Keynote 経由が現実解 - ただし Keynote のレンダリングは PowerPoint と整列がわずかに異なるため、画像は「大きな崩れの検出」用と割り切り、最終判定は XML の値(
paragraph.alignment等)で行う
共通する教訓
4つの落とし穴に共通する学びをまとめます。
| 教訓 | 具体的にやること |
|---|---|
| Office 文書は zip + XML | ライブラリで直せない問題は unzip して XML を直接読む・直す |
| 編集前の防御 | unzip -l で図形の有無を確認し、必ずバックアップを取る |
| 機械検証 | xmllint --noout で XML 整合、部品単位の md5 比較で「意図した箇所以外がバイト一致」を確認 |
| 目視検証 | 数値上正しくても見た目は別問題。画像化して目で確認する工程を挟む |
AI にコードを書かせる時代だからこそ、この手の「実行は成功するのにファイルが静かに壊れる」問題はレビューをすり抜けやすくなっています。生成されたコードを信頼する前に、unzip -l と xmllint の2つのチェックを習慣にするのがおすすめです。
同じ落とし穴を踏んだ方の時間節約になれば嬉しいです。