PythonでExcel(xlsx)を自動化する際、誰もが一度は次のような要件に直面します。
「巨大な全社明細Excelから、支社別や担当者別のシートだけを切り離して、個別のExcelファイルとして保存したい」
しかし、これを標準的な手法で実装しようとすると、ほぼ100%の確率で次の2つの壁にぶつかります。
-
copy_worksheet()が別ブック間のコピーに対応しておらず例外でクラッシュする。 - ネット上の「セルを1行ずつループコピーするコード」を書いた結果、セルの結合が解除され、計算式や罫線が壊れて大惨事になる。
本記事では、最新の openpyxl(v3.1系)でも未解決である仕様制限の回避策を解説します。
書式・結合セル・計算式を100%崩さずに個別ファイルへ高速分割する「差分削除パターン」を紹介します。
1. なぜ「セルのループコピー」は失敗するのか?
まず、なぜネット上でよく見かけるコードでは現場のExcelが壊れてしまうのか、原因を整理します。
openpyxl の仕様上の制限
openpyxl にはシートを複製する copy_worksheet() という便利なメソッドが存在します。
しかし、このメソッドは**「同一ブック内」でのコピーにしか対応していません**。
import openpyxl
wb1 = openpyxl.load_workbook("source.xlsx")
wb2 = openpyxl.Workbook()
# 他のワークブックにシートを直接コピーしようとすると例外が発生する
wb2.copy_worksheet(wb1["Sheet1"])
# ValueError: Cannot copy between worksheets from different workbooks
ネットでよく見る「セル単位の移植」が抱える罠
公式メソッドが使えないため、検索すると「セルを走査して値とスタイルを1つずつ移植する」という次のようなコードがヒットします。
# ⚠️ 現場の複雑なExcelでは失敗しやすいコード例
for row in src_ws.iter_rows():
for src_cell in row:
dst_cell = dst_ws.cell(row=src_cell.row, column=src_cell.column, value=src_cell.value)
if src_cell.has_style:
dst_cell.font = copy(src_cell.font)
dst_cell.border = copy(src_cell.border)
dst_cell.fill = copy(src_cell.fill)
一見すると綺麗に動いているように見えます。しかし、現場の実務Excelに適用した瞬間、以下の問題が噴出します。
- セルの結合(merged_cells)が消滅する: 結合されていたはずのセルが勝手に解除され、レイアウトが崩壊します。
-
計算式(数式)の参照先が壊れる: セル位置や依存関係がズレて
#REF!エラーが発生します。 - 列幅や行の高さが反映されない: すべてデフォルトの幅に戻ってしまい、文字が見切れます。
- 処理速度が極めて遅い: 万単位のセルを1つずつPythonオブジェクトとして複製するため、実行時間が膨大になります。
2. 解決策:白紙コピーではなく「元ブック全体を丸ごと複製して不要シートを消す」
この問題を根本から解決するのが、発想を逆転させた「差分削除アプローチ」です。
「白紙のExcelを作って必要なデータを移すのではなく、元ファイルをそのまま複製保存し、該当しない不要シートを削除する」
この方式を採用するメリットは圧倒的です。
- 書式・結合セルが100%保持される: 元ファイルを物理的に丸ごと複製しているため、セルの結合や罫線、フォント色が一切壊れません。
- 計算式やマクロもそのまま残る: SUM関数や掛け率の計算式も、元ファイルの完全な状態で維持されます。
- 圧倒的に高速: セル単位のPythonループが発生しないため、一瞬で個別ファイルが完成します。
3. そのままコピペで動く実装コード
以下が、不要シートを wb.remove() で削除して個別ファイルを切り離す最小の実装コードです。
import os
import openpyxl as ex
def split_sheets_by_diff_delete(source_excel_path: str, output_dir: str, target_mapping: dict[str, list[str]]):
"""
元ファイルを丸ごと複製し、不要シートを削除することで
書式・結合セル・数式を100%維持したまま個別ブックを作成します。
"""
os.makedirs(output_dir, exist_ok=True)
# 元の巨大ブックをメモリに一度だけロード
main_wb = ex.load_workbook(source_excel_path)
for target_name, match_keywords in target_mapping.items():
out_file_path = os.path.join(output_dir, f"{target_name}.xlsx")
# 1. 元ブックをそのまま丸ごと対象ファイル名で保存
main_wb.save(out_file_path)
# 2. 複製したブックを開き直す
wb = ex.load_workbook(out_file_path)
# 3. 該当しない不要シートを逆引きで一括削除
search_set = set(match_keywords)
for sheet_name in list(wb.sheetnames):
# シート名に対象キーワードが含まれていない場合は削除
if not any(keyword in sheet_name for keyword in search_set):
try:
wb.remove(wb[sheet_name])
except Exception:
pass
# 4. 変更を上書き保存
wb.save(out_file_path)
wb.close()
print(f"作成完了: {out_file_path}")
main_wb.close()
4. 現場で絶対にハマる2大例外トラブルと回避策
実務のExcelを扱う際、仕様書どおりにいかないのが現場特有のイレギュラーデータです。運用で必ず直面する2つの落とし穴と対策を組み込みます。
① 全角スペース(\u3000)による突合すり抜け
Excelのセルやシート名には、佐藤 太郎(全角空白)と 佐藤 太郎(半角空白)が平然と混在しています。
単純な完全一致検索を行うと条件をすり抜け、本来振り分けるべき担当者の個別ファイルからシートが欠落してしまいます。
# 解決策: ロード直後にシート名や比較文字列から全角スペースを一括除去
for sheet in main_wb.worksheets:
if '\u3000' in sheet.title:
sheet.title = sheet.title.replace('\u3000', '')
② 「該当シート0件」でopenpyxlがクラッシュする罠
その月に成約実績のない担当者がいた場合、一致するシートが0枚になります。
openpyxl は**「シートが1枚も存在しないワークブック」を保存しようとすると例外を発生させてプログラム全体を停止**させます。
# 解決策: シートが0枚になった場合は即座にファイルを削除し、スキップログへ記録
if not wb.sheetnames:
wb.close()
if os.path.exists(out_file_path):
os.remove(out_file_path)
skipped_targets.append(target_name)
continue
# スキップされた担当者を事務員向けにテキスト出力
if skipped_targets:
log_path = os.path.join(output_dir, "該当データなし一覧.txt")
with open(log_path, "w", encoding="utf-8") as f:
f.write("\n".join(skipped_targets))
この2つのフェイルセーフを組み込むことで、途中で止まることのない堅牢な自動化バッチが完成します。
5. まとめ
openpyxlで別ブックへシートを分割する際、セルを1行ずつ移植する手法は多くの落とし穴を伴います。
- 「元ブック全体を丸ごと複製保存し、不要シートを削除する」アプローチが最も確実
- セル結合、計算式、書式、列幅を一切壊さず維持できる
- 全角スペース正規化と0シート例外対策により、実運用へ耐えうるコードになる
Excelの書式崩れに悩んでいる方は、ぜひこの「差分削除パターン」を試してみてください。
関連記事・お問い合わせ
-
開発背景・アーキテクチャの解説:
本ツールの設計思想や、設定ファイル駆動アーキテクチャ、UI刷新の裏話は Zennの開発手記記事 にまとめています。 -
業務導入・受託開発のご相談:
AI Nexusでは、業務効率化ツールの受託開発やAI導入コンサルティングを行っています。詳細は AI Nexus 公式サイト をご覧ください。

