ホーム / お役立ち記事 / xlutils で .xls に書くと書式が崩れる理由|レコードだけを差し替えて体裁を保つ

Knowledge

xlutils で​ .xls に​書くと​書式が​崩れる​理由|​レコードだけを​差し替えて​体裁を​保つ

古い .xls(Excel 97-2003)の様式に Python の xlutils で値を書くと、書いたセルの罫線や塗りが消え、数式・入力規則・印刷範囲も落ちることがあります。LibreOffice で開いて保存し直すと、選択範囲内で中央の見出しが結合セルに変わります。原因、見分け方、書くセルのレコードだけを差し替える直し方、落とし穴、確かめ方を、弊社の実測と実際の Excel での確認でまとめます。

著者: 村山 悠太(ゴムマリ) 最終更新: 2026年11月

古い .xls(Excel 97-2003 の形式)の様式に、Python の xlutils で値を書くと、書いたセルの罫線や塗りが消えることがあります。数式・入力規則・オートフィルタ・印刷範囲も、何も言われずに落ちます。

原因は、xlutils が様式を「読めた分だけ読み、新しいブックとして書き直す」ことです。LibreOffice で開いて保存し直す方法も、ブック全体を書き直すので、選択範囲内で中央の見出しが結合セルに変わりました。

直し方は、ブックを書き直さず、書くセルのレコードだけを差し替えることです。弊社がお客様の .xls の様式3本で試したこの方式では、書いた欄以外の書式の差は0でした。

この記事は、既存の .xls の様式に値を書き込む仕組みを作る・任されている業務の担当者・情報システムの担当者に向けて書きます。

弊社の実測と見本での再現、[MS-XLS](Microsoft が公開する .xls の形式の仕様)、xlrd と xlutils の文書で整理します。

原因: xlutils も​ LibreOffice も、​ブック全体を​書き直している

.xls の中身は、1つのファイルの中に複数のストリームを持つ複合文書(OLE の複合ファイル)です。[MS-XLS] は、ブックは Workbook ストリームを必ず持つ、と定めています(2.1.1・2.1.2 節)。

Workbook ストリームは、レコードと呼ぶ塊の並びで書きます(2.1.2 節)。セルの書式は XF レコードが持ち、XF を拡張する書式は XFExt レコードが持ちます(2.2.6.1.1・2.4.355 節)。

xlutils の文書は、xlutils.copy を「xlrd で読んだブックを xlwt のブックに写し、変えられるようにする」関数と説明しています。

文書の例は、写したブックに wb.get_sheet(0).write(0,0,'changed!') で書き、別のファイルに保存します。

xlrd の文書は、数式は読まず計算結果だけを取り出し、オートフィルタと入力規則などは読まずに無視する、と書いています。

つまり xlutils で保存した .xls には、xlrd が読まなかった部品が入りません。弊社の再現では、xlrd が読む印刷範囲の名前も、写しには入りませんでした。

LibreOffice で開いて保存し直す方法も、保存のときにブック全体を書き直します。

弊社の​実測: お客様の​様式で、​2つの​書き方を​比べた

弊社は、お客様の担当の方が .xls の様式を1枚ずつ開いて手で直している作業を、直した様式そのものを出す形に置き換える仕組みを作っています。そのために、2つの書き方を比べました。

項目 内容
実測の日 2026年9月
様式 お客様の .xls 3本(帳票2種とひな形1本)
書いた欄 60セル
比べた物 書いた欄以外の全セル(約2万7千セル)の書式、印刷の設定、名前の定義、図形、ファイルの中のストリーム
書き方 結果
LibreOffice で開いて保存し直す 3本中2本で様式の作りが変わった
書くセルのレコードだけを差し替える 3本とも書式の差0・印刷の設定と名前と図形の差0

LibreOffice で保存し直した版で変わった所は、次のとおりです。

  • 結合セル: 選択範囲内で中央の見出しが結合セルに置き換わり、約170範囲増えた。
  • 図形: オートフィルタの矢印の図形が5つから約60に増えた。
  • 色: 灰色の塗りを持つ書式6つで、拡張色の XFExt が落ちた。
  • 名前: 隠していた名前が見える名前に変わり、印刷範囲の名前が5つ増えた。
  • ストリーム: 文書の管理の情報を持つストリーム4つが落ちた。

レコードだけを差し替えた版は、Workbook 以外のストリームがバイト単位で元のままでした。同じ入力で2回流すと、出力は2回ともバイト単位で同じでした。

2026年9月、この方式で書いた .xls 4本を実際の Excel で開き、修復を求める画面は出ませんでした。見え方と、Excel の版、Mac か Windows かは記録していません。

手元の​再現: xlutils で​書くと、​どこが​消えるか

お客様の様式は使わずに、弊社で作った見本の帳票で、xlutils を含む4つの書き方を比べました。見本は、結合・罫線・塗り・列幅・入力規則・オートフィルタ・印刷範囲・数式2つを持つ2シートの .xls で、Excel に保存させたものです。

項目 内容
再現の日 2026年11月2日
書いた欄 17セル(文字・数値・日付)
道具 Python 3.14.6・xlrd 2.0.2・xlwt 1.3.0・xlutils 2.0.0・LibreOffice 26.2.4.2
Excel Mac 版の Microsoft Excel 16.114(AppleScript で開き、体裁を読んだ)

結果は次のとおりです。「書式番号を戻す」は、書く前のセルの書式番号を、書いた後に戻したものです。

書き方 書いたセルの書式 数式2つ 入力規則・オートフィルタ・印刷範囲 選択範囲内で中央 灰色の塗り(Excel で読んだ色)
xlutils(文書の例どおり write) 17セルとも消えた 数字の0に置き換わった 3つとも消えた 保った 別の灰色に変わった
xlutils(書式番号を戻す) 罫線・塗り・フォントは保った(日付の表示は変わった) 数字の0に置き換わった 3つとも消えた 保った 別の灰色に変わった
LibreOffice で保存し直す 見た目は保った(日付の書式は作り変わった) 数式のまま・計算し直された 保った(名前が2つ増えた) 結合セルに変わった 別の灰色に変わった
レコードだけを差し替える 保った 数式のまま・結果を差し替えた 保った 保った 元の灰色のまま

xlutils の版で数式が0になったのは、xlrd が数式の計算結果だけを読み、写しに数字として書いたためです。原本の計算結果は、入力欄が空のときの0でした。

xlutils の文書の例どおりに書いた版では、書いたセルのフォントが Arial に変わり、日付は Excel で「46328」という数字のまま表示されました。

書式番号を戻した版でも、日付は、レコードだけを差し替えた版の「2026/11/2」でなく「11/2/26」と表示されました。

日付の書式番号が、組み込みの14から、xlwt が作った独自の165(m/d/yy)に変わったためです。「2026/11/2」は、組み込みの14をこの Mac の地域の設定で表示したもので、設定によって変わりえます。

書式番号を戻した版では、列幅の記録が使っていない列の分(1枚目248列・2枚目253列)だけ消えました。2行目と17行目は、高さの指定の無い状態から 255(原本の既定は 380・単位は1/20ポイント)になりました。

LibreOffice の版でも、書いた日付のセルの書式は、組み込みの14から独自の書式([$-411]yyyy/m/d)に作り変わりました。

原本の灰色は、XFExt に 166・166・166 を、XF に近い標準の色(150・150・150)の番号を持っていました。XFExt が落ちた版は、Excel で 150・150・150 と読めました。

レコードだけを差し替えた版は、弊社の比べる道具で差が0でした。Excel から読んだ結合の有無(5つのセル)・列幅(8列)・行の高さ(3行)・罫線(1か所)・塗り(5つのセル)・印刷範囲も、原本と同じでした。

見分け方​: どの​書き方で​崩れたか

見え方 疑う所
書いたセルだけ罫線と塗りが消え、文字が Arial になった xlutils の write が、書く前の書式を引き継いでいない
合計の欄が数字のままで、値を変えても動かない xlrd が数式を読まず、計算結果を数字として写した
印刷範囲・オートフィルタ・入力規則のプルダウンが無い xlrd が読まないか、xlutils が写さない部品で、写しに入っていない
選択範囲内で中央の見出しが、結合セルになった LibreOffice で開いて保存し直した
灰色などの塗りの色が、少しだけ変わった 拡張色の XFExt が落ちた

直し方​: 書く​セルの​レコードだけを​差し替える

弊社が実際に通した手順は次のとおりです。

  1. 複合文書から Workbook ストリームだけを取り出す。ほかのストリームは触らない
  2. 書く欄の既存のセルのレコード(書式だけを持つ空白のセル)を、書式番号はそのままに、文字列なら LABELSST、数値なら NUMBER のレコードに置き換える
  3. 足した文字列は、共有文字列表(SST)の末尾に足す
  4. レコードの長さが変わって位置がずれる所を、計算し直す(下の表)
  5. 書かずに組み直すと原本とバイト単位で同じになることを、毎回確かめる
  6. 書いた値に依存する数式の計算結果は、別に計算して差し替える

4の位置の情報は、[MS-XLS] が次のように定めています。

レコード 持っている位置 節
BoundSheet8 各シートの先頭(BOF レコード)の、ストリームの中の位置 2.4.28
Index 行の塊(DBCell レコード)ごとの位置 2.4.144
DBCell 行の塊の中の、セルのレコードの位置 2.4.78
ExtSST 共有文字列表の中の、文字列の組の位置 2.4.107

5は位置の計算の検査です。弊社の道具は、お客様の様式3本と弊社の見本で、書かずに組み直した Workbook ストリームが原本とバイト単位で同じでした。

6は、弊社は LibreOffice で書く前と後を計算し、値が変わった数式のセルだけ結果を差し替えました。考え方はPython で値を書いた Excel の計算結果が古いままの理由と同じです。

弊社の道具は、値も書式も無いセルへの書き込み、文字列を返す数式の結果の差し替え、大きいファイル(複合文書の管理の表が足りなくなるもの)には対応していません。

当たったら書かずに止まるようにコードを作ってあります(今回の再現では、止まる場面は流していません)。

xlutils を使い続けるなら、書く前の書式番号を戻すと、書いたセルの罫線・塗り・フォントは保てました(日付の表示は変わりました)。弊社の再現で流した形です。

import xlrd
from xlutils.copy import copy

rb = xlrd.open_workbook("様式.xls", formatting_info=True)
wb = copy(rb)
ws = wb.get_sheet(0)


def write_keep(ws, r, c, v):
    row = ws._Worksheet__rows.get(r)  # xlwt の内部の属性(公開の使い方ではない)
    old = row._Row__cells.get(c) if row else None
    ws.write(r, c, v)
    if old is not None:
        ws._Worksheet__rows[r]._Row__cells[c].xf_idx = old.xf_idx


write_keep(ws, 2, 2, "TEST-001")  # C3(行と列は0から数える)
wb.save("出力.xls")

この形でも、数式・入力規則・オートフィルタ・印刷範囲・拡張色は戻らず、日付の書式番号と列幅・行の高さの記録も変わります。xlwt の内部の属性を使うので、版が変わると動かなくなるおそれがあります。

落とし穴: 実際に​踏んだ​もの

  • 文書の例どおりに書くと、書いたセルの書式だけが消えるxlutils の文書の例の write は、書く前のセルの書式を引き継ぎませんでした。書いていないセルの書式は変わらないので、書いた欄を見ないと気づきません。
  • xlrd で読み直しても、消えた数式に気づかないxlrd は数式の計算結果を読むので、数式が数字に置き換わった版でも同じ値が返ります。値を読み戻すだけの検査は通ります。
  • 結合セルへの変化は、目で見比べても気づきにくい見出しの見た目は近いまま結合セルに変わりました。お客様の様式では、オートフィルタの範囲の中にも結合セルが増えていました。
  • Excel で開いて確かめる仕組みが、確認の画面で止まった2026年11月、openpyxl 3.1.5 で作った xlsx を LibreOffice で .xls にした見本を Excel に開かせると、「名前の重複」の画面が出て進まなくなりました。
  • Excel で開いただけで、ファイルのバイトが変わった保存せずに閉じても、ファイルの一部のバイトが変わりました。出力のハッシュで照らすなら、Excel には写しを開かせます。

4つ目の件の .xls(LibreOffice 26.2.4.2 で変換)には、組み込みの印(fBuiltin・[MS-XLS] 2.4.150 節)の無い「_xlnm.Print_Area」がありました。

弊社は今、Excel に開かせる前に、この印を道具で見ています。

確かめ方​: 書式の​差を​数え、​最後に​ Excel で​開く

書く前の様式と書いた後の出力を、全セルの書式と、書き直すと落ちたり増えたりする部品のレコードの数で比べます。次は、弊社が再現で使った道具です(xlrd 2.0.2 と olefile 0.47)。

import collections
import struct
import sys

import olefile
import xlrd

# Workbook ストリームの中で、書き直すと落ちたり増えたりする部品のレコード(数だけを比べる)
WATCH = {0x00E5: "結合セル", 0x087D: "拡張色(XFEXT)", 0x0092: "パレット", 0x0018: "名前の定義",
         0x00A1: "印刷の設定", 0x0014: "ヘッダー", 0x0015: "フッター", 0x01BE: "入力規則",
         0x009D: "オートフィルタ", 0x005D: "図形(OBJ)", 0x0006: "数式"}


def records(path):
    data = olefile.OleFileIO(path).openstream("Workbook").read()
    cnt, pos = collections.Counter(), 0
    while pos + 4 <= len(data):
        rt, ln = struct.unpack_from("<HH", data, pos)
        if rt in WATCH:
            cnt[WATCH[rt]] += 1
        pos += 4 + ln
    return cnt


def formats(bk):
    out = {}
    for sh in bk.sheets():
        for r in range(sh.nrows):
            for c in range(sh.ncols):
                xf = bk.xf_list[sh.cell_xf_index(r, c)]
                f, b, bg, al = bk.font_list[xf.font_index], xf.border, xf.background, xf.alignment
                out[(sh.name, r, c)] = (
                    f.name, f.height, f.weight, f.italic, bk.colour_map.get(f.colour_index),  # 太字は weight で見る(bold の印は書き手で違う)
                    b.left_line_style, b.right_line_style, b.top_line_style, b.bottom_line_style,
                    bg.fill_pattern, bk.colour_map.get(bg.pattern_colour_index),
                    al.hor_align, al.vert_align, bk.format_map[xf.format_key].format_str)
    return out


def layout(bk):
    out = {}
    for sh in bk.sheets():
        out[(sh.name, "結合")] = sorted(sh.merged_cells)
        out[(sh.name, "列幅")] = {c: i.width for c, i in sh.colinfo_map.items()}
        out[(sh.name, "行の高さ")] = {r: i.height for r, i in sh.rowinfo_map.items()}
    return out


def main(orig, out):
    a = xlrd.open_workbook(orig, formatting_info=True)
    b = xlrd.open_workbook(out, formatting_info=True)
    fa, fb = formats(a), formats(b)
    cells = [k for k in sorted(set(fa) | set(fb)) if fa.get(k) != fb.get(k)]
    print("セルの書式が変わった:", len(cells), "セル", cells[:5])
    la, lb = layout(a), layout(b)
    for k in sorted(set(la) | set(lb)):
        x, y = la.get(k), lb.get(k)
        if x == y:
            continue
        if isinstance(x, dict):
            ch = sorted(i for i in set(x) | set(y or {}) if x.get(i) != (y or {}).get(i))
            print("変わった:", k, len(ch), "か所", [(i, x.get(i), y.get(i)) for i in ch[:3]])
        else:
            print("変わった:", k, "増えた", sorted(set(y) - set(x))[:3], "減った", sorted(set(x) - set(y))[:3],
                  "(%d→%d)" % (len(x), len(y)))
    ra, rb = records(orig), records(out)
    for k in sorted(set(ra) | set(rb)):
        if ra[k] != rb[k]:
            print("レコードの数が変わった:", k, ra[k], "→", rb[k])
    n = len(cells) + sum(la.get(k) != lb.get(k) for k in set(la) | set(lb)) + sum(ra[k] != rb[k] for k in set(ra) | set(rb))
    print("差の数:", n)
    return 1 if n else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1], sys.argv[2]))

使い方は python xls_diff.py 様式.xls 出力.xls で、差があれば終了コード1です。弊社の再現では、レコードだけを差し替えた版が差0、書式番号を戻した版が差9、xlutils の文書の例どおりの版が差26でした。

この道具は、セルの書式を xlrd が読める範囲で比べ、結合・名前・数式・入力規則などの部品はレコードの数だけを見ます。

日付の書式番号が組み込みから独自の物に変わっても、表示の文字が同じなら見逃します。レコードの数が同じでも、中身が変わっていれば見逃します。

最後は Excel で開いて確かめます。弊社は AppleScript で Excel に写しを開かせ、30秒で打ち切る問いが約2秒で返ることと、結合・列幅・塗り・印刷範囲を読んで原本と比べました。

自力で​どこまで​やれるか

書いたセルの罫線・塗り・フォントだけを保てばよく、日付の表示と列幅・行の高さの差を受け入れられるなら、xlutils で書いて書式番号を戻す形が使えます。

ただし、様式に数式・入力規則・オートフィルタ・印刷範囲・標準でない色の塗りがあれば落ちます。xlrd の文書が読まないと書くグラフ・マクロ・画像・コメント・ハイパーリンク・条件付き書式も落ちるはずですが、弊社は試していません。

様式を .xlsx に変えてよいなら、道具を選べます。.xlsx の様式に壊さず書く方法はExcel帳票への自動書き込みでファイルが壊れる理由にまとめています。

自力の壁は次の3つです。

  • 位置の情報(シートの先頭・行の塊・文字列の索引)を計算し直す道具を作り、書かずに組み直してバイト単位で同じになるまで試す必要がある
  • 書いた値に依存する数式の計算結果を、別に計算して差し替える必要がある
  • 書式の差の検査と実際の Excel での確かめを、出す前の工程として毎回回す仕組みが要る

外注する​場合の​判断基準

外注の分かれ目は、.xls の様式を .xlsx に変えられず、様式の持ち主が体裁の崩れを許さないかです。外注するなら、見積もりを比べる前に次の3点を確かめてください。

  • 書いた欄以外の書式の差を、全セルで数えて0を確かめる検査が入っているか
  • 数式・入力規則・オートフィルタ・印刷範囲が残っているかを、ファイルの中で確かめているか
  • 実際の Excel で開いて、確認の画面で止まらないことを確かめているか

ゴムマリでは、既存の Excel の様式へ書き出す業務を含め、1つの業務を回す AI エージェントを御社の業務に合わせて作る仕事を、AIエージェント導入として承ります。決めた確かめを通らない出力は、出さずに止める作りにします。

お渡しするのは、その業務の AI エージェント、判断の基準を書いた文書、使い方の手順です。料金は1つの業務ごとに初期50万円から、月額5万円から、初期構築は4週間からです。

月額には、出す前の確かめを保ち、足していくことを含みます。進め方は相談からご連絡ください。

次の​一歩

値を書いている .xls の出力を1本選び、書く前の様式と並べて、確かめ方の節の道具を流してください。差の行を見れば、この記事の見分け方の表のどれに当たるかが分かります。

関連記事: Excel帳票への自動書き込みでファイルが壊れる理由/Python で値を書いた Excel の計算結果が古いままの理由/Python で書き出した Excel が「修復」を求める理由

この記事を書いた人

代表 村山悠太のポートレート

村山 悠太(ゴムマリ代表)

ワークスアプリケーションズで大手企業向けERPの営業と導入を担当し、外資系SaaS企業と国内大手放送局で販路と新規事業の立ち上げ、AI学習用データを扱うAIスタートアップ2社でマーケティングと営業を担った。2024年10月から生成AIの実装に入り、いまは大手企業の現場で、AIエージェントを要件定義から運用まで手がけている。

代表の経歴を見る

FAQ

よく​ある​質問

xlrd で formatting_info=True を付けて読めば、xlutils で書いても書式は残りますか?

弊社の再現では残りませんでした。formatting_info=True で読んで xlutils.copy で写し、文書の例どおり write で書くと、書いた17セルの罫線・塗り・フォントが消えました。書く前のセルの書式番号を戻すと、罫線・塗り・フォントは残りましたが、日付の表示は変わりました。

書式番号を戻せば、xlutils で体裁を保ったまま書けますか?

弊社の再現では、書いたセルの罫線・塗り・フォントは保てましたが、日付の表示と列幅・行の高さの記録が変わり、数式・入力規則・オートフィルタ・印刷範囲は戻りませんでした。xlrd の文書は、数式は計算結果だけを読み、オートフィルタと入力規則は読まない、と書いています。読まない部品は写しに入りません。

LibreOffice で開いて保存し直す方法なら、数式は残りますか?

弊社の再現では、数式は数式のまま残り、書いた値で計算し直されました。ただし、選択範囲内で中央の見出しが結合セルに変わり、灰色の塗りが別の色になり、名前の定義が2つ増えました。お客様の様式でも、3本中2本で様式の作りが変わりました。

Excel で開いて修復の画面が出なければ、体裁も保たれていますか?

保たれているとは限りません。弊社の再現では、4つの書き方の出力を Excel に開かせ、30秒で打ち切る問いがどれも約2秒で返りました(確認の画面で止まっていないとみています)。それでも、LibreOffice で保存し直した版は見出しが結合セルに変わり、xlutils の版は数式と印刷範囲が消えていました。

Windows の Excel でも同じですか?

弊社の再現で Excel を使ったのは Mac 版の Excel 16.114 だけで、Windows 版では確かめていません。消えた部品(数式・入力規則・オートフィルタ・印刷範囲)は、ファイルの中のレコードの数で確かめたもので、開く Excel の版によりません。

Related

関連

ご相談は​こちらから

相談は無料です。

相談する