【Python】Excel、Word、PowerPointの図形処理をしたら嵌った話

## 目次
[1. はじめに](#1-はじめに)
[2. 環境](#2-環境)
[3. インストールするライブラリ](#3-インストールするライブラリ)
[4. サンプルコード](#4-サンプルコード)
[5. 補足](#5-補足)
[6. 参考にしたサイト](#6-参考にしたサイト)

## 1. はじめに
Pythonで`Excel`、`Word`、`PowerPoint`の図形処理をしたら嵌ったお話

- Googleで検索だと`python-docx`、`python-pptx`のライブラリが出てくるが、このライブラリだと図形で操作できない機能があるので嵌る（結局、`pywin32`になる）
- `Excel`、`Word`、`PowerPoint`でパスワード（読み取り、書き込み）を設定されてるとダイアログが表示されて処理が停止して嵌る（Googleで検索するとパスワード解析ばかりで、スキップするだけの内容が出てこない）
- ネットワークドライブ経由のファイルサーバ操作で大量処理すると`win32com`でネットワークエラーになり嵌る（`win32com`エラーはあまり情報がなくて探すのが大変）

## 2. 環境
- Windows10 Pro：21H2
- Microsoft Office 2019 
- Python：3.10.4

## 3. インストールするライブラリ
- pywin32：WindowsのAPIを呼び出すライブラリ
```PowerShell
pip install pywin32
```

## 4. サンプルコード
- [GitHub](https://github.com/takanori-azegami-jp/python-msoffice-shapecontrol)

```python:shape_control.py
import win32com.client
import pythoncom
from abc import ABCMeta, abstractmethod

#main処理
def main():
 #Excel図形操作
 excel = Excel(r"Excelファイルのフルパス")
 excel.shape_control()

 #Word図形操作
 word = Word(r"Wordファイルのフルパス")
 word.shape_control()

 #PowerPoint図形操作
 powerpoint = PowerPoint(r"PowerPointファイルのフルパス")
 powerpoint.shape_control()

#-----------------------------------------------------
# Documentクラス（インターフェース）
#-----------------------------------------------------
class Document(metaclass=ABCMeta):
 @abstractmethod
 def shape_control(self):
  pass
#-----------------------------------------------------
# Excelクラス
#-----------------------------------------------------
class Excel(Document):
 def __init__(self, file_name):
  self.file_name = file_name

 # Excel図形操作
 def shape_control(self):
  pythoncom.CoInitialize()  #win32com開始前にこれを呼び出す
  excel = win32com.client.DispatchEx("Excel.Application") #ファイルサーバなどのリモートの大量処理ではDispatchではなくDispatchExを使う
  excel.Visible = True
  try:
   excel.DisplayAlerts = False
   doc = excel.Workbooks.Open(self.file_name,False ,False ,None ,"dummy@password" ,"dummy@password" ,True) #パスワード有りはダミーパスワードで無視
   sheet = doc.Worksheets(1) #1シート目を対象
   #シート内の図形を全て処理
   for shape in sheet.Shapes:
    try:
     print(shape.TextFrame.Characters().Text.strip())
    except:
     print("Error")
   doc.Close(False) #閉じる
  except:
   print("Error")
  finally:
   excel.DisplayAlerts = True
   excel.Quit()
   del excel
   pythoncom.CoUninitialize() #終了した後はこれを呼び出す

#-----------------------------------------------------
# Wordクラス
#-----------------------------------------------------
class Word(Document):
 def __init__(self, file_name):
  self.file_name = file_name

 # Word図形操作
 def shape_control(self):
  pythoncom.CoInitialize()  #win32com開始前にこれを呼び出す
  word = win32com.client.DispatchEx("Word.Application") #ファイルサーバなどのリモートの大量処理ではDispatchではなくDispatchExを使う
  word.Visible = True
  try:
   word.DisplayAlerts =  False
   doc = word.Documents.Open(self.file_name, False, False, None,"dummy@password", "dummy@password", False, "dummy@password", "dummy@password") #パスワード有りはダミーパスワードで無視する
   #ドキュメント内の図形を全て処理
   for shape in doc.Shapes:
    try:
     print(shape.TextFrame.TextRange.Text.strip())
    except:
     print("Error")
   doc.Close(False) #閉じる
  except:
   print("Error")
  finally:
   word.DisplayAlerts =True
   word.Quit()
   del word
   pythoncom.CoUninitialize() #win32com終了時にこれを呼び出す

#-----------------------------------------------------
# PowerPointクラス
#-----------------------------------------------------
class PowerPoint(Document):
 def __init__(self, file_name):
  self.file_name = file_name

 # PowerPoint図形操作
 def shape_control(self):
  pythoncom.CoInitialize()  #win32com開始前にこれを呼び出す
  powerpoint =win32com.client.DispatchEx("PowerPoint.Application") #ファイルサーバなどのリモートの大量処理ではDispatchではなくDispatchExを使う
  powerpoint.Visible = True
  try:
   doc = powerpoint.Presentations.Open(self.file_name +"::dummy@password::dummy@password") #パスワード有りはダミーパスワードで無視する
   slide = doc.slides[0] #1スライド目を対象
   #ドキュメント内の図形を全て処理
   for shape in slide.shapes:
    try:
     print(shape.TextFrame.TextRange.Text.strip())
    except:
     print("Error")
   doc.Close() #閉じる
  except:
   print("Error")
  finally:
   powerpoint.Quit()
   del powerpoint
   pythoncom.CoUninitialize() #win32com終了時にこれを呼び出す

#main呼び出し
if __name__ == "__main__":
 main()
```

## 5. 補足
-  **【情報少なめの嵌まりポイント】** 
pywin32（win32com）で頻発するエラー
:::note alert
Traceback (most recent call last):
  File "C:\Python39\lib\site-packages\win32com\client\dynamic.py", line 86, in _GetGoodDispatch
    IDispatch = pythoncom.connect(IDispatch)
pywintypes.com_error: (-2147221021, '操作を利用できません', None, None)
    IDispatch = pythoncom.CoCreateInstance(
pywintypes.com_error: (-2146959355, 'サーバーの実行に失敗しました', None, None)
:::
:::note alert
Traceback (most recent call last):
  File "C:\Python39\lib\site-packages\win32com\client\dynamic.py", line 707, in __setattr__
    raise AttributeError(
AttributeError: Property 'Excel.Application.Visible' can not be set.
:::

- 上記エラーは、下記2つのリンク先のあわせ技で解消
    1. `Dispatch`を`DispatchEx`にする
    ファイルサーバとかリモートのファイルを大量処理する場合にプロセスを殺しきれない時があるので`DispatchEx`した方が良い
    [pywin32 を使用すると、Dispatch と DispatchEx の違いは何ですか?](https://stackoverflow.com/questions/18648933/using-pywin32-what-is-the-difference-between-dispatch-and-dispatchex)

    2. `CoInitialize` と `CoUninitialize` を追加する
    上記1.だけでも問題ないと思われるが、それでも`win32com`を使いエラーになる時はおまじない的に呼んでおいた方が良い
    [com_error at / (-2147221008, 'CoInitialize は呼び出されていません。', None, None)](https://takeg.hatenadiary.jp/entry/2019/03/12/121627)   

## 6. 参考にしたサイト
- [PyWin32でExcel / Word / PowerPointのファイルを開く](https://ottfoekst.com/open-office-file-with-pywin32/1340/)

- 必ず、読むことになるメソッド
    - [Workbooks.Open メソッド (Excel)](https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbooks.open)
    - [Documents.Open メソッド (Word)](https://docs.microsoft.com/ja-jp/office/vba/api/word.documents.open)
    - [Presentations.Open メソッド (PowerPoint)](https://docs.microsoft.com/ja-jp/office/vba/api/powerpoint.presentations.open)