Python for .NET で Microsoft.Office.Interop.Excel は難儀のようだぞ今のところ

やっぱ真っ先に思いつく需要はこれだろてことなのだが。

入り口だけには立っときたいなと思って少し格闘し始めて、すぐに「ブチ当たる」。

まず Microsoft.Office.Interop.Excel に限らず、の基礎的な話から。

.NET アッセンブリのうち、「皆が喜んで使えるもの」は、「GAC (Global Assembly Cache)」に登録されている。ワタシはちょっと誤解していたんだけれど、.NET 前夜の「COM」では、同じように「登録」という行為が必要な点は変わらないんだけれど、COM 流儀と .NET 流儀は全然シカケが違うんだね。

COM は、1. まずは自身のアイデンティティを GUID で主張する、2. COM 語でおはなしするための窓口のネェちゃんを IDL で定義する、3. これをレジストリに登録する、てのがまぁ「雑な」説明なんだけれど、「COM ライブラリを開発する」という行為をする場合、1. と 2. が「開発工程」の作業、3. がインストール時の作業(開発時も自身が使うために同じ行為が必要)ね。

上で「皆が喜んで使えるもの」なんてヘンチクリンな言い回しをしたのは、.NET が、「COM では「不可欠」として強制された一連のパブリッシュ手続き」が、.NET では一段階緩くなっていて、「ご自分にしか使わんもんならラフにやってよろしい」つーことらしいのね。つまり自家発電用途のものを作って使うぶんには、これでワタシが不安に感じたことは「杞憂」、アレでいいらしい。対して「皆が喜んで使える清く正しいライブラリ」を作って公開したいと願った場合に初めて「COM 流儀にあったような厳格な規約に従わねばならぬ」と。けどそれは COM のそれとはかなり違って、「厳格な命名(とバージョニング)規則に従わなければならない/public keytoken を主張しなければならない」という点が「そーね厳格ね」なのだが、まぁ登録は言っちゃえば「決められた場所に配置するだけ」。置き場所の規則も厳格だけれど、レジストリに魔法が潜むなんてことはなくて、ほんと「置くだけ」(実際は手で置くんじゃなく gacutil を使うみたい)。

でその「厳格な規約に従ってくれたありがたーいライブラリたち(だけが GAC に登録出来る)」が、まぁ開発者目線では「GAC のフォルダにほいほい置いてあるだけ」にみえる。場所は古いものが c:/Windows/assembly に、新しいものが c:/Windows/Microsoft.NET/assembly (.NET 4.0 以降) にいる。なのでここにいる連中は基本、pythonnet から Assembly.LoadWithPartialNameclr.AddReference でロード出来る。

てわけで、GAC を漁れば Microsoft.Office.Interop.Excel とか Microsoft.Office.Tools.Excel がすぐに見つかる。無論これまで何をインストールしてきたかとか、プリインストールの状態とかにもよるけどとにかく「自分ちで使えるもの」はすぐにわかる。(COM は大変だったのよ、COM/OLE ビューワなる専用のビューワで探索するしかなかったのだが、まぁこれが「膨大」でなぁ。)

さぁ始められるぞ…:

1 import clr
2 from System.Reflection import Assembly
3 #clr.AddReference("Microsoft.Office.Interop.Excel")  # NG.
4 Assembly.LoadWithPartialName("Microsoft.Office.Interop.Excel")
5 import Microsoft.Office.Interop.Excel as Excel
6 
7 xlApp = Excel.Application()

ここまでは良かったの。で、例えば Read Excel File in C# を真似していきゃすぐだろ、て思うわけだ。けどこれがダメなのね。

どうやら「Excel を扱う .NET ライブラリ」は「本物の .NET と本物の COM」のミックスで実現されているらしく、使いたい Workbook などが全て .NET で包まれてない模様。これを .NET for Python (pythonnet) からストレートに扱えない:

1 Traceback (most recent call last):
2   File "xxx.py", line 26, in <module>
3     xlApp.Workbooks.Open("new-book.xlsx")
4 AttributeError: '__ComObject' object has no attribute 'Open'

これは 公式の issue として挙がっている。面白いというか救いなのが、issue を挙げた本人(Owner)が 「解決策」を挙げていることだったりもする。.NET for Python に取り込まれてくれたらありがたいが、「そんなもんです」と受け容れてしまうのもないではないか…。この解決策はまだ試みてないけど、これでイケるようなら、これまでは「Excel をフルに扱えるまともなもの」は

  • pyExcelerator
  • xlrd, xlwt, xlutils
  • PyWin32 の COM/OLE 連携

しかほとんどないに等しかったけれど、選択肢が一つ増えてハッピーだね、てね。特に最後の PyWin32 とは完全に競合し、PyWin32 を「捨ててもいい」となりそうなのが個人的には嬉しかったりはする。

なお、「.NET for Python で本物の COM を使う」方法は公式ドキュメントに書かれている。けど今回のこの問題とは違うような気はする。違わないのかもしれないけれど、ただ、今回のヤツって、「Application オブジェクト」を取るとこまではほかの .NET 使いと全然変わらんのよね、だから「違う」んだと思う。


21:10 追記:
イケた:

 1 # -*- coding: utf-8 -*-
 2 from __future__ import absolute_import
 3 from __future__ import unicode_literals
 4 from __future__ import print_function
 5 
 6 import os
 7 
 8 import clr
 9 from System.Reflection import Assembly
10 #clr.AddReference("System.Reflection")
11 from System.Reflection import BindingFlags
12 
13 #
14 Assembly.LoadWithPartialName("Microsoft.Office.Interop.Excel")
15 import Microsoft.Office.Interop.Excel as Excel
16 
17 from System import Array
18 import System
19 
20 #
21 class comobj(object):
22     """
23     TNX:
24 
25 https://gist.github.com/denfromufa/ec559b5af41060c5ac318f7f59d8b415#file-excel_interop_vsto-ipynb
26 
27     """
28     def __init__(self, obj):
29         #AttributeError: _comobj__ComObject???
30         #if not isinstance(obj, clr.System.__ComObject):
31         #    raise TypeError('Type not System.__ComObject for obj {}'.format(type(obj)))
32         self.obj = obj
33         self.typ = obj.GetType()
34 
35     def __getattr__(self, name):       
36         def newm(*argsv):
37             if not argsv:
38                argsv=(None,)
39             return self.typ.InvokeMember(
40                 name,
41                 BindingFlags.InvokeMethod | BindingFlags.GetProperty,
42                 None, self.obj, *argsv)
43         return newm
44 
45 
46 #
47 xlApp = Excel.ApplicationClass()
48 xlApp.Visible = True
49 
50 books = comobj(xlApp.Workbooks)
51 arrobj = Array[System.Object]
52 arr = arrobj.CreateInstance(System.Object, 1)
53 arr[0] = os.path.abspath("book.xlsx")  # existing book
54 books.Open(arr)
55 books.Close()

まぁ「.NET や COM 経由で Excel を扱う」こと自体が苦痛な作業ではあるわけで、「おぉすげーぜ」と思うためにはそもそも「Python らしいことをしたいから」というモチベーションが不可欠だよ。例えば「計算は NumPy 使ったるぜをら」とかさ。そうでないなら Python から扱えても「うれしぅない」と思うのが普通。そしてやってみた例からわかるように、「C# より遥かにダルい」のは明白、「comobj 問題」がなくても。