ふにゃるんv2

もとは、http://d.hatena.ne.jp/Wacky/

wxWebKitを使って、WebKitエンジン製の簡易なブラウザを作ってみる

最近、WebKitと その派生版が流行っておりまして、自分も色々と勉強中です。
で、WindowsWebKitを扱うのに便利なライブラリは無いかしら?と思って色々と調べていました。


で、現時点で、自分が理解した事を、簡単にメモしておきます。(嘘が入っているかも知れません)

  1. WebKitは外部からActiveXコントロール制御できない。
    どうやら商用でActiveXコントロール化した派生はあるようなんですが、WebKit自身はActiveXコントロール化されていない模様で、各種言語から CreateObject 等を使って外部から制御する事は出来ないようです。
  2. Windows版の内部実装は、COM実装されている模様。
    少なくとも、Windows版は AcitveXの基底技術である COMで実装されています。
    ただ、OLEオートメーションで必要なインタフェースを実装していないので、外部から制御できないと。
  3. WebKit技術を使ったライブラリは幾つかあるが、上記理由の為、実装が各ライブラリ専用に留まる。
    ライブラリ毎にWebKitが小分けになっているので、C#用のライブラリをセキュリティアップデートしたからと行って、QT用のライブラリの中身が入れ替わるという事は無いようです。
    イメージ的には、IEのライブラリはDLLに対するラッパー実装。WebKitのライブラリはスタティックLIB実装。って感じですかね。
  4. 派生ライブラリとしては、.NET用、wxWindows用、QT用、GTK用 等がある。
    ちなみに、各ライブラリ共 ほぼオレAPI化されており、互換性はあまり無いと思った方が良いと思います。
    (IEは、ActiveX化されているので、APIは ほぼ統一)


WebKitを調べる際、WebKitの外部制御のサンプル事例(Mac OS除く)が IEに比べて異様に少ないなぁと思ったのですが、通常だとC++言語+ソースコードを用意しないと手が出せない。各種ライブラリがオレ実装状態。じゃぁ、しょうがないなぁ。という感じですね。

上記状態からして、巷でよく見るように、WebKitIEを比べるのは意味が無いなぁ。と感じました。
実装形態が全く違う(汎用のIE、専用のWebKit)ので、速度やセキュリティに差が出るのは当然かなぁ、という感じです。

Windowsで使えそうな派生ライブラリ

で、話を戻しまして、Windows上でWebKitエンジンを簡易に使えるライブラリとして、どんなものがあるか少し調べてみました。
ちなみに、Pythonバインディング中心です。(他の言語は、あまり勉強していないので真面目に調べていません。ははは)


1.WebKitオリジナル
C++コンパイラを用意できるなら、WebKitの本家からソースコードをダウンロードしてきて、ビルド環境を揃えて、ビルド出来たら使えます。

正直難易度が高いです。


2..NET用ライブラリ
WebKit.NETという.NET用のライブラリを作っている所があります。


Windowsプログラマだったら、一番これが扱いやすいかな?と思います。
何しろ、ライブラリ一式をダウンロードした後、ファイルを展開し、アセンブリを参照して、フォームに WebKitコントロールを ぺたっと貼り付けるだけですからね。
APIも、IEを意識しているようで、かなり似せています。


3.Python用のwxWebKitライブラリ
まだプレビュー版ですが、wxWebKitというwxPythonWebKit専用ライブラリがあります。


おっPy派なら、多分これが扱いやすいかな?と思います。
とりあえず、Windowsインストーラー形式でライブラリが配布されているので、ぽんぽんインストールして、少しスクリプトを書くだけで使えますからね。
今回のネタは、これを使って簡易ブラウザを作っています。


4.Python用のpyWebKitGtkライブラリ
これも開発中のようですが、pyWebKitGtkというpyGTK用のライブラリがあります。


スクリーンショットを見るとWindowsでも扱えるようなのですが、ソースだけの配布なので少々難易度が高めです。
Wikiメーリングリストを眺め回してみたんですが、開発者の方々はLinuxメインのようで、Windowsポートについてあまり言及している箇所が見つからなかったんですね。


5.QT用のWebKitライブラリ
QT用にもWebKitライブラリがあるようです。


が、まだ試していません。
QWebView クラスの説明では、WebKit云々と書いてあるので、多分使えると思います。

wxWebKitをインストールする

長い話でしたが、wxWebKitを使って、Pythonで簡易ブラウザを作ってみましょう。

今回は、Python 2.6を入れた後、以下のWindowsインストーラを入れました。

  • wxPython2.8-win32-unicode-2.8.10.1-py26.exe
  • wxWebKit-win32-wx2.8-Py2.6-preview1.exe


これを入れた後、Pythonを立ち上げて 以下を手打ちしてみて下さい。

$ python
Python 2.6.1 (r261:67517, Dec  4 2008, 16:51:00) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import wx
>>> import wx.webview
>>> app = wx.PySimpleApp()
>>> frame = wx.Frame(None)
>>> w = wx.webview.WebView(frame)
>>> w.LoadURL("http://www.google.com")
>>> frame.Show()
True
>>> app.MainLoop()

以下のウィンドウが表示されたらOKです。(ウィンドウが前面に出ないので、タスクバーをよく見てください)
1
1 posted by (C)wacky

結構簡単ですよね?

wxWebKitを使って、簡易ブラウザ

では、次に簡易ブラウザを作って見ましょう。
以下にコード例を示します。

# -*- coding: utf_8 -*-  
import sys
import pdb
import wx
import wx.webview

class MyWindow(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id, u"WebViewを使ったWebブラウザ", size=(640,400) )
        
        box = wx.BoxSizer(wx.VERTICAL)
        sz1 = wx.BoxSizer(wx.HORIZONTAL)
        if True:
            self.urlLabel = wx.StaticText(self, -1, "URL:")
            self.urlText  = wx.TextCtrl(self, -1, "http://www.google.com", size=(300, 20) )

            self.goButton = wx.Button(self, label="GO!")
            self.Bind( wx.EVT_BUTTON, self.OnSubmit, self.goButton )
            
            sz1.Add(self.urlLabel, 0, wx.LEFT, 20)
            sz1.Add(self.urlText,  0, wx.LEFT, 20)
            sz1.Add(self.goButton, 0, wx.LEFT, 20)
        
        self.web = wx.webview.WebView(self, size=(300, 400))
        self.web.Bind(wx.webview.EVT_WEBVIEW_LOAD, self.OnLoad)
        
        box.Add(sz1, 0, wx.EXPAND)
        box.Add(self.web, 1, wx.EXPAND)
        
        self.SetSizer(box)
        self.SetAutoLayout(True)
        box.Fit(self)

    def OnSubmit(self, event):
        print "set URL:", self.urlText.GetValue()
        self.web.LoadURL(self.urlText.GetValue())
    
    def OnLoad(self, event):
        print "load state:", event.GetState()
        if event.GetState() == wx.webview.WEBVIEW_LOAD_DL_COMPLETED:
            o = self.web.GetMainFrame()
            html_code = o.GetPageSource()       # HTML code
            print type(html_code)
            print html_code.encode("cp932", "ignore")

if __name__=='__main__':
    app=wx.PySimpleApp()
    frame=MyWindow(parent=None,id=-1)
    frame.Show()
    app.MainLoop()

上記コードを実行すると、以下のようになります。
2
2 posted by (C)wacky


たった、これだけのコードで、簡単なブラウザが作れました。

最後に

いかがでしたでしょうか?
WebKitを素のままで使うのは少々骨が折れますが、スクリプト言語サポートを介してなら、比較的簡単に制御できます。


ちなみにここまで書いておいて何ですが、リンクするだけでOKなWebKitライブラリのバイナリ版無いですかね?
ソースコードから何度もビルドしようとしているんですが、全然うまく行かないです。orz

2010/05/16: 追記

WebKitは外部からActiveXコントロール制御できない。」の所ですが、どうも「ActiveX登録すれば OLEオートメーションによる制御が可能」のような感じ。
register serverしたら、もしかしたらうまく出来るのかなぁ?

[python] HTMLをWebDAV経由でiPod touchに送って、オフライン閲覧しよう

今更ですが、最近iPod touchを購入しました。
何の為かというと、

  • 先日長距離出張がありまして、本を読むのだけじゃヒマになりそうだなぁ。というのが発端。
  • 机に向かっていると、段々 お○が痛くなるので、ベッドや安楽イスでネットでカウチをしたいなぁ。というのが本音。
  • 値段をチェックすると、結構安い事に気付いた。というので決定。

という次第です。
ちなみに、iPhoneにしなかったのは、常時ネット接続すると自分で歯止めが効かなくなりそうだな。と思ったのです。


購入してから暫くいじってみて、確かにユーザーインタフェースが優れているなぁと感心しました。
後、ごてごてしていない機能デザインも良いですね。
自分には逆上がりしても真似出来ないですね。(周りの要求に屈しやすい私)

オフラインなiPod

とまぁ、前振りはさておいて問題です。
iPod touchはネットワーク環境として Wi-Fiが使える訳ですが、外に出ると このWi-Fiって、意外と使える所が少ない訳です。
そういう人の為に、PHSや携帯のネットワークに繋ぐアダプタが売られているようなんですが、自分はそこまでネットワークへの強い渇望がある訳じゃないんですね。


会社への行き来で空いたヒマな時間を潰せれば十分なんです。
だから、例えばオンライン状態(家とか)で、ちょこちょこコンテンツをダウンロードしておいて、ヒマな時間で見るのでも(今は)十分事足りるんですね。


ニュースサイトとかは専用アプリで、オフラインでも読めるものがあったので、早速使っているのですが、問題は個人のWeb site。
自分は、定期的にいつも見ている個人Web siteがあるので、これをオフラインで見られると良いだろうなぁ。と思いました。
が、標準のWebブラウザは、お気に入りの保存程度しかない。
手動でWebをHTMLに落としおいて iPod touchへUSB経由でコピーし、後で見る? …面倒でやってられません。


何か方法が無いか?と思って調べてみました。


・候補1.「Read it Later」などの後で読むサービスを使う
以下に示すように、「Instapaper」と「Read it Later」が有名なようです。

無料版をちょっと試してみたんですが、

  • 「読んだ」ボタンを押すとブックマーク?が消える。
  • 残したブックマークも、ブックマークした時点の情報しか残ってないので、定期的に見る事が出来ない。

という自分的使い方に沿わない問題がありました。
何か他の方法がある気もしたんですが、Blogでの体験記事を見る限り、自分の目的とは別の使い方をされているようで…。これ以上の調査を諦めました。


・候補2.「iWebSaver」と「Web2Touch」でオフラインなブックマークを使う
以下に示すように、ブックマークにコンテンツ情報を記録する事で、オフラインでも閲覧できるようです。

見つけた時は、「キタ、コレ!」と思いました。
が、少し問題が。

  • 一々 iTunes を立ち上げるのが面倒(マシンが古くて、iTunesの起動がもっさりなので)。
  • USB接続なんぞ面倒でしたくねぇ(毎日コネクタ挿抜なんて怖いよぅ)。

という個人的わがままです。(ヤレヤレ

AirFilesとWebDAV

何か良い方法は無いかな?と思ってWebサーフィンしていたら、ふと、ファイルをWi-Fi経由で送り込んで iPod touch上で見る事ができるソフトがある事を思い出しました。

そういえば、HTMLヘルプを iPod touchで読みたいが為に、AirFiles というソフトを購入していたのだった。


これを使うと、WebブラウザWebDAVクライアントを使って Wi-Fi経由で iPod touchに送れるんですね。


これを思い出した結果、

  1. HTMLファイルを定期巡回してローカルにダウンロード
  2. WebDAVクライアントを使って、ダウンロードしたHTMLファイルを iPod touchに転送
    (Webブラウザを使わないのは、自動化が面倒そうだから)
  3. iPod touchのAirFilesで、HTMLファイルをオフライン閲覧

という自分的解決方法を思いつきました。

WebDAVを使った自動転送の実装

大分寄り道しましたが、いよいよ実装です。
今回は、わたなべさんのBlogで公開されている PythonWebDAVクライアントライブラリを、利用させてもらう事にしました。ありがとうございます。


そのままでは使う事が出来なかったので、以下のWeb siteの資料を眺めつつ、勝手にリファインさせて頂きました。


改造したコードと、それを使った自動化転送用のスクリプトとバッチファイルは、↓ここに置いています。ご自由にどうぞ。

プログラムの説明

上記の圧縮ファイルには、以下の3ファイルが入っています。

  • gethtml.py
    Webショートカットを読み取って、WebからHTMLファイルにダウンロードするスクリプト
  • filesys.py
    ローカルのHTMLファイルを、WebDAVを使ってiPod touchのAirFilesストレージにアップロードするスクリプト。(わたなべさんのソースを弄っています)
  • up_air.bat
    上の2つを連動させるバッチファイル。単に連続して呼び出しているだけです。


以下のように処理を行う流れを想定しています。

  1. 定期的に巡回したいWeb siteを、Webショートカットファイル(お気に入りの実体ファイル)として、適当なフォルダに配置する。
    仮に、'C:\LINK'フォルダに置くとします。
  2. gethtml.pyを呼び出して、Webショートカットを入れたフォルダを指定し、WebからHTMLファイルをダウンロードして指定フォルダに保存する。
    例えば、「python gethtml.py C:\LINK\* C:\HTML」とすると、'C:\LINK'にあるWebショートカットファイルを順次、'C:\HTML'フォルダに保存します。
  3. iPod touchから AirFiles アプリを起動して、タイトル画面の「Wi-Fi 情報」が出た所で、一旦止める。
    (AirFilesのサイト画像より)
  4. filesys.pyを呼び出して、ローカルのHTMLファイルを iPod touchの AirFilesの管理ストレージにアップロードする。
    例えば、AirFilesの起動メッセージが「http://192.168.2.103:8080」とあれば、「python filesys.py --upload http://192.168.2.103:8080/Myfiles/ C:\HTML\*.htm」とすると、'C:\HTML'フォルダの全てのHTMファイルを iPod touchに転送します。

終わりに

  • まずは作ってみたので、これで どれだけ使い物になるか暫く使ってみようかな、と。
  • 正直、リッチなHTMLでなくてもOKなので、w3mとかLynxを使って、テキストに落としたものを見るようにしようかなぁ。
  • WebDAVクライアントのパッチ修正なんですが、非常にテキトーです。
  • AirFilesのストレージは、WebDAVから見ると、/Myfilesの下に既定フォルダが並んでいる点にご注意を。
    /(ルート)の直下にファイルを置くと、AirFilesの画面に何も現れない事になります。
  • iPhone4.0とかやっているけど、スクリプト言語をアプリに登録してくれないかしらん?
    ○獄すりゃOKのようですけど。
  • filesys.pyは、FTPクライアントも実装しているので、FTPサーバーを持つiPodアプリ(iComicとか)に応用が利くかも。

VC++ 2008で、プロジェクトのみに適用する環境変数を設

唐突ですが、久しぶりに、Visual C++を持ち出してビルドしていました。
いや、用意されたSDKが、C/C++環境でないとビルド出来ないようになってまして、しょうがなしに なんですけどね。
(最近は、ちょびちょび改良するなら Python。それ以外なら C# のどちらかに なってきています)


SDKに用意されていたサンプルが、バッチ+makeビルドでして。
何度もコマンドプロンプトを開いて→コマンドを打ち込んでEnterの繰り返し作業に段々飽きてきまして、Visual C++ 2008上でビルド出来ないかな?と思った訳です。
(まぁ、オートコンプリート使いたいというのもあります)

問題

で、Visual C++ 2008上でプロジェクトを作って、設定内容を移植していて気付いたんですが、使用するSDK特有の環境変数を、どのようにすれば持って来れるんだろう?という事です。


プロジェクトのプロパティ設定で、環境変数の設定欄は特に見当たらないし、ビルド前イベントから SET コマンドを使って設定を掛けてみたんですが、どうも うまく行かない。


で、他の方々はどうしているんだろう?と、ぐぐって見たんですが、システム→プロパティの環境変数に設定するのが ちらほら出てくるのみ。
これだと、全システムに影響するので、今一しっくり来ないんですね。


プロジェクト単位に設定する方法があるんじゃないか?とMSDN Libraryを眺め回していました。

設定方法

で、答えですが、ちゃんとプロジェクト単位に環境変数を設定する方法はあります。

早速ですが、(自分用メモとして)画面付きで紹介します。

  1. 「表示」→「その他のウィンドウ」→「プロパティ マネージャ」で、プロパティマネージャを開く。
    1
    1 posted by (C)wacky
  2. プロパティマネージャ上で右クリック→「新しいプロジェクト プロパティシートの追加」を選ぶ。
    2
    2 posted by (C)wacky
  3. 適当な名前で、プロパティシートファイル(vsprops)を作る。
    3
    3 posted by (C)wacky
  4. プロジェクトにプロパティシートに対して(ここでは、myprop)、マウスでダブルクリックする。
    4
    4 posted by (C)wacky
  5. プロパティシートエディタが開くので、「共通のプロパティ」→「ユーザーマクロ」を選択し、「マクロの追加」ボタンを押す。
    5
    5 posted by (C)wacky
  6. ここで、追加したい環境変数名を「名前」欄に、環境変数値を「値」欄に、「ビルド環境でこのマクロを環境変数に設定する」にチェックを入れる。
    6
    6 posted by (C)wacky
  7. これで環境変数(マクロ)が追加される。
    7
    7 posted by (C)wacky
  8. 後は、「$(環境変数名)」という形で利用すればOK。
    例えば、下図は追加のインクルードディレクトリの設定に、追加した環境変数を参照に使っている。(「%環境変数名%」では無い事に注意しましょう)
    8
    8 posted by (C)wacky


いかがでしょうか?
判ってみれば、大した事は無かったんですが、これに至るまで暫く悩んでいました。(何で、SET コマンドの環境変数設定が反映されないんだ?)

グランドマスター!

実は、先週くらいに、この次の巻が出ているのですが、バカ笑いした記念に。

グランドマスター!―聖都をめざせ (コバルト文庫)

グランドマスター!―聖都をめざせ (コバルト文庫)


というか、これがコバルト文庫から出ている事が信じられない。(いい意味で)

葛藤するハルセイデスの心の動きを読んだかのように、ミイラのような老婆がガバとはねおきた。老人とは思えぬ力でハルセイデスの首に「がしっ」と両腕をまわすと、目をとじたままホオを染め、んーっと唇とタコのようにつきだす。

この後のオチが傑作なんですが、いや、ゴーレム団長@ハルセイデスの苦難の旅の面白い事、面白い事。
アニメになったら、絵的に面白いと思うんですがね。

win32comでの、Excelのワークシート操作の高速化テクニック

win32comを使って、Excelのワークシートを操作する時、普通に制御すると結構時間が掛かると思います。
それを、少しでも速くしましょう。ってネタです。


話は飛びますが、Excelを操作するってケースは結構多いようで、ちょっとググるだけで、ポンポン出て来ます。

まぁ、仕事で大抵必須ですからねぇ。

普通のワークシート操作の場合

まずは、普通〜に、ワークシートを操作するプログラムを作る場合、どうなるか?を見てみましょう。
大抵の場合、こんな感じに書くハズです。

#!/bin/env python
# -*- encoding: cp932 -*-
"""
    ExcelのCOMオブジェクトの使い方テスト
"""
import sys
import win32com
import pdb
from win32com.client import *
import pprint
import datetime
import msvcrt

def main():
    xapp = win32com.client.Dispatch("Excel.Application")
    xapp.Visible = True
    
    book = xapp.Workbooks.Open(r"H:\user\test3\sp2Changes.xls")
    sheet = book.Worksheets.Item(r"SP-2 Changes")
    
    wk = sheet.UsedRange
    x_cnt = wk.Columns.Count
    y_cnt = wk.Rows.Count
    print "x_cnt=%d y_cnt=%d" % (x_cnt, y_cnt)
    
    st_tm = datetime.datetime.today()
    cnt = 0
    for y in range(1, y_cnt):
        for x in range(1, x_cnt):
            print "check x=%d y=%d  \r" % (x + 1, y + 1),
            if wk.Cells(y, x).Value != None:
                cnt += 1
    print "総有効セル数=%d" % (cnt)
    en_tm = datetime.datetime.today()
    print "検査開始:%s\n検査終了:%s\n実行時間:%s" % (st_tm, en_tm, en_tm - st_tm)

if __name__ == "__main__":
    main()

これを私の手元の環境(P4 2.6GHz。何世代前のマシンやねん orz)で動かすと、以下の結果でした。

$ python test_1.py
x_cnt=35 y_cnt=624
総有効セル数=4873
検査開始:2009-10-11 19:21:43.406000
検査終了:2009-10-11 19:22:54.984000
実行時間:0:01:11.578000

ふむふむ、1分11秒(77秒)ですか。

改善したワークシート操作の場合

まず、答えから言うと、以下のWeb siteで紹介されているテクニックを使います。


上で示されたテクニックを、Python風に書き直すと、以下のようになります。

#!/bin/env python
# -*- encoding: cp932 -*-
"""
    ExcelのCOMオブジェクトの使い方テスト
"""
import sys
import win32com
import pdb
from win32com.client import *
import pprint
import datetime
import msvcrt

def main():
    xapp = win32com.client.Dispatch("Excel.Application")
    xapp.Visible = True
    
    book = xapp.Workbooks.Open(r"H:\user\test3\sp2Changes.xls")
    sheet = book.Worksheets.Item(r"SP-2 Changes")
    
    wk = sheet.UsedRange
    x_cnt = wk.Columns.Count
    y_cnt = wk.Rows.Count
    print "x_cnt=%d y_cnt=%d" % (x_cnt, y_cnt)
    
    st_tm = datetime.datetime.today()
    cnt = 0
    wk2 = wk.Value
    for y in range(0, y_cnt - 1):
        for x in range(0, x_cnt - 1):
            print "check x=%d y=%d  \r" % (x + 1, y + 1),
            if wk2[y][x] != None:
                cnt += 1
    print "総有効セル数=%d" % (cnt)
    en_tm = datetime.datetime.today()
    print "検査開始:%s\n検査終了:%s\n実行時間:%s" % (st_tm, en_tm, en_tm - st_tm)

if __name__ == "__main__":
    main()


これを私の手元の環境で実行すると、以下の結果が得られました。

$ python test_2.py
x_cnt=35 y_cnt=624
総有効セル数=4873
検査開始:2009-10-11 19:24:20.234000
検査終了:2009-10-11 19:24:23.406000
実行時間:0:00:03.172000

おっと、3秒です。
先の実行結果から、96%の改善です。速いですね。

参考:2つのコードの違い

ちょっと判りにくいかと思うので、ユニファイド形式でのdiff結果を載せておきます。

$ diff -u test_1.py test_2.py
--- test_1.py   2009-10-11 20:38:36.703125000 +0900
+++ test_2.py   2009-10-11 20:38:23.390625000 +0900
@@ -25,10 +25,11 @@

        st_tm = datetime.datetime.today()
        cnt = 0
-       for y in range(1, y_cnt):
-               for x in range(1, x_cnt):
+       wk2 = wk.Value
+       for y in range(0, y_cnt - 1):
+               for x in range(0, x_cnt - 1):
                        print "check x=%d y=%d  \r" % (x + 1, y + 1),
-                       if wk.Cells(y, x).Value != None:
+                       if wk2[y][x] != None:
                                cnt += 1
        print "総有効セル数=%d" % (cnt)
        en_tm = datetime.datetime.today()

何が、どう変わったのか?(技術的な話)

上のテクニックは、「セルを配列に入れる」と書いてあります。
これを技術的な言葉に変換すると、「アウトプロセスサーバー呼び出しの回数を減らす」でしょうか。


つまりですね。

  • 最初のコードの場合、「if wk.Cells(y, x).Value != None:」のアウトプロセスサーバー呼び出しを、35列×624行=21840回呼び出す。
  • 次のコードの場合、「wk2 = wk.Value」で、一旦Pythonプロセスの二次元配列に変換&コピーし、後はPythonプロセス内で「if wk2[y][x] != None:」の内部配列参照を行っている。つまり、「1回だけ」アウトプロセスサーバー呼び出している。

って事なんです。
21840回 VS 1回 じゃ、比べ物にならないですよね。そういう事なんです。


ちなみに、アウトプロセス呼び出しって何やねん?という場合、こんな感じで捉えれば良いかと。

  • インプロセス呼び出し:DLLの関数を呼び出す
  • アウトプロセス呼び出し:他所のプロセス(EXE)の関数を呼び出す
  • リモートプロセス呼び出し:他所のマシンのプロセス(EXE)の関数を呼び出す


Pythoのライブラリレベルで強引な例えをすると、以下の感じでしょう。

  • インプロセス呼び出し:スクリプト内の関数を呼び出す
  • アウトプロセス呼び出し:Python2.6に備わった、multiprocessingモジュールを使って関数を呼び出す
  • リモートプロセス呼び出し:xmlrpclibモジュールを使って関数を呼び出す


という訳で、このテクニックを使えば、大分速くなりますよ。と。

余談

Webで眺めていると、他の高速化テクニックとして、以下がありました。

  • Application.ScreenUpdating = False : 画面更新を抑える
  • Worksheet.EnableCalculation = False : シートの再計算を抑える


このテクニックは、逐次描画を抑える事で高速化に貢献しようというTipsでして、頻繁に描画更新される状態じゃないと、あまり高速化に貢献してくれませんでした。
それよりかは、アウトプロセスサーバー呼び出しの回数を如何に抑えるか、コードの呼び出しを見直した方が良いかも知れません。


後、自分でプログラムを作っていた時の話ですが。
自分のマシンが遅い事もあって、win32com.client.Dispatch("Excel.Application")でプロセスを起動させるのに結構時間を喰っていました。(後、Openもバカにならない程、時間を喰う)
なので、Excelプロセスが起動済みなら、そこにアタッチする事で、少し時間を稼ぎました。(win32com.client.GetObject(None, "Excel.Application"))


他にどんな高速化があるかなぁ。

お助け〜

自信ありげに説明していますが、自分でも困っている事があります。
上のセル情報を、一括で配列に叩き込む手法なんですが、Range.Interior オブジェクトや、 Range.Comment オブジェクトには効かないんですよね。


もし、こうやれば Range.Value 以外のオブジェクトに対して、配列技が使えるぜ!という方が居られましたら、教えてくださいませ〜。

win32comで、makepyを使って COM(AcitveX)の定数をロードさせる手順について

Pythonで、COM(ActiveX)を操作する際は、win32com下のクラスを使う事になります。
この時、ActiveXの中でタイプライブラリとして定義している定数値を使えると、コードの可読性が良くなって便利です。


Pythonの場合、win32com.client.constants 下に、定数値が列挙されるようになっていますが、makepyツールを使って予めタイプライブラリ情報をPythonに教えておく必要があります。
それは、以下のWeb siteの記述にも書かれています。

Python では、タイプライブラリで定義された定数を利用できます。 Ruby では、指定したモジュール内に定義されますが、Python では win32com.client.constants という名前空間で定義されます。
''COM の定数を利用するには、あらかじめ MakePy ユーティリティを使って、利用可能な状態にしておく必要があります。'' MakePy ユーティリティは、PythonWin というツールからメニューで選択できます。


ここでは、その設定手順について簡単に説明します。(時折忘れるので、自分に対してのメモの意味合いもあります。ははは)

準備開始前

例えば、Excelの定数値を設定しようとします。
Pythonから、Excelオブジェクトを操作しようとする場合、大体以下のようにすればOKです。

$ python
Python 2.6.1 (r261:67517, Dec  4 2008, 16:51:00) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import win32com.client
>>> o = win32com.client.Dispatch("Excel.Application")
>>> o.Help()  <= ヘルプファイルが開く

この段階で、Excelオブジェクトの定数が使えるか?というと、使えません。
こんな感じです。

>>> win32com.client.constants.__dicts__
[]

mekepyを呼び出す

makepyを呼び出すと、Pythonの「Lib\site-packages\win32com\gen_py」ライブラリフォルダ下に、指定したCOMオブジェクトのキャッシュが出来上がります。
Excelオブジェクトのタイプライブラリをロードする前、例えば以下の様だったとしましょう。
1
1 posted by (C)wacky


makepyは、Pythonの「Lib\site-packages\win32com\client\mekepy.py」にあります。
これを呼び出すと、以下のダイアログが出ます。
2
2 posted by (C)wacky

今回は、Excelオブジェクトを選択します。
すると、以下のように正常終了すると思います。
これで設定準備完了です。

C:\tool2\Python26\Lib\site-packages\win32com\client>python makepy.py
Generating to C:\tool2\Python26\lib\site-packages\win32com\gen_py\00020813-0000-
0000-C000-000000000046x0x1x3.py
Building definitions from type library...
Generating...
Importing module


Lib\site-packages\win32com\client\mekepy.py」フォルダを確認すると、何かファイルが増えているようです。
3
3 posted by (C)wacky

再び使う

再び、Pythonを起動して、Excelオブジェクトをロードします。
すると、win32com.client.constants下に、Excelオブジェクトの定数が辞書形式で入っている事に気づきます。

$ python
Python 2.6.1 (r261:67517, Dec  4 2008, 16:51:00) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import win32com.client
>>> o = win32com.client.Dispatch("Excel.Application")
>>> win32com.client.constants
<win32com.client.Constants instance at 0x00C5D3C8>
>>> win32com.client.constants.__dicts__
[{'xlMixedScript': 3, 'xlParamTypeInteger': 4, 'xlExtended': 3, 'xlPivotChartDro
pZone': 32, 'xlWK1FMT': 30, 'xlHairline': 1, 'xlEMDFormat': 10, 'xlExcel4MacroSh
eet': 3, 'xlDialogPageSetup': 7, 'xlRange': 2, 'xlCellTypeAllFormatConditions':
-4172, 'xlShort': 1, 'xlPyramidColClustered': 106, 'xlPinYin': 1, 'xlNoSelection
...
>>> win32com.client.constants.xlEnd
2
>>> win32com.client.constants.xlColorIndexAutomatic
-4105

ちょっとしたテクニック

定数値は辞書形式に入っている為、「あぁ〜、あの定数名の綴り、何だっけ? xlの…」といった時、以下の簡単な関数を作れば対応できます。(10/12追記>__dicts__が2つ以上のケースを忘れてた。orz)

>>> def find_const(find_str):
...   ret = []
...   for i in range(len(win32com.client.constants.__dicts__)):
...     for s in win32com.client.constants.__dicts__[i].keys():
...       if s.find(find_str) >= 0:
...         ret.append(s)
...   return ret
...
>>> find_const("xlE")
['xlExtended', 'xlEMDFormat', 'xlExcel4MacroSheet', 'xlEndSides', 'xlErrorBarInc
ludeBoth', 'xlErrorBarTypeFixedValue', 'xlExternal', 'xlExcel2FarEast', 'xlEditB
ox', 'xlExcel7', 'xlExcel5', 'xlExcel4', 'xlExcel3', 'xlExcel2', 'xlErrorBars',
'xlEditionDate', 'xlExclusive', 'xlErrorBarIncludePlusValues', 'xlEdgeLeft', 'xl
EdgeRight', 'xlExponential', 'xlEqual', 'xlErrorBarIncludeMinusValues', 'xlExcel
4IntlMacroSheet', 'xlErrRef', 'xlErrNA', 'xlEnd', 'xlEPS', 'xlExcelLinks', 'xlEr
rorBarTypeCustom', 'xlEdgeTop', 'xlExcelMenus', 'xlErrValue', 'xlEntireChart', '
xlExcel9795', 'xlErrorBarIncludeNone', 'xlErrDiv0', 'xlEntirePage', 'xlErrorBarT
ypeStDev', 'xlErrNum', 'xlErrorHandler', 'xlEdgeBottom', 'xlExpression', 'xlErro
rBarTypeStError', 'xlErrNull', 'xlErrors', 'xlExcel4Workbook', 'xlErrorBarTypePe
rcent', 'xlErrName']
>>>

win32comって便利ですね。

まだ試していませんが

まだ試していませんが、py2exeでEXE化する時、躓く可能性があるようです。
その際は、以下のWeb siteをどうぞ。

そうそう

win32comは、以下からダウンロードしてインストールして下さいね。

VS2005/2008のプロジェクトファイル中のCOM参照を、ビルド環境に合わせて自動更新する

今回のネタは、以下の方にしか役に立ちません。かなり狭いですね。

  • Visual Studio 2005/2008上で ActiveX(COM)を使っている
  • 自作ないしは結構更新が頻繁な、ActiveX(COM)を使っている

何をしたいのか?

要するに、何をしたいのか?と言うと、以下を行いたいのです。


VS2005/2008上でActiveX(COM)参照を使うと、プロジェクトファイル(*.proj)中に COMReference タグが作られ、COM参照のパラメータが設定されます。
例えば、以下は私の環境で、Windows Media PlayerをCOM参照した結果です。

  <ItemGroup>
    <COMReference Include="AxWMPLib">
      <Guid>{6BF52A50-394A-11D3-B153-00C04F79FAA6}</Guid>
      <VersionMajor>1</VersionMajor>
      <VersionMinor>0</VersionMinor>
      <Lcid>0</Lcid>
      <WrapperTool>aximp</WrapperTool>
      <Isolated>False</Isolated>
    </COMReference>
    <COMReference Include="WMPLib">
      <Guid>{6BF52A50-394A-11D3-B153-00C04F79FAA7}</Guid>
      <VersionMajor>1</VersionMajor>
      <VersionMinor>0</VersionMinor>
      <Lcid>0</Lcid>
      <WrapperTool>tlbimp</WrapperTool>
      <Isolated>False</Isolated>
    </COMReference>
  </ItemGroup>


ビルド環境を変えたりActiveX(COM)をバージョンアップ更新すると、ビルドエラーが出て「COM参照を解決しろ」と文句を言われると思いますが、これは上記の「GUID(Guidタグの値)」の値が異なる為に、ActiveX(COM)が見つからなくなった為です。(もしかしたら、バージョン値もチェックしているかも)


ちなみに、このGUIDですが、プラットフォームSDKとかに含まれている OLEVIEW で確認する事が出来ます。(赤で記している箇所です)
01
01 posted by (C)wacky


GUID値の不一致が問題ならば、ビルド環境のActiveX(COM)のGUIDを、プロジェクトファイルの COMReference タグの Guid タグ値に反映させりゃいいじゃん?って事です。
今回は、そういう設定変更を半自動で行うスクリプト(wsf)を作った次第です。

プログラム本体

作ったスクリプトコードは、以下の通りです。
今回は、起動が軽めのWSHで作ってみました。はい。
ファイル名は、え〜、適当に com_chg.wsf 辺りでどうでしょう。

<job>
<runtime>
    <description>
VS2005/2008のプロジェクトファイルのCOM参照のGUIDとバージョン値の書き換えを行う
要するに、COMオブジェクトのGUID値を、ビルド環境に合わせる事で、参照の再設定を避けるのが目的。
    </description>
    <unnamed name="--proj [ファイル名]"  helpstring="変更対象となるVS2005/2008のプロジェクトファイル" />
    <unnamed name="--name [COM参照名]"   helpstring="変更対象となるCOM参照名。COMReferenceタグ、Include属性の値" />
    <unnamed name="--file [COMファイル]" helpstring="COMオブジェクトを持つファイルを指定。--progのどちらかを指定せよ" />
    <unnamed name="--prog [ProgID]"      helpstring="COMオブジェクトのProgIDを指定。--fileのどちらかを指定せよ" />
    <unnamed name="--eval [コード]"      helpstring="COMオブジェクトに対するフィルタを行う。infoオブジェクトに対する操作を行う事" />
</runtime>
<script>
//-----------------------------------------------
function P(msg)
{
    WScript.StdOut.WriteLine(msg);
}

function change_value(name, obj, prop, value)
{
    var ret = (obj[prop] == value) ? 0 : 1;
    var s = name + ": "+ obj[prop] + " -> ";
    obj[prop] = value;
    s += obj[prop];
    P("  " + s);
    
    return ret;
}

//-----------------------------------------------
// global変数宣言
var fso = new ActiveXObject("Scripting.FileSystemObject");

var xml = new ActiveXObject("MSXML2.DOMDocument");
xml.async = false;
var xml_file_name = null;
var include_name = null;

var TLI = WScript.CreateObject("TLI.TLIApplication");
var info = null;

// 引数解析
var argv = WScript.Arguments;
for(var i = 0; i < argv.length; i++){
    P(i + ":" + argv(i));
    switch(argv(i).toLowerCase()){
    case "--proj":  i++; P("project file:" + argv(i));      xml_file_name = argv(i); xml.load(xml_file_name); break;
    case "--name":  i++; P("xml include name:" + argv(i));  include_name = argv(i); break;
    case "--file":  i++; P("load COM file:" + argv(i));     info = TLI.TypeLibInfoFromFile(argv(i)); break;
    case "--prog":  i++; P("load COM object:" + argv(i));   info = TLI.InterfaceInfoFromObject(new ActiveXObject(argv(i))); break;
    case "--eval":  i++; P("eval is '" + argv(i) + "'");    info = eval(argv(i)); break;
    }
}

// 設定変更処理
P("COM object info:");
P("  " + info.Name + ":" + info.GUID);
P("  " + "Ver." + info.MajorVersion + "." + info.MinorVersion);

P("XML info:");
P(xml.url);
var node = xml.selectSingleNode("//COMReference[@Include='" + include_name + "']");
P(node.xml);

var is_chg = 0;
is_chg += change_value("Guid", node.selectSingleNode("Guid"), "text", info.GUID);
is_chg += change_value("MajorVersion", node.selectSingleNode("VersionMajor"), "text", info.MajorVersion);
is_chg += change_value("MinorVersion", node.selectSingleNode("VersionMinor"), "text", info.MinorVersion);
P("change:" + node.xml);
if(is_chg == 0){
    P("--- no change complete ---");
    WScript.Quit(0);
}

var new_file_name = xml_file_name;
var old_file_name = xml_file_name + ".old";

if(fso.FileExists(old_file_name) == true) throw old_file_name + "が既に存在する";
fso.MoveFile(xml_file_name, old_file_name);
xml.save(new_file_name);
P("xml save to:" + new_file_name);
P("--- complete ---");
</script>
</job>

使い方

大体、以下の感じにして使います。

1.先ほどのスクリプトを、プロジェクトのフォルダに置きます。

2.プロジェクトのプロパティ→ビルドイベントを選択します。(C#プロジェクトの場合。VBの場合は コンパイル→ビルドイベント です)
02
02 posted by (C)wacky

3.「ビルド前に実行するコマンドライン」に、更新する目標のCOM参照と設定値を入れます。
Windows Media PlayerをCOM参照にしているなら、以下のように記述します。

cd $(ProjectDir)
cscript com_chg.wsf --proj WindowsFormsApplication1.csproj --name WMPLib --prog WMPlayer.OCX --eval "info.Parent;"

4.ビルドします。
すると、ビルド前に 指定されたCOM参照のGUIDを比較し、不一致ならばプロジェクトを更新かけます。

技術的なちょっと話

ActiveX(COM)のGUID取得は、TypeLibInformation オブジェクトに含まれる、TLI.TLIApplication インタフェースを使っています。


欲しいよぅ、という人は↓以下からどうぞ。


GUID取得には、ProgID経由とファイル経由(Bindだろうなぁ)の二種類を用意しています。
ファイル経由の場合は、'--file'で指定します。自分が使っているActiveX(COM)の実体ファイルは何?という場合は、OLEVIEWでは 青でマーキングした所を見て下さい。
ProgID経由の場合は、'--prog'で指定します。自分が使っているActiveX(COM)のProgIDは何?という場合は、OLEVIEWでは、紫でマーキングした所を見て下さい。
なお、ProgIDの場合、そのままだと得られるGUIDはインタフェースの方で、ファイルの方じゃありません。なので、「--evel "info.Parent;"」を指定する事で、上位のGUID、即ちファイルのGUIDを得ています。

01
01 posted by (C)wacky


余談ですが、ここではCLSIDとか色々細かい用語の違いがある所、全てGUIDとしています。
確信的に言葉を誤用しちゃっています。正確には、ActiveXとかCOMとかOLEの技術書を読んでください。


プロジェクトファイルの操作は、MSXMLを使っています。まぁ、中身XMLですからね。
COM参照の探索は、XPathを使って求めています。いやぁ、便利ですね。

最後に

ここ最近、ActiveX(COM)を使ったプロジェクトを使っていまして、どのビルド環境でも一発でビルドしたいのだけど、COM参照が邪魔して 何度も同じ繰り返しを余儀なくされていたんですね。


こんな感じのスクリプトで問題が解決できたので、今後は少し楽が出来そうです。