PythonからExcelをwin32comで操作する

# はじめに

Excel ファイルを自動で処理したい。そのための言語としていくつか手段があることが分かった。
どれを選ぶべきか。その中で結果として選んだ Python + win32com の操作例を書き残しておく。

# なぜ Python + win32com で Excel を操作するのか

Excel VBA, PowerShell + COM, Python + xlwings, Python + openpyxl, Python + win32com を比較した結果、自分のニーズに近いのは win32com でした。

まず、Excelを自動で操作したいと考えたとき、初めに思いつくのはExcel VBAでした。ただ、これは向かない用途があるようでした。

Excelファイルの所有者にVBA入れて管理するにはすべてのユーザにマクロ付きのExcelファイルの利用を強制することになるのでOSからExcelのバージョンまで細かな動きをVBA側で考慮する必要があります。VBAを強制することによるセキュリティ上のリスク等も考え始めるとあまり選択肢としてよくない。

次に、データが詰まってるExcelファイルとVBAを管理するExcelファイルを分離することを考えたとしても、VBAのコード管理がめんどい。Gitなりで非テキストファイルを管理するのは簡単でなく、がんばってモジュールを節目でエクスポートして管理、というのは面倒そう。継続的にメンテする費用がなければ避けたい選択肢だった。

Excel VBA の次に Excel の自動化で思いついたのは、 PowerShell + COM でした。が、PowerShell って癖があって苦手意識がある。エスケープがバックスラッシュなの忘れるし、パス名にワイルドカード文字列が入るとエスケープ処理に翻弄される程度の、低レベル PowerShell コーダーには敷居が高すぎる。100行程度ならPowerShellで頑張る。それ以上はお勧めできない。

次に他の言語はさておき、 Python + win32com か、xlwings, openpyxl で悩んだ。

openpyxl は、xlsx を読めるが計算式の計算手段を持って無い気がするのに計算結果を得られそうなメソッドがあって不安がある。おそらくキャッシュしたデータを読めるのだと思うけれど値に不整合があるのではないか、仮に無かったとして書き込み処理を行った後に読んではいけないデータなのではと不安を感じる。誤差が許される読み取り処理や、読み取りせずExcelを出力するならopenpyxlは使い勝手よさそう。

xlwings は、素朴なExcelファイルなら採用したいくらい良くできてる。 `dir(workbook)` でメソッド一覧が出る、メソッド名が原則小文字で統一されている、など使い勝手が良い。ただし、すべてのExcelの機能に対応してないように見える。例えば、ざっと調べた限りでは保護されたExcelシートを解除したい場合は、VBAのコードをxlwings経由で実行する等のワークアラウンドが必要でデータ処理には向くが、かゆいところに手が届かない印象。また、Execel を導入済が前提となる。動作確認してないが macOS で動かしている事例があった。

残ったwin32comは上記の課題をすべてクリアできそう。ただし、制約として、Windows+Excel導入済みが前提になる。デスクトップでExcelを実行中にwin32com経由でExcelを実行すると挙動が変わる部分があるなどテスト面で環境の影響を受けやすい。 `dir(workbook)` でメソッド名の一覧が出なかったり、メソッド名の大文字小文字を誤ると期待通りに動かないといった動作もある。ドキュメントはあまりないので、Python書く際はVBAの知見なり、MS公式APIドキュメントなり、Stackoverflowなりを参照することになる。

いずれも一長一短ですね。Excelを最も細かく使えるのがwin32comで一択。開発者にやさしく今後期待できるのがxlwings。全部で1~2画面程度の短いコードならPowerShellでもまあいいか。Excelファイルを自分だけで使うなら版管理なしのVBAでもいいか、という印象です。

# 前提

* Windows 10
* Python3
* Excel 2013

# ディレクトリ、ファイル

```
. (directory)
|
+ data (directory)
  |
  +-- sample.xlsx
```

# パッケージをインストールする

```bash
pip install pywin32
```

# Excel を読む

## Excel を起動・終了する

```python
import win32com.client

# 起動する
app = win32com.client.Dispatch("Excel.Application")

# 終了する
app.Quit()
```

## xlsxファイルからワークブックを読み取り専用で開く

```python
from pathlib import Path

# 開く
abspath = str(Path(r"data/sample.xlsx").resolve())
workbook = app.Workbooks.Open(abspath, UpdateLinks=0, ReadOnly=True)

# 閉じる
workbook.Close()
```

## ワークブックの一覧を得る

`xxx[Index]` と `xxx(Index)` で要素番号の開始が異なるので注意する。

```python
print(app.Workbooks.Count)
for i in range(0, app.Workbooks.Count):
    print(app.Workbooks[i].name)
```

```python
print(app.Workbooks.Count)
for i in range(1, app.Workbooks.Count + 1):
    print(app.Workbooks(i).name)
```

## ワークブックを有効にする

```python
workbook.Activate()
```

## シートの一覧を得る

```python
print(workbook.Worksheets.Count)
for i in range(0, workbook.Worksheets.Count):
    print(workbook.Worksheets[i].name)
```

```python
print(workbook.Worksheets.Count)
for i in range(1, workbook.Worksheets.Count + 1):
    print(workbook.Worksheets(i).name)
```

## シート名からシートを得る

```python
sheet = workbook.Worksheets("Sheet1")
```

## シートからセルを得る

```python
sheet.Cells.Item(1,1).Value
```

## シートから範囲を得る

```python
arg = "A1:B3"
r = sheet.Range(arg)

ret = list()
for row_index in range(1, r.Rows.Count + 1):
    row = []
    for col_index in range(1, r.Columns.Count + 1):
        row.append(r(row_index, col_index).Address)
    ret.append(row)

print(ret)
   # --> [['$A$1', '$B$1'], ['$A$2', '$B$2'], ['$A$3', '$B$3']]
```

## セルの計算式を得る

```python
cell.Formula = '=1+2'
cell.Formula
  # --> '=1+2'
cell.Value
  # --> 3.0
```

## セルの値を得る

```python
row_index, col_index = 1, 2
cell = sheet.Cells(row_index, col_index)
cell.ClearFormats()
cell.NumberFormatLocal
  # --> 'G/標準'

# 整数
cell.Value = 1
cell.Value
  # --> 1.0

# 文字列
cell.Value = 'あ'
cell.Value
  # --> 'あ'

# 日付に解釈されそうな文字列
cell.NumberFormatLocal
  # --> 'G/標準'
cell.Value = '2019-01-01'
cell.NumberFormatLocal
  # --> 'yyyy/m/d'
cell.Value
  # --> pywintypes.datetime(2019, 1, 1, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True))

import datetime
datetime.datetime.fromtimestamp(timestamp=pywindt.timestamp(), tz=pywindt.tzinfo)
  # --> datetime.datetime(2019, 1, 1, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True))

# 時刻に解釈されそうな文字列
cell.Value = '00:00:00'
cell.NumberFormatLocal
  # --> 'h:mm:ss'
cell.Value
  # --> '0.0'
cell.Value = '24:00:00'
cell.Value
  # --> '1.0'
cell.Value = '48:00:00'
cell.Value
  # --> '2.0'
```

## セルのテキストを得る

```python
cell.ClearFormats()
cell.NumberFormatLocal
  # --> 'G/標準'

cell.Value = '=1'
cell.Value
  # --> 1.0
cell.Text
  # --> '1'
```

# Excel を書く

## 新規ワークブックを開く

```
workbook = app.Workbooks.Add()
```

## ワークブックを保存する

```python
workbook.Save()
```

# その他

## Excel を見えなくする

```python
app.Visible = False
```

## シートの保護を設定・解除する

```python
sheet.Protect()
sheet.Unprotect()
```

## 警告を表示しないようにする

```python
app.DisplayAlerts = False
```

## セルのアドレスを得る

```python
cell.Address
  # --> '$B$1'
cell.GetAddress()
  # --> '$B$1'
cell.GetAddress(RowAbsolute=False, ColumnAbsolute=False)
  # --> 'B1'
# NGな例
#cell.Address(RowAbsolute=False, ColumnAbsolute=False)
```

# Tips

## dirメソッドは使えない

```python
import win32com.client
app = win32com.client.Dispatch("Excel.Application")
dir(app)
  # --> ['_ApplyTypes_', '_FlagAsMethod', '_LazyAddAttr_', '_NewEnum', '_Release_', '__AttrToID__', '__LazyMap__', '__bool__', '__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_builtMethods_', '_enum_', '_find_dispatch_type_', '_get_good_object_', '_get_good_single_object_', '_lazydata_', '_make_method_', '_mapCachedItems_', '_oleobj_', '_olerepr_', '_print_details_', '_proc_', '_unicode_to_string_', '_username_', '_wrap_dispatch_']
```

# 参考資料

## ライブラリ比較

PythonでExcelファイルを扱うライブラリの比較
https://note.nkmk.me/python-excel-library/

## Microsoft

Workbook オブジェクト
https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbook

## Excel + VBA

VBAのソースを、Gitとかで管理する
https://qiita.com/ryotaro76/items/63cad3dfb891a9df7c88

## PowerShell + Excel

PowerShell で Excel をどうのこうのすることに興味を持ってくれると嬉しい
https://qiita.com/miyamiya/items/161372111b68bad0744a

## Python + openpyxl

openpyxl - A Python library to read/write Excel 2010 xlsx/xlsm files
https://openpyxl.readthedocs.io/en/stable/

## Python + win32com

Python for Windows (pywin32) Extensions
https://github.com/mhammond/pywin32

TODO: Exccel + win32com でよくまとまった記事を見た記憶があるがリンクを見つけられない。見つけたら書く。

## Python + xlwings

xlwings - Make Excel Fly!
https://docs.xlwings.org/en/stable/index.html

ExcelからPythonを使う
https://qiita.com/katzhide/items/60d0336b322105bf8fe9

PythonとxlwingsでExcelファイルをいじる
https://idontwannawork.github.io/posts/edit-excel-with-python-and-xlwings/

how to unprotect a sheet? #1032
https://github.com/xlwings/xlwings/issues/1032