Pythonでパスワード付Excelファイルを読む

xlsx(xlsm)ファイルのアンケートを集計するために、Pandasで読もうとしたらパスワード付で詰まったので、対処法をメモしておく。

## パスワード付xlsxファイルが開けない
PandasでExcelのファイルを読むには、通常`read_excel("ファイルへのパス")`で読み込めるが、パスワード付だと以下のようなエラーが出る。
>xlrd.biffh.XLRDError: Can't find workbook in OLE2 compound document

回答秘匿とかの意味でパスワード付けないといけなかったので、これは困った。

## msoffcryptoで開く

ググってみると[この記事](https://qiita.com/osoken/items/7adf019ea9522894b728)が参考になった。
`msoffcrypto`というモジュールでパスワードを指定して開くことができるみたい。
クラックするものではないので、あくまでパスワードは事前に知ってないといけないのだが、アンケートのパスワードは、周囲への秘匿のために僕が自分でつけたものなので、問題なく使えそうだ。

### モジュールインポート

この辺を使う。

```python
import tempfile
import msoffcrypto
import pandas as pd
from pathlib import WindowsPath
```

- tempfile：パスワード解除後のファイルを一時ファイルで扱うために使う
- msoffcrypto：**今回の本命**。パスワード付officeファイルを開くために使う
- pandas：ファイルを読み込むために使う。今回は回答集計が目的だったのでこれにした
- pathlib：ファイルアクセスのために使う。使いやすさで選んだだけ

### 回答ファイルに順次アクセス

ただのpathlibの使い方だが、アンケートの回答が入ったディレクトリパスのインスタンスを作って、中にあるExcelファイルを`glob()`メソッドで順次取り出す。読み出したファイルへはforループ内で`file`でアクセスできる。(名前はただの変数名なので自由に命名していい)
今回はxlsmだったのだが、用途に応じてファイル名とか拡張子で適当にフィルタすると予期しないファイルを読まずに済むので安全。

```python
file_dir = WindowsPath(r"アンケートが入ったディレクトリのパス")
# 回答ファイルを順次確認
for file in file_dir.glob("*.xlsm"):
```

### ファイルを開く

抽出した`file`オブジェクトの`open()`メソッドでとりあえず開く。excelのファイルなのでバイナリモードで。
ここでパスワード解除後のファイルを格納するテンポラリファイルを一緒に作っておく。

- パスワード付のファイル`f`で`office_file = msoffcrypto.OfficeFile(f)`としてインスタンスを作る。
- 作ったインスタンスで`office_file.load_key(password="パスワード")`としてパスワードを指定するとパスワードを解除した状態にできる。
- `office_file.decrypt(tf)`でパスワードを解除状態のファイルを別のファイルに書き込める。ここで、最初にファイルを開くと同時に作ったテンポラリファイルを対象に書き込む。
- `df = pd.read_excel(tf, header=None)`でパスワードがない状態のファイルを通常通り読み込める。今回はpandasのDataFrameとして読んだが、ここは用途に応じて好きな読み方で。

```python
    # パスワード解除したテンポラリファイル作成
    with file.open("rb") as f, tempfile.TemporaryFile() as tf:
        office_file = msoffcrypto.OfficeFile(f)
        office_file.load_key(password="パスワード")
        office_file.decrypt(tf)
        # テンポラリファイルから回答をロード
        df = pd.read_excel(tf, header=None)
```

## 一連で

無事に読めるようになったので、一連で処理。
with文の中でDataFrameが作成できるので、DataFrameの加工はwith文の外でやる。
集計を`pandas.Panel`でやるために、forループの末尾でID振りながらdictに登録していき、forを抜けたところでPanelにしている。
最後に、`pandas.Panel.to_excel()`でexcelファイルでエクスポートして完成。

```python
import tempfile
import msoffcrypto
import pandas as pd
from pathlib import WindowsPath

pnl_dict = dict()
key = 0
file_dir = WindowsPath(r"アンケートが入ったディレクトリのパス")
# 回答ファイルを順次確認
for file in file_dir.glob("*.xlsm"):
    # パスワード解除したテンポラリファイル作成
    with file.open("rb") as f, tempfile.TemporaryFile() as tf:
        office_file = msoffcrypto.OfficeFile(f)
        office_file.load_key(password="パスワード")
        office_file.decrypt(tf)
        # テンポラリファイルから回答をロード
        df = pd.read_excel(tf, header=None)

    # 読み込んだdfを色々加工

    # 最後にPanelにするので、IDつけてdictに入れてく
    pnl_dict[key] = df.copy()
    key += 1
# Panelにして1つのexcelでエクスポート
pnl = pd.Panel(pnl_dict)
pnl.to_excel("test.xlsx")
```

## Panelのワーニング

実行したらこんなワーニングが出た。
>sys:1: FutureWarning:
Panel is deprecated and will be removed in a future version.
The recommended way to represent these types of 3-dimensional data are with a MultiIndex on a DataFrame, via the Panel.to_frame() method
Alternatively, you can use the xarray package http://xarray.pydata.org/en/stable/.
Pandas provides a `.to_xarray()` method to help automate this conversion.

どうやらPanelは廃止予定らしい…DataFrameのMultiIndexを使えとのこと。確かにDataFrameとSeriesはよく使うけど、Panelはちゃんと使うの初めてかも。
とりあえず今回のは今回専用の集計ができればいいので、そのまま使う。

## 終わりに

これでパスワード付のofficeファイルも怖くない。僕はExcelしか扱ったことないが、Wordとかpptもいけるみたい。自然言語処理とかやる時はWordファイル相手に似たようなことすることもあるのかなぁ…
main関数すら切らない雑なつくりをしてしまったり、ファイルパスとパスワード渡してDataFrame返すような関数にしといたほうが、今後も使いまわせて便利だったな～と思うようなところは反省…

## 参考

- [Python+xlrd+msoffcrypto-tool でパスワードの掛かったエクセルファイルを読込む](https://qiita.com/osoken/items/7adf019ea9522894b728)
- [GitHub -msoffcrypto-](https://github.com/nolze/msoffcrypto-tool)