IronPythonを使ったExcel操作の覚え書き(前編)


はじめに

F#(fsi)によるExcelの操作をまとめたので,IronPythonでの操作も備忘録としてまとめておきます。

「python excel」をキーワードに検索すると「openpyxl」など,いくつかのライブラリがあがってきます。
それぞれ優秀なライブラリですが,VBAからの移行にはそれなりの学習コストを伴います。
その点でIronPythonはハードルが低いので入門者の選択肢の一つになると思います。

IronPythonのメリット(個人の感想)

  • ほぼVBA(Microsoft.Office.Interop.××× 使用時)
  • .NETの活用ができる(対話環境は.NET入門者の学習にも役立つ)

デメリット

  • Python2.7である IronPython3.4(バージョン 3.4.0-alpha1)がリリースされました。
  • 外部ライブラリが使えない(と思っていてください)
  • COMオブジェクトの解放問題(後述)

目的は,Excelの単純作業でちょっとした自動化をはかることですのでデメリットは気にしていません。
対話モードではTab補完も効きますし,dir関数やhelp関数もそれなりに役立ちます。

IronPythonのインストールやドキュメントは公式サイトなどをご覧ください。

とりあえず,前編としてVBAからの移行する際のポイントをまとめてみました。

作業環境

Windows 10 Pro(Ver. 20H2)
Excel for Microsoft 365 MSO 32bit
IronPython 2.7.11 (2.7.11.1000)
.NET Framework 4.8.4341.0 (64-bit)

IronPythonとExcelの接続

注意:IronPythonによる操作は元に戻すことができないので,既存ファイルを操作する場合は事前のバックアップをおすすめします。

以下,対話モードでの実行を前提とします。

(1) IronPythonからExcelを起動する例

IronPython
import System    # 必須ではないが必要な場面多し
import clr
clr.AddReference('Microsoft.Office.Interop.Excel')
from Microsoft.Office.Interop.Excel import *
from System.Runtime.InteropServices import Marshal    # 後で使うのでインポートしておく
ex = ApplicationClass(Visible = True)
wb = ex.Workbooks.Add()    # 新規作成の場合
# 既存ファイルを開く場合
# path = r'C:\Users\user1\Desktop\sample.xlsx'
# wb = ex.Workbooks.Open(path)
ws = wb.Worksheets[1]

(2) すでに開いているExcelと接続する場合(ActiveSheetをターゲットにする例)

IronPython
import System
import clr
clr.AddReference('Microsoft.Office.Interop.Excel')
from Microsoft.Office.Interop.Excel import *
from System.Runtime.InteropServices import Marshal
ex = Marshal.GetActiveObject('Excel.Application')
wb = ex.ActiveWorkbook
ws = ex.ActiveSheet

単独セルの操作

値の設定

対象のワークシート指定が必須なことを除けば,ほぼVBAと同様です。
Valueプロパティの省略などもVBAに沿った扱いができます。
ただし,Excelの各オブジェクトについて,プロパティの引数は角かっこ[]で囲みます。
注意点がありますので,詳しくは後述します。

IronPython
ws.Cells[1, 1] = 123
ws.Cells[2, 'A'] = 'ABC'
ws.Cells['1', 'B'] = 456
ws.Range['B2'] = 'DEF'
ws.Range['A3'] = '2021/4/1'                         # 日付
ws.Range['A4'] = '令和3年4月1日'                    # 日付
ws.Range['A5'] = 'R3.4.1'                           # 文字列
ws.Range['A6'] = System.DateTime.Parse('R3.4.1')    # 日付(要import System)

余談ですが,セルA5の代入はws.Range['A5'] = '4.1.R3'とすると,日付データになります。

値の取得

セルの値を取得する場合は,Valueプロパティを使いますが引数を要するので()をつけます。
値が数値または文字列であればValue2プロパティでも同じ結果ですが,Value2プロパティには引数がないので()はつけません。
セルA1とA3の値をそれぞれのプロパティで取得します。

IronPython
val_A1 = ws.Range['A1'].Value()
val2_A1 = ws.Range['A1'].Value2
val_A3 = ws.Range['A3'].Value()
val2_A3 = ws.Range['A3'].Value2

結果を確認します。

IronPython 対話モード画面
>>> val_A1 
123.0
>>> val2_A1
123.0
>>> val_A3 
<System.DateTime object at 0x000000000000006F [2021/04/01 0:00:00]>
>>> val2_A3
44287.0

セルA3の値からわかるように,日付データをDateTimeオブジェクトとして処理する場合はValueプロパティを使用します。
なお,日付データの扱いは.NETとの関係として後編で触れます。

()と[]

VBAからIronPythonに移行する際に引っかかったのが,RangeCellsValueプロパティのように引数を要するプロパティでの()と[]の使い分けです。
以下,自己流の整理です。

  1. 引数は基本的に[]で囲む。
  2. ただし,引数を省略する(既定値を使用する)場合,空の[]は文法エラーになる。
  3. ()で囲んでもプロパティの値を返す。(関数呼び出しと見なして実行する?)
  4. 空の()は引数省略(既定値)として値を返し,文法エラーにならない。
  5. 代入文によるプロパティの設定では左辺に()を使うことができない。(関数呼び出しなので。)

IronPythonでのRangeオブジェクトの扱い

VBAの公式ドキュメントでは,次の「注釈」があります。IronPython上でRangeオブジェクトを扱うときも同様に動作します。

ここまでの検証

()や[]の扱いも含めて,検証してみます。

例1(左辺()の場合)

IronPython 対話モード画面
>>> ws.Range('D1') = 999    # 左辺は関数呼び出しだから代入できない
  File "<stdin>", line 1
SyntaxError: can't assign to function call
>>> ws.Range('D1')[1] = 999    # 左辺はws.Range('D1').Item[1].Valueなので代入可
>>>

例2(Value を明示してみる)

IronPython 対話モード画面
>>> ws.Range['D1'].Value[]    # 文法エラー
  File "<stdin>", line 1
    ws.Range['D1'].Value[]
                         ^
SyntaxError: unexpected token ']'
>>> ws.Range['D1'].Value[None]    # とりあえず引数1つを与えておけばエラーなく働く
999.0
>>> ws.Range['D1'].Value()    # 「セルの値を返す関数」として実行した状態
999.0
>>> ws.Range['D1'].Value    # かっこ無しだとオブジェクトを返す
<Microsoft.Scripting.ComInterop.DispCallable object at 0x000000000000007A [<bound dispmethod Value>]>
>>> _()    # 得られたオブジェクトを関数として実行
999.0

ws.Range['D1'].Value[None]は,検証のために試しましたが推奨するものではありません。
Valueプロパティの既定の引数を省略せずに明記すると
ws.Range['A1'].Value[XlRangeValueDataType.xlRangeValueDefault]
です。
またws.Range['A1'].Value[System.Type.Missing]で「既定値の使用」を明示することもできます。

例3(Valueの省略)

IronPython 対話モード画面
>>> ws.Range['D1']()    # ws.Range['D1'].Value()と等価
999.0
>>> ws.Cells[1, 'D']()
999.0

注目点は,Rangeオブジェクトの後に()をつけるとValueが得られる」ということです。
後編で応用してみます。

例4(動作を細かく調べてみる)

IronPython 対話モード画面
>>> ws.Range
<Microsoft.Scripting.ComInterop.DispCallable object at 0x0000000000000078 [<bound dispmethod Range>]>
>>> _['D1']
<System.__ComObject object at 0x0000000000000079 [System.__ComObject]>
>>> _()
999.0

その他,Range.Addressプロパティなども引数をとるので同様に確認することができます。

以上,引数を要するプロパティについて()と[]の使い分けなどを確認してみました。
蛇足ですが,メソッドはそもそもオブジェクトに作用する関数と見なせますので引数をとらない場合も末尾の()が必要になります。

例5(プロパティとメソッド)

IronPython 対話モード画面
>>> ws.Range['A1'].Select()    # Selectメソッドの実行(成功するとTrueを返す)
True
>>> ex.Selection()    # 見た目はメソッドだがex.Selection.Value()と等価
123.0

後始末について

IronPythonにおける「COMオブジェクトの解放問題」について,自身の環境下における結果だけをまとめておきます。

<結論>
「Excelを閉じた後,Marshal.FinalReleaseComObject(ex)を実行して,ipy.exeを終了する。」
注:exは,IronPythonと接続されたExcelの名前です。

この<結論>は,タスクマネージャーでバックグラウンドプロセスが残るか否かだけに絞った話ですので,安全性を保証するものではありません。

Excelを閉じると(自動・手動問わず),Excelはバックグラウンドプロセスに移ります。
Marshal.FinalReleaseComObject(ex)を実行してもバックグラウンドプロセスには残ったままですが,ipy.exeを終了するとほとんど間を置かずにバックグラウンドプロセスから消えます。
Marshal.FinalReleaseComObject(ex)を実行せずにipy.exeを終了すると,Excelがバックグラウンドプロセスに残ります。(タスクマネージャーで終了可能)

個人的には,次の手順をコピペして終了させています。

IronPython
wb.Close()
# wb.Close(True)  # 上書きして閉じる
# wb.Close(False) # 保存せず閉じる
ex.Quit()
Marshal.FinalReleaseComObject(ex)

この点はF#(fsi)の場合と大きく異なります。

セル範囲の扱いなど,後編に続きます。