0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

なぜopenpyxlの別ブックコピーは未だに失敗するのか?結合セルと計算式を100%守り切る「差分削除」の決定版

0
Last updated at Posted at 2026-10-26

PythonでExcel(xlsx)を自動化する際、誰もが一度は次のような要件に直面します。

「巨大な全社明細Excelから、支社別や担当者別のシートだけを切り離して、個別のExcelファイルとして保存したい」

しかし、これを標準的な手法で実装しようとすると、ほぼ100%の確率で次の2つの壁にぶつかります。

  1. copy_worksheet() が別ブック間のコピーに対応しておらず例外でクラッシュする。
  2. ネット上の「セルを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を作って必要なデータを移すのではなく、元ファイルをそのまま複製保存し、該当しない不要シートを削除する」

基準エクセルの指定シート
図1: 振り分け条件を指定する基準エクセル画面

マスタ照合テーブル
図2: 契約者と担当者のマッピングマスタ画面

この方式を採用するメリットは圧倒的です。

  • 書式・結合セルが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 公式サイト をご覧ください。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?