Python Tips: パスワード付きExcel (.xlsx) を扱うならコレ！

## はじめに

ある日、**Pythonでパスワード付きExcelファイル（.xlsx）を開く**という案件が舞い込んできました。  
最初は、Windows環境でよく使われる `pywin32` を使って問題なく操作できていたのですが、別の環境で実行してみると、**パスワード入力画面が表示されたまま処理が止まる**という現象が発生しました。

この記事では、最終的にたどり着いた **「msoffcrypto + openpyxl」** によるスマートな解決方法を、エラー回避のTipsとともに紹介します。

---

## 1. pywin32 での挑戦

```python
import win32com.client

excel = win32com.client.Dispatch("Excel.Application")
workbook = excel.Workbooks.Open("ファイルパス.xlsx", False, True, None, "パスワード")
```

上記のようなコードで、一部のWindows環境では問題なく開けていたのですが、別の環境ではパスワード入力ダイアログが出て、スクリプトが停止。

環境差異（パッケージやOfficeバージョンなど）を疑いましたが、原因は不明でした。

---

## 2. xlwings を試してみた

```python
import xlwings as xw

wb = xw.Book("ファイルパス.xlsx", password="パスワード")
```

こちらも一見動きそうでしたが、**同様にパスワード画面が表示され止まる**という結果に。  
GUIが絡むツールの限界を感じました。

---

## 3. msoffcrypto + openpyxl によるスマートな解決策

最終的に辿り着いたのがこの組み合わせです。  
`msoffcrypto` でExcelファイルのパスワードを解除し、 `openpyxl` で中身を読み込むという流れです。

### 必要なパッケージのインストール

```bash
pip install msoffcrypto-tool openpyxl
```

### サンプルコード
2025年4月12日 NASの場合ｈ失敗する現象を修正
```python
from msoffcrypto.format.ooxml import OOXMLFile
import msoffcrypto
import openpyxl
import io
import os
import tempfile
import shutil

def test_excel_password(file_path, password):
    """
    エクセルファイルのパスワード動作を検証する最小限のコード
    
    Args:
        file_path (str): エクセルファイルのパス
        password (str): パスワード
    """
    temp_file = None
    decrypted = None
    
    try:
        print("1. ファイルを確認します")
        print(f"   ファイルパス: {file_path}")
        print(f"   パスワード: {password}")
        
        # ファイルパスの正規化（ネットワークパス対応）
        normalized_path = os.path.abspath(os.path.normpath(file_path))
        print(f"   正規化されたパス: {normalized_path}")
        
        # 一時ファイルの作成（ローカルディスク上に作成）
        temp_dir = tempfile.gettempdir()
        temp_file = os.path.join(temp_dir, f"temp_excel_{os.getpid()}.xlsx")
        print(f"   一時ファイル: {temp_file}")
        
        # ファイルが存在しない場合は新規作成
        if not os.path.exists(normalized_path):
            print("2. 新規ファイルを作成します")
            wb = openpyxl.Workbook()
            ws = wb.active
            ws.title = 'Sheet1'
        else:
            try:
                # まず通常のExcelファイルとして開く
                wb = openpyxl.load_workbook(normalized_path)
                print("2. 既存の暗号化されていないファイルを開きました")
            except:
                # パスワード付きファイルの場合
                print("2. パスワード付きファイルを開きます")
                try:
                    with open(normalized_path, 'rb') as f:
                        file = msoffcrypto.OfficeFile(f)
                        file.load_key(password=password)
                        decrypted = io.BytesIO()
                        file.decrypt(decrypted)
                    
                    # 解除したファイルを読み込む
                    decrypted.seek(0)
                    wb = openpyxl.load_workbook(decrypted)
                except Exception as e:
                    print(f"ファイルの読み込み中にエラーが発生しました: {str(e)}")
                    return False
        
        # シート名を表示
        for sheet in wb.sheetnames:
            print(f"   シート名: {sheet}")
        
        # Sheet1のB1セルに値を書き込む
        if 'Sheet1' in wb.sheetnames:
            ws = wb['Sheet1']
            ws['B1'] = 'bbb'
            print("3. Sheet1のB1セルに'bbb'を書き込みました")
        else:
            print("警告: Sheet1が見つかりません")
        
        print("4. パスワード付きで保存します")
        
        try:
            # 一時ファイルに保存
            wb.save(temp_file)
            
            # パスワード付きで保存
            with open(temp_file, "rb") as plain:
                file = OOXMLFile(plain)
                with open(normalized_path, "wb") as f:
                    file.encrypt(password, f)
            
            print("5. 処理が完了しました")
            return True
            
        except Exception as e:
            print(f"ファイルの保存中にエラーが発生しました: {str(e)}")
            return False
            
    except Exception as e:
        print(f"エラーが発生しました: {str(e)}")
        return False
    
    finally:
        # メモリバッファのクリーンアップ
        try:
            if decrypted is not None:
                decrypted.close()
        except:
            pass
        
        # 一時ファイルのクリーンアップ
        try:
            if temp_file and os.path.exists(temp_file):
                os.remove(temp_file)
        except Exception as e:
            print(f"一時ファイルの削除中にエラーが発生しました: {str(e)}")

if __name__ == "__main__":
    # テスト用のパスとパスワードを設定
    test_file_path = r"c:\test.xlsx"  # ネットワークパス
    test_password = "zxcv"  # 実際のパスワードに置き換えてください
    
    print("=== パスワードテスト開始 ===")
    print(f"ファイルパス: {test_file_path}")
    print(f"パスワード: {test_password}")
    
    test_excel_password(test_file_path, test_password)
```


---

## まとめ

| 方法 | メリット | デメリット |
|------|----------|------------|
| pywin32 | Windowsでは簡単に使える | 環境依存・パスワード画面が止まる場合あり |
| xlwings | Pythonらしい書き方 | 同様に止まるケースあり |
| msoffcrypto + openpyxl | GUIなし、安定動作 | 少し手間がかかるが信頼性高い |

最終的に、**msoffcrypto + openpyxlの組み合わせが最も安定しており再現性が高い**と感じました。  
同じような壁にぶつかっている方の助けになれば幸いです！