咖啡日语论坛

 找回密码
 注~册
搜索
查看: 18082|回复: 7

Win32OLE 活用法

[复制链接]
发表于 2007-8-1 08:14:27 | 显示全部楼层 |阅读模式
【第 1 回】 Win32OLE ことはじめ

【第 2 回】 Excel

【第 3 回】 ADODB

【第 4 回】 Adobe Illustrator

【第 5 回】 Outlook

【第 6 回】 Web 自動巡回

【第 7 回】 ほかの言語での COM
回复

使用道具 举报

 楼主| 发表于 2007-8-1 08:15:16 | 显示全部楼层
【第 1 回】 Win32OLE ことはじめ


はじめに受信ボックスにたまった Excel ファイルを添付したメール。Excel の表にしたがって、DB から検索して、特定のセルに入力を繰り返す面倒な作業。気が滅入ります。
今まで、皆さんはこういう面倒な作業を効率化するためにどんなアプローチをしてきましたか? たとえば、MS Office には Visual Basic for Application (VBA) を使うことで自動的に処理させることができます。だけど、VB はあまりよく知らないし、使い慣れた Ruby で自動化していきたいって思ったことありますよね。
こんなときに活躍するのが Win32OLE です。この連載では Win32OLE という Ruby の拡張ライブラリを使ったプログラムについて紹介していきます。 Win32OLE という名前からわかるとおり、Microsoft Windows (以下、Windows) でしか動作しません。そのため、本連載は Windows を利用している Rubyist を対象としています。
Win32OLE は、COM とか ActiveX などと呼ばれたりする技術を扱うためのライブラリです。この記事の中では OLE/COM/ActiveX といった用語をあまり区別せずに COM という言葉でまとめておきます。Windows アプリケーションは一般にいくつかの部品を組み合わせてできています。この部品化する技術の中に Microsoft 社が開発した技術に Component Object Model (COM) があります。
COM には言語に非依存であるという特徴があります。つまり、COM で作られた部品であれば C++ でも VB でも Delphi でも、そして Ruby でもプログラムの中で扱うことができます。
こう言ってもピンとこないかもしれません。具体的に言うと、Ruby で書いたプログラムで Internet Explorer を制御したり、Excel 表の値を取得、変更したりすることが可能だということです。
COM を Ruby で扱えるようにする拡張ライブラリが Win32OLE です。Ruby 1.8 では標準添付されており、cygwin 版でもネイティブ win32 版の Ruby をインストールした時点で使用可能です。
この連載では、この Win32OLE の使い方および Win32OLE を Ruby で効果的に使うためのレシピについて紹介していきます。



今回の記事の目的と構成今回の記事の目的は、3つあります。
  • Win32OLE を体験してもらうこと
  • COM の概要について理解してもらうこと
  • COM のオブジェクト、メソッドの調べられるようになること
まず、Win32OLE の体験として、Internet Explorer を操作するスクリプトを紹介します。
次に、COM の概要について説明します。そして、今回の記事のメインである COM オブジェクトのインターフェースを調べる方法について説明します。具体的に言うと、オブジェクトの作成の仕方やメソッドの名前や引数を調べる方法です。このオブジェクトやメソッドというのは、Ruby の世界ではなく、COM の世界での意味のオブジェクトやメソッドのことを指しています。
これらの調べ方が分かれば自分が必要なオブジェクトを適切に選ぶことや、自分が使いたい機能を持つメソッドが何かを調べることができるようになります。
Win32OLE を使う具体的な方法などについては、インターネットなどを調べてもなかなか資料がなく、私自身使うときに苦労してきました。今回の内容を活用することで、Win32OLE を用いたプログラミングが楽になるでしょう。




Internet Explorer の制御を通じて Win32OLE を体験このスクリプトでは自動的に Google で ruby という単語で検索を行います。実際に実行してみてください。
ie.rb    1|require 'win32ole'   2|   3|ie = WIN32OLE.new('InternetExplorer.Application')   4|ie.Navigate("http://www.google.co.jp/")   5|ie.Visible = true   6|while ie.busy   7|  sleep 1   8|end   9|q = ie.document.all.Item("q")  10|q.Value = "ruby"  11|btnG = ie.document.all.Item("btnG")  12|btnG.click()
このスクリプトでは Internet Explorer の COM オブジェクトを作成しています。COM オブジェクトというのは、ざっくりと Windows アプリケーションの外部から利用できる形式の部品という理解をしておいてください。
実際にこのスクリプトが何をしているのかというと、Google のページに移動し、テキストボックスに 「ruby」 と入力し、「Google 検索」のボタンをクリックする、という動作を行っています。
COM オブジェクトを作成することで、Internet Explorer があたかも Ruby のオブジェクトであるかのように使えるわけです。このような形で、他に Word や Excel などの MS Office のアプリケーションも Win32OLE を用いることであたかも Ruby のオブジェクトを扱うようにコントロールすることができます。
上記のプログラムで重要なことは、2 つあります。
  • COM オブジェクトの作成
  • COM オブジェクトのメソッド呼び出し
1 つ目は Internet Explorer のオブジェクトを作成するとき 'InernetExplorer.Application' という文字列を指定しているということです。この文字列を ProgID といいます。つまり、COM オブジェクトは WIN32OLE::new(ProgID) の形式で作成します。この ProgID として指定する文字列を変えることで、 Internet Explorer のオブジェクトを作成するか、Excel のオブジェクトを作成するかなどを制御できます。
2 つ目は呼び出すべきメソッドさえ分かっていれば、そのコンポーネントの内部の詳細を知っている必要がないということです。ただ、メソッド名とその引数、返り値などが分かっていれば十分なのです。
残りの行は、Internet Explorer のオブジェクトが持つメソッドやプロパティを使っています。残りの行について詳しく解説することはもちろんできます。例えば、Navigate というメソッドに URL を引数として渡すことで、目的のページに自動的に移動できます。
しかし、この記事では、ここに深入りするのではなく、もっと大事なことを説明します。
自分が作成したい COM オブジェクトの ProgID の調べ方やそのオブジェクトが持つメソッドの一覧を得る方法、そのメソッドがどういう引数をとるを調べる方法などについて説明します。
この記事を読み終えて、いろいろ実験すると、さまざまなアプリケーションが COM のオブジェクトとして作成できるということにあなたは驚くはずです。そして、Ruby の Win32OLE のライブラリを活用することで、今までやってきた面倒な定型的な業務をとても簡単なプログラムで実現できるようになります。




COM とはCOM を使って Internet Explorer を実際に操作してみました。それでは COM とはどういう技術なのでしょうか。
COM をきちんと理解するにはいろいろと知らなければいけない知識があります。しかしながら、この記事では Ruby と Win32OLE を利用できるようになることが目的なので、難しい部分は省略して説明します。きちんと知りたい方は本稿末尾にある参考文献を手がかりに調べてみてください。
Ruby では、たとえば String クラスのインスタンスに何かメソッド (たとえば length) を適用することで、ある機能を実行することができます (length ならば、文字列の長さを取得することができる)。これを、Ruby で書くと、次のようになるでしょうか。
str = String.new("abc")str.length #=> 長さが返るこのとき、「あるクラス (String)」の「インスタンスを生成 (String.new("abc"))」し、「あるメッセージ (length)」をその生成したオブジェクトに投げる (str.length) ことで処理 (その文字列の長さを取得) をしていきます。
Ruby ではインスタンスを生成するのに「あるクラス」に対して「new」というメソッドを実行するわけですが、COM ではインスタンスを生成するときに「COM コンポーネント」を指定するための ProgID を引数として WIN32OLE.new メソッドを実行します。 COM コンポーネントとは、Windows に登録された、ある機能 (たとえば、IE や MS Office の制御機能) を提供するためのものです。この「COM コンポーネント」を利用して作成したインスタンス を「COM オブジェクト」とこの記事では呼んでいます。 COM オブジェクトを作成した後は、その COM オブジェクトで定義されている「メソッド」を実行することでそのコンポーネントが提供する機能を実現することができます。
COM の特徴として オブジェクト指向でサービスを提供しているということがあります。ご存知のとおり、Ruby はオブジェクト指向で設計されているので COM の技術と相性がいいと言えます。
そこで、Win32OLE の出番です。Win32OLE は、Ruby から、COM コンポーネントを利用することを目的とした拡張ライブラリです。Win32OLE を使うと、 COM コンポーネントからオブジェクトの作成、COM のメソッドの実行などを、 Ruby オブジェクトに対するそれと同様のインターフェースで行えます。
Windows では、多数の COM コンポーネントが標準で利用できます。また、標準以外の他のCOMコンポーネントをインストールして、標準のコンポーネントと同じように利用することもできます。たとえば、Internet Explorer は、標準で COM コンポーネントとして利用可能ですし、MS Office の各アプリケーションも COM コンポーネントとして機能を提供しています。
COM コンポーネントを使うためには、まずどんな COM コンポーネントが利用可能か、そしてその COM コンポーネントがどんな機能を提供しているのかを知らなければなりません。この情報は、ドキュメントとして公開していれば話が早いのですが、多くの COM コンポーネントの情報は公開されていません。
しかしながら、COM ではオブジェクトの名やメソッド名といったインターフェースなどを調べるための API が用意されています。Ruby でもこの機能を使うことで、インターフェースを調査できます。今回は COM コンポーネントを利用するために利用可能な調査方法について説明します。




COM オブジェクトの調べ方この章では COM オブジェクトを使う方法を調べるための方法について説明します。

Ruby だけを利用する調べ方まずは、Ruby のライブラリ Win32OLE を使うことで、COM のオブジェクトを生成する方法や、メソッド名や引数について調べる方法を説明します。
作成したい COM オブジェクトの探し方先ほど説明しましたように COM オブジェクトを生成するにはその COM コンポーネントに対応した ProgID が何かを調べる必要があります。
そこでまず、あなたのパソコンで利用可能な ProgID の一覧を表示させてみましょう。コマンドラインから次のコマンドを入力してみてください。
ruby -rwin32ole -e 'puts WIN32OLE_TYPE.progids'このコマンドを実行すると、ProgID の一覧、つまり、利用できる COM コンポーネントの一覧が得られます。実行するとインストールしているアプリケーションが多い方の場合は、数千行出力されると思います。参考までに筆者の環境では、4906 行出力されました。
あなたの使いたいコンポーネントの ProgID をこの中から見つけて WIN32OLE.new メソッドで呼び出せば、そのコンポーネントのオブジェクトを Ruby の世界で作成できます。
どのような ProgID を指定すればどのような COM オブジェクトを作成できるかは、次の表を参考にしてください。有名なオブジェクトを幾つか紹介しています。(MSDN: OLE プログラム ID などを参考にして作成)
作成するオブジェクトProgID
Microsoft Internet ExplorerInternetExplorer.Application
Microsoft ExcelExcel.Application
Microsoft AccessAccess.Application
ADODB ConnectionADODB.Connection
ADODB RecordsetADODB.Recordset
Microsoft OutlookOutlook.Application
Microsoft WordWord.Application|
Microsoft Word DocumentWord.Document
Windows Scripting HostWscript.Shell
先ほどのプログラムでは InternetExplorer.Application という ProgID を用いて COM オブジェクトを作成しています。
オブジェクトが持つメソッドの調べ方次にすることは、作成する COM オブジェクトがどのようなメソッドを持つかを調べることです。つまり、それが実際にどんなことができるかを確認します。
WIN32OLE#ole_methods メソッドを使うことで、オブジェクトが持つメソッドの一覧を得ることができます。
ruby -r win32ole -e "ie = WIN32OLE.new('InternetExplorer.Application');puts ie.ole_methods;"上記コマンドは、ProgID が InternetExplorer の COM コンポーネントから COM オブジェクトを生成し、その COM オブジェクトが持つメソッドの一覧を出力させています。この出力に先ほど使いました Navigate という文字列が含まれています。
この Navigate というメソッドが何を引数とするどのようなメソッドかを調べるには次のスクリプトを実行します。
method_param.rb    1|require 'win32ole'   2|   3|ie = WIN32OLE.new('InternetExplorer.Application')   4|puts  ie.ole_obj_help.to_s   5|method = ie.ole_method_help('Navigate')   6|puts method.return_type   7|method.params.each do |param|   8|  str = ""   9|  str.concat "[in] " if param.input?  10|  str.concat "[out] " if param.output?  11|  str.concat "[optional] " if param.optional?  12|  str.concat "[retval] " if param.retval?  13|  str.concat param.name  14|  str.concat " = #{param.default}" if param.default  15|  str.concat " As #{param.ole_type}"  16|  puts str  17|end
上記スクリプトの実行結果は次のとおりです。
IWebBrowser2VOID[in] URL As BSTR[in] [optional] Flags As VARIANT[in] [optional] TargetFrameName As VARIANT[in] [optional] PostData As VARIANT[in] [optional] Headers As VARIANT実行結果の 1 行目は COM タイプライブラリの説明文。 2 行目は、返り値の型。3 行目以降は、引数についての情報を第一引数から順に出力しています。
これから上記スクリプトとその実行結果について説明します。
ie = WIN32OLE.new('InternetExplorer.Application')この行では、先ほど説明したように、ProgID を引数とすることで Internet Explorer の COM オブジェクトを作成しています。
puts  ie.ole_obj_help.to_s#=> 出力結果IWebBrowser2という行は、指定した型のタイプライブラリの説明文を表示しています。タイプライブラリというのは、メソッド、定数、インタフェースなどを定義するものの総称です。これがあるから、このようなリフレクションを行い、メソッドやインタフェースなどを調べることができるわけですね。
puts method.return_type#=> 出力結果VOIDという行はこのメソッドの返り値が COM の型で VOID であることを示します。 VOID は C 言語 の void 型と同じで戻り値がないことを意味します。
method = ie.ole_method_help('Navigate')この行で使われている WIN32OLE#ole_method_help というメソッドはメソッド名を引数としてとり、WIN32OLE_METHOD 型を返します。
WIN32OLE_METHOD 型では、params メソッドが重要です。 WIN32OLE_METHOD#params は、WIN32OLE_PARAM 型の配列を返します。 WIN32OLE_PARAM 型を用いることで引数の詳細を知ることができます。
WIN32OLE_PARAM 型を逐次まわすループのところが上記スクリプトのハイライトです。
method.params.each do |param|  str = ""  str.concat "[in] " if param.input?  str.concat "[out] " if param.output?  str.concat "[optional] " if param.optional?  str.concat "[retval] " if param.retval?  str.concat param.name  str.concat " = #{param.default}" if param.default  str.concat " As #{param.ole_type}"  puts strendまず、WIN32OLE_PARAM#input? はその引数が入力用であるとき true、そうでないとき false を返します。同様に、WIN32OLE_PARAM#output? はその引数が出力用であるとき、 WIN32OLE_PARAM#optional? は省略可能なとき、 WIN32OLE_PARAM#retval? は返り値のときに true を返します。
WIN32OLE_PARAM#name は、その引数の名前を返します。いわゆる仮引数名と呼ばれるものです。これは実際に使う上での重要なヒントです。例えば上記の例でも第一引数は文字列で URL という名をつけられています。何を引数とすれば良いか想像できますよね?
WIN32OLE_PARAM#default は省略時のデフォルト値、WIN32OLE_PARAM#ole_type は引数の COM での型を示します。
BSTR というのは、COM での文字列型のことです。Win32OLE が Ruby の文字列との変換を勝手にしてくれます。なので、単に文字列型だと思っていただければいいです。
VARIANT というのは、数値、文字列、配列などさまざまなオブジェクトを表し得る型です。
COM での型がどのように Ruby での型に変換されるかは、次回に詳しく説明する予定です。

オブジェクトブラウザでの調べ方ここまでは、いちいち手で ruby を実行して、必要な ProgID を調べ、そして ProgID で作成できる COM オブジェクトの持つメソッドやその引数について調べる方法について紹介してきました。
こういう作業を GUI ベースで行える Simple OLE Browser という Ruby で書かれたツールもあります。助田さんのページで無料でダウンロードできます。 (Win32OLE 製作過程の雑記)
名前のとおりシンプルな作りです。しかしながら、上記 URL を確認してもらうと分かりますが、 DESCRIPTION というペインで簡単なメソッドの説明文を見ることもできたりするなど、結構便利です。詳しい使い方については上記 URL を参考にしてください。
この節で紹介する他のツールでもそうですが、Simple OLE Browser でもまずタイプライブラリの名前を指定します。そして、そのタイプライブラリで定義されているオブジェクトのメソッドの説明を見ることができます。
ProgID とタイプライブラリの名前との間の関係はレジストリを見れば分かります。詳しい説明については次のページを参考にしてください。([VB] ActiveX コンポーネントで作成されるレジストリ エントリ)
レジストリを見るのは面倒なので、私自身のやり方としては、あてずっぽでいくつかクリックして、対応関係を見つけています。この方法でもそれほど不便していません。
ところで COM のテクノロジーは、言語に非依存です。そのため他の言語においても、COM を利用することができます。それはつまり他の言語向けに開発された COM オブジェクトの詳細を閲覧するツールも利用できるということです。
たとえば海外では非常に人気があるスクリプト言語である Python では、 "Python Object Browser" というある COM の型が持つメソッドの引数などを簡単に調べるためのツールがあります。 PythonWinをダウンロードしたら、Tools から選択して起動できます。ツリー形式で見れて便利です。無料で手に入ります。
とさまざまなブラウザがありますが、真打はやはり Microsoft が提供しているオブジェクトブラウザでしょう。Visual Basic Editor に含まれるこのツールを使うと、クラスが持つメソッドやプロパティの一覧やメソッドの使い方を見ることができます。オブジェクトブラウザは、MS Office の Visual Basic for Applications のエディタをまず起動して、[F2] キーを押すことで起動できます。これは、無料では手に入りませんが使いやすくて便利です。
プロパティがどういう型かなどがよく分かる点や、「戻る」ボタンがあることなどが便利で私はよく利用します。
オブジェクトブラウザに関する説明は検索すれば解説がいくつか見つかると思います。例えば、MSDN には次のページがあります。(オブジェクト ブラウザ)
他に、Microsoft が提供する無料で使えるツールとして、Microsoft の Resource Kit の Free Tool の中にある OLE View というツールもこの目的に使えます。 (Oleview.exe: OLE/COM Object Viewer)

その他の調べ方WEB で検索して、COM オブジェクトのインターフェースについて調べることもできます。
適当なメソッド名や ProgID を使って Google で検索すれば、詳しいドキュメントが得られることもあります。
ProgID が分からないときは、CreateObject という単語を Google で検索すると Visual Basic や VB Script でのサンプルコードや説明を検索できるというテクニックもあります。使える小技です。
Visual Basic で MS Office アプリケーションを自動化する話はよくありますからそういうコードを参考にするというのも非常に良い方法です。例えば、 Microsoft Office アプリケーションのオートメーションに関してドキュメントが次のページにあります。([HOWTO] Office オブジェクト モデルに関するドキュメントの検索および使用方法)
このページにも紹介されている方法ですが、Excel のようなマクロ機能がある MS Office のアプリケーションでは、行いたい動作を一旦マクロとして記録してみるという方法があります。生成された Visual Basic のコードを読めば何をするときにどのようなプロパティ・メソッドを使うかの参考になると思います。
他に、MSDN や MS Office の Visual Basic に付属のヘルプはとても参考になります。MS Office のアプリケーションはドキュメントが充実しています。見る価値はあります。
繰り返しますが、COM は言語に依存しないため、Visual Basic が呼び出しに使用しているメソッド名や呼び出し規則はRuby と同じようになります。もちろん文法に起因する違いはありますがオブジェクトがもつメソッドなどを調べるには十分な情報が得られます。このような点で Visual Basic での書き方は参考になりますので、一度見てみるようにした方がいいでしょう。
あ。一つ、言っておかないといけないことがあります。それは、COM において、メソッド名やプロパティは大文字と小文字を区別しないということです。自分の好きな方を使っていただいて結構です。Visual Basic のコードで大文字始まりのメソッド名であってもRuby では小文字始まりでかまいません。もちろん大文字でも大丈夫です。

私の調べ方以上、自分が求めるクラス、メソッドをどうやって見つけるかについて説明しました。
最後に私が実際にどうやって新しいクラスの使い方を調べる方法について少しだけ書きます。ひょっとしたらこの節がこの記事で参考になるところかもしれません。
まずは MS Office 付属の Visual Basic Editor のオブジェクトブラウザで勘で使えそうなタイプライブラリを参照設定してみて、クラス名やメソッド名を眺めてみることから始めます。このようにして、だいたいやりたいことができるメソッドがあるかどうかを調べます。
メソッド名や、クラス名以上の情報が必要な場合は、Google で検索したり、 MSDN での情報も参考にします。MS Office に付属の VBA のヘルプも参考にします。
それでも情報が足りない場合は、使いたいメソッドを実際に使用するような小さなスクリプトを書いてみたりします。スクリプトを書く目的は、短い時間でそのオブジェクトが自分が必要としている機能を実際に持っているのかどうかを調査することです。
あなたがもしもアジャイルなプログラマであれば、これをスパイクと呼んだりするのもいいもしれません。スパイク中にメソッドの使い方を調べるときは、 WIN32OLE#ole_method_help などのメソッドを使います。実際にそのオブジェクトを作成するスクリプトを書いている途中にそのオブジェクトの使い方を調べる場合は、Ruby に閉じて調べられるWin32OLE のメソッドは便利です。オブジェクトブラウザも平行して使いながら、自分が求める機能を実現するにはどうすればよいのかを探っていきます。そして、だいたい目的が達成できそうだと分かるとプログラムを書き上げていきます。




まとめさて、今回は、Win32OLE を使った簡単なアプリケーションを紹介し、そして自分が欲しいオブジェクトを得るためにはどうするかということを解説しました。
次回以降では実際に Excel 等のオブジェクトを得て、それらを具体的に活用していく方法について紹介します。
(アドバイザー:arton、助田 雅紀)




今回の理解度チェック今回の記事に出てきた Internet Explorer のサンプルファイルに出てきたメソッドの引数などについて調べてみましょう。
ヒント:オブジェクトブラウザを使って調べる場合は、対応するタイプライブラリを知る必要があります。 [InternetExplorer.Application] という ProgID は、 [Microsoft Internet Controls] というタイプライブラリに対応しています。 ie.document のメソッドについて調べる場合は [Microsoft HTML Object Library] というタイプライブラリを調べるとよいでしょう。 Ruby だけで調べる場合は、対応するタイプライブラリが分からなくても調べられます。




参考文献Windows での Ruby プログラミングについて知りたい方へ
COM について詳しくなりたい方へ




著者についてcuzic は、通信系の会社に勤める会社員です。世界史、経済学、健康、J-POP などに興味があります。著者への連絡先は cuzic atmark cuzic dot com です。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-8-1 08:18:16 | 显示全部楼层
【第 2 回】 Excel



プロローグあなたは、メールボックスに溜まった Excel ファイル添付付きのメールを見ていて、長い間うんざりとした気分を味わってきました。
「どうして、こんな単純な作業を俺がやらないといけないんだっ。」
「これくらい機械でもできるはずだっ」
あなたは、Excel ファイルの解析を自動的にできるようにするスクリプトを作成する決意を固めました。
そして、思いました。
「いったい、どうすれば楽に Excel ファイルを扱えるんだろう?」




はじめにExcel を使ってのうんざりするような作業っていろいろありますよね。
Excel でデータを渡されて、それを使って行う一連の作業があるような場合とか。他に、何がしかのログとかデータが別にあって、それを整形したり解析した結果を、お客さまや上司の指定で Excel で出力する必要がある場合もあります。
私たちが今回学ぶ内容は、このような業務で適用できる事柄になります。私たちは、今回、Excel ファイルに含まれるデータを Ruby から扱う方法と Ruby で Excel ファイルを作成する方法について学びます。




今回の目的これから私たちは次の内容について学びます。
  • Excel のオブジェクトモデルの概略
  • Excel ファイルのパース
  • Excel ファイルの自動作成
  • Excel を操作するのによく使うクラスやモジュールのリスト
Excel は、前回に説明した COM という技術を用いて、外部のプログラムから利用できる形で、部品となる COM オブジェクトを提供しています。
Ruby から Excel を操作するにはこれらのオブジェクトとその関連を知らなければいけません。したがって、最初に Excel のオブジェクトモデルについて簡単に説明します。
その次に、今回の記事のメインである Excel ファイルをパースする方法について説明します。ここでは、パースする方法をいくつかに分類して、それぞれどういう状況で適用できるのかについて説明します。具体的にパースを行うスクリプトを紹介しながら、既存の Excel のファイルを、 Ruby で扱えるように読み取っていくときのコツを共有していきたいと思います。
Excel ファイルの自動作成については、 Excel の特定のセルを変更していく処理について簡単に説明します。




Excel のオブジェクトモデルExcel のオブジェクトの中で重要なものは4つあります。
  • Application オブジェクト
  • Workbook オブジェクト
  • Worksheet オブジェクト
  • Range オブジェクト
Application オブジェクトは Excel のアプリケーション自身を表現します。実行しているアプリケーションに関する情報を提供します。
Workbook オブジェクトは Excel アプリケーションで開かれている特定のブックを表現します。
Worksheet オブジェクトは、ブックの中の一枚のシートを表します。
Range オブジェクトは Excel を利用するときに最も頻繁に利用するオブジェクトです。 Range オブジェクトはセル、行、列、1つ以上のセルのブロックを含む範囲、さらには複数のセルにまたがるグループを表現します。目的の範囲を Range オブジェクトとして取得することでその値や、境界線、書式などを取得、変更できます。
Application オブジェクトは、Excel ブックを表現する Workbook オブジェクトを1個以上所有します。そして、Workbook オブジェクトは 1個以上の Worksheet オブジェクトを所有します。そして、Worksheet オブジェクトのの中にある任意の範囲は、 Range オブジェクトを使用して表現されることになります。
この所有関係が Excel のオブジェクトモデルの基本になります。
正確には Chart というグラフを表現するオブジェクトも含めて考える必要があるので、この理解は正確ではありません。しかしながらワークシートのみを扱う場合はこれで充分です。
Excel を操作するときはこの関係を念頭におくと分かりやすくなります。




Excel ファイルのパースここまでで、Excel のオブジェクトモデルの概略を理解できました。
これから私たちは、Excel ファイルを実際に扱っていく方法について学んでいきます。
この章では特に、 既存の Excel ファイルを Ruby でパースする方法について学びます。

Excel ファイルの全データを出力この節では、Excel ファイルに含まれる全シートの全セルの値を出力するスクリプトについて説明します。
私たちは次のことについて学びます。
  • ファイルの絶対パスの取得方法
  • Excel ファイルの開き方
  • 全ワークシートに対して処理を行う方法
  • 全行、全列に対して処理を行う方法
  • ブックの閉じ方、始め処理、終わり処理のイディオム
  • セルの値の取得の仕方
  • Excel のセルの型と Ruby の型の関係
それでは、まずはスクリプトを見てみましょう。このプログラムを実行するのに必要なファイルはもう少し後に出てきますので、実行するのはちょっと待ってください。
excel1.rb   1| require 'win32ole'  2|   3| def getAbsolutePath filename  4|   fso = WIN32OLE.new('Scripting.FileSystemObject')  5|   return fso.GetAbsolutePathName(filename)  6| end  7| filename = getAbsolutePath("sample1.xls")  8|   9| xl = WIN32OLE.new('Excel.Application') 10|  11| book = xl.Workbooks.Open(filename) 12| begin 13|   book.Worksheets.each do |sheet| 14|     sheet.UsedRange.Rows.each do |row| 15|       record = [] 16|       row.Columns.each do |cell| 17|         record << cell.Value 18|       end 19|       puts record.join(",") 20|     end 21|   end 22| ensure 23|   book.Close 24|   xl.Quit 25| end
このスクリプトでは、Excel 表のデータを Ruby で扱えるように読み出しています。これを本稿では「パースする」と言います。
これだけのスクリプトでも説明すべきことはいろいろとあります。
本稿の最後によく使うクラスとメソッドの簡単な説明も書きますので、そちらも参照してください。
def getAbsolutePath filename  fso = WIN32OLE.new('Scripting.FileSystemObject')  return fso.GetAbsolutePathName(filename)endfilename = getAbsolutePath("sample1.xls")順番が前後しますが、後に使う Workbooks オブジェクトの Open メソッドでは Excel ファイルの Windows での絶対パスを渡す必要があります。
Win32 ネイティブ版の Ruby であれば簡単に File::expand_path メソッドを用いて絶対パスを取得できます。しかしながら、Cygwin 版であればこれで得られるのは UNIX 形式の絶対パスとなります。
ここでは Win32 ネイティブ版、Cygwin版の違いに関わらず絶対パスを取得する方法として、Scripting.FileSystemObject という COM コンポーネントを使っています。 Scripting.FileSystemObject の GetAbsolutePathName メソッドを使うことでファイルの絶対パスを取得することができます。
このようにすることで、カレントディレクトリにある Excel ファイルの絶対パスを、Win32 ネイティブ版、Cygwin 版に関わりなく取得できます。
そして、ここで得た絶対パスは変数 filename に代入しています。
xl = WIN32OLE.new('Excel.Application')という行で Excel.Application の COM オブジェクトを作成しています。先ほど説明した Application オブジェクトが xl になります。
book = xl.Workbooks.Open(filename)という行で、先ほど得た filename の Excel ファイルを開きます。
Application オブジェクト xl の Workbooks は読み取り専用のプロパティです。プロパティというのは、COM の用語の1つです。構文上はパブリック変数のように扱えるメソッドの一種のことをプロパティと呼びます。 Ruby でいうと、読み取り専用のプロパティというのは attr_reader で作るメソッド、読み取りも書き込みも可能なプロパティというのは attr_accessor で作るメソッドに対応します。
Workbooks オブジェクトが持つ Open メソッドで、既存の Excel ファイルを開くことができます。このとき、開いた Workbook をオブジェクトを返します。なお、新規に Excel ファイルを作成する場合は Workbooks オブジェクトの Add メソッドを用います。
book.Worksheets.each do |sheet|  ...endと書くことで、開いたブックのすべての Worksheet に対して同じように処理させることができます。
COM オブジェクトには名前付け規則があり、「~s」というプロパティは「s」をとった COM オブジェクトのコレクションオブジェクトとなることが多いです。ここではコレクションオブジェクトというのは、簡単に Win32OLE で each メソッドが使えるようなオブジェクトという理解をしていてください。ここでは、ブロック引数として渡されている sheet は、 Worksheet オブジェクトになります。
  sheet.UsedRange.Rows.each do |row|    record = []    row.Columns.each do |cell|      record << cell.Value    end    puts record.join(",")  endここが出力を行う箇所になります。 Worksheet の UsedRange というプロパティは、そのワークシートで使われているセルの範囲を返します。これは Range オブジェクトになります。
先ほども説明したように Range オブジェクトは Excel の操作に関して最も頻繁に用いる COM オブジェクトです。
このブロックでは Range オブジェクトの Rows プロパティ、 Columns プロパティ、Value プロパティを用いています。
  sheet.UsedRange.Rows.each do |row|    ...  endと書くことで、ワークシートの使用している範囲を一行ごとに取り出して順に処理させることができます。ここで、 row は一行分の範囲を表現する Range オブジェクトとなります。
    row.Columns.each do |cell|      record << cell.Value    endRange オブジェクトの Rows プロパティがその Range オブジェクトが表現する範囲の一行ずつに対応したのに対して、Columns プロパティは一列分のセルに対応します。
ここで、一行分の範囲の一列分というのは、結局セル1つに対応します。
そして、そのセルの値を取得するために Value プロパティを利用しています。
record が一行分の一連のデータを格納する配列で、そこに順にデータを追加しています。そして、その後 Array#join メソッドを用いてカンマ区切りで文字列の連結を行っています。
  xl.Workbooks.Close最後に Workbooks オブジェクトの Close メソッドで開いているすべてのブックを閉じています。そのブックだけを閉じたい場合は、
book.Closeと実行しましょう。
xl.Quitさらに Application オブジェクトの Quit メソッドを実行すると Excel アプリケーションそのものを終了することができます。
Workbooks オブジェクトの Close メソッドと Application オブジェクトの Quit メソッド終了時に呼び出すのは良い習慣です。
前回紹介しました Internet Explorer を起動するスクリプトのようにアプリケーションを起動することが目的の場合は、起動したアプリケーションを終了する必要はありません。しかしながら今回紹介するスクリプトのような場合は、 Excel を起動した状態でスクリプトを終了するのは望ましい動作ではありません。 Application オブジェクトの Quit メソッドを起動して、 Excel アプリケーションを終了するようにしましょう。
終わり処理(後始末)を書かないようなスクリプトを実行した場合はタスクマネージャを起動すると、Excel がプロセスに残っていることを確認できるでしょう。正しく終わり処理を書けばこのような問題は起こりません。
必ず終わり処理を行わせるためには、このスクリプトのように
... # 始め処理begin  ... # 行いたい処理ensure  ... # 終わり処理endと書くことがよくあります。
これは、始め処理に対応する終わり処理がある場合によく使われるイディオムです。また、途中で例外が発生した場合でも、終わり処理が必ず実行されるため、安心できます。
説明が延々と長くなってしまいまして、すいません。話を戻します。
ここで、先ほどのスクリプトをサンプルのエクセルファイルに対して実行してみましょう。
ここでは、次の Excel ファイルをサンプルとして利用します。
sample1.xls
20世紀の大きな地震の履歴を書いた Excel ファイルです。このスクリプトを実行するときは、添付する Excel ファイルをカレントディレクトリに置いてください。
すると、以下の結果が得られます。
1923/09/01 00:00:00,関東大震災,7.9,142807.0,true1994/10/04 00:00:00,北海道東方沖地震,8.1,0.0,false1995/01/17 00:00:00,阪神淡路大震災,7.2,6418.0,true2004/10/23 00:00:00,新潟県中越地震,6.8,37.0,trueあれれ。少し変ですね。そのまま考えれば、次のように出力されるような気がします。
1923/09/01,関東大震災,7.9,142807,TRUE1994/10/04,北海道東方沖地震,8.1,0,FALSE1995/01/17,阪神淡路大震災,7.2,6418,TRUE2004/10/23,新潟県中越地震,6.8,37,TRUEたとえば、Excel 表では、発生日のところ、「1923/09/01」と入力されているところ、出力されたデータは「1923/09/01 00:00:00」となっています。他にも 「死者・不明者」のところが Excel では整数のところが、「14287.0」のように最後に小数点付きで出力されています。
どうして、このような結果になったのでしょう?
実は、セルには内部的に表現されている型があります。もう少し説明すると、セルの値は Variant という COM の型で表現されており、 Variant 型の中でもそれがどのような Variant 型かという区別があるのです。
Win32OLE はこの型を適切に Ruby の型へと変換します。
具体的には Excel での型は次のように Ruby の型に変換されます。
Excelでの型Rubyの型
文字列String
数値Float
日付YYYY/mm/dd HH:MM:SS形式の String
Booleantrue もしくは false
日付型が YYYY/mm/dd 形式の文字列となる理由は、Ruby の Time クラスで表現できる値と、Excel で表現できる日付型の範囲が異なっているからです。 Ruby の Time クラスは、1901年12月14日から2038年1月19日までしか表現できません。それと違って、Excel での日付型は 100年1月1日から 9999年1月1日となっています。
このような問題があるため、日付型は Ruby の文字列型として変換する形になっています。この文字列を Ruby の Time クラスに変換するのはプログラマの責任になっています。
この文字列を Ruby の Time クラスに変換することを考慮して、さきほどのスクリプトを書き換えてみましょう。
そうするには、 record という配列に追加しているところを次のように変更します。
     if cell.Value.is_a?(String) &&        cell.Value =~ %r(\d\d\d\d/\d\d/\d\d \d\d:\d\d:\d\d)        begin          record << Time.mktime(*cell.Value.split(%r([:/])))        rescue ArgumentError => e          STDERR.puts e.inspect        end     else       record << cell.Value     end%r() は知っていると思いますが、正規表現リテラルを作るための別記法です。 Time.mktime メソッドは、
Time::mktime( year, mon, day, hour, min, sec)の形式の引数をとるメソッドです。引数で指定された時刻を表現する Time オブジェクトを返します。*array の表現は、最後の引数の直前に * がついている場合は、その最後の引数の値が展開されて、引数として渡されるという Ruby の文法上のルールを利用しています。
なお、Time::mktime メソッドは Ruby の Time 型に変換できない場合に ArgumentError の例外を raise します。実際にプログラムを書くときはこの例外を捕捉するように書きましょう。

Excel ファイルのタイプごとのパース与えられた Excel ファイルのセルの内容をすべて出力する方法はこれで分かりました。
単純な形式の Excel のファイルをパースする場合なら、これでも充分かもしれません。
しかしながら、Excel ファイルの中にあるデータを取り出せるようになったとして、データが何についてのデータなのかはどうやって分かるのでしょう?
Excel ファイルに含まれる値を取り出すことができたとしてもそれが何の値かが分からなければ意味がありません。
日常的に業務で流れてくる Excel ファイルというのは、なかなかそう単純な形式ではないものですよね。人間が目で見れば分かるけれども、コンピュータで処理しにくいような形式であったりします。
本稿では、あるデータがどういうデータなのかを調べる方法として、次の3つの方法について紹介します。
  • カラムが決まればどのような値かが決まる場合
  • 同一カラムの上の方に、見出しがあり、見出しの文字列からどのような値かが決まる場合
  • 境界線から、見出しの位置が導かれ、そのセルがどのような値かが決まる場合
これから、これらの場合について順に学んでいきます。
カラムが決まればどのような値かが決まる場合きれいなマトリックスで書かれているような Excel ファイルは比較的簡単にパースできます。そうでなくても、列が決まれば、そのセルが何についての情報が格納されているかが決まるような場合には、それに基づいてスクリプトを書けば Excel ファイルをパースしていけます。
この場合は、簡単にパースするスクリプトを書くことができます。
この節では次の項目について説明します。
  • Worksheet の中で特定の行、列の値を取得する方法
  • module および Object#extendの利用
  • 複数引数の [] メソッドの定義
  • アクティブなオブジェクトの簡単な参照方法
次のスクリプトは、さきほどのサンプルの Excel ファイルをこのパターンでパースしていきます。
excel2.rb   1| require 'win32ole'  2|   3| module Worksheet  4|   def [] y,x  5|     self.Cells.Item(y,x).Value  6|   end  7| end  8|   9| def getAbsolutePath filename 10|   fso = WIN32OLE.new('Scripting.FileSystemObject') 11|   return fso.GetAbsolutePathName(filename) 12| end 13| filename = getAbsolutePath("sample1.xls") 14|  15| xl = WIN32OLE.new('Excel.Application') 16|  17| begin 18|   xl.Workbooks.Open(filename) 19|   sheet = xl.Worksheets.Item("Sheet1") 20|   sheet.extend Worksheet 21|    22|   recordset = [] 23|   2.upto(5) do |y| 24|     record = {} 25|     1.upto(5) do |x| 26|       v = sheet[y,x] 27|       title = sheet[1,x] 28|       record[title] = v 29|     end 30|     recordset << record 31|   end 32|  33|   recordset.each do |record| 34|     puts record.map{|title,value|  35|       "#{title}=#{value}" 36|     }.join(",") 37|   end 38| ensure 39|   xl.Workbooks.Close 40|   xl.Quit 41| end
上記スクリプトは、まず Worksheet モジュールをあらかじめ定義しています。
module Worksheet  def [] y,x    self.Cells.Item(y,x).Value  endendWorksheet オブジェクトの sheet.Cells.Item(y,x).Value という一連の呼び出しで、y 番目の行で x 番目の列に対応するセルの値を取得できます。最初の行は、1番目で、1から順に数え上げます。なお、あまり知られていないかもしれませんが Ruby では [] メソッドに対して、複数の引数を指定できます。
sheet = book.Worksheets.Item("Sheet1")という行は、Work オブジェクトのプロパティメソッド Worksheets を用いて、 Worksheets コレクションを取得しています。そして、 "Sheet1" に対応する Worksheet オブジェクトを取得しています。
なお、参考までにここでは代わりに
sheet = xl.Worksheets.Item("Sheet1")と書くこともできます。試してみてください。
Application オブジェクトの Worksheets プロパティを使うとアクティブな Workbook のオブジェクトの Worksheets プロパティと同じオブジェクトを返します。こういうアクティブなオブジェクトのプロパティを操作できるようにするショートカットは、Office アプリケーションオブジェクトモデルではしばしば提供されます。
Workbooks オブジェクトの Open メソッドを用いると既存のブックを開き、その Workbook オブジェクトを返します。このとき、その Workbook オブジェクトを ActiveWorkbook となるため、xl.Worksheets と書いてもうまく動作するわけです。これについての詳細は、 エクセレントな Office の冒険 を参照してください。
話を戻します。一般にコレクションでは、 Item メソッドで特定のオブジェクトを取得できます。ここでは、 Worksheets がコレクションで、Sheet1 という名前のワークシートを book.Worksheets.Item("Sheet1") で取得しています。
このスクリプトでは、明示的に範囲指定を行っています。 2.upto(5) や 1.upto(5) などといった箇所です。このやり方は、自分で制御しやすい書き方です。先ほど紹介しました UsedRange を用いる方法とどちらが良いかは状況によって異なります。明示的に範囲指定を行うことが容易である場合はこの方法を使用すると良いでしょう。
このスクリプトは 1 行目に見出しがあることを前提としています。
    title = sheet[1,x]そして、その見出しを title に代入しています。このように ある行に見出しがあることを前提とできることは多いでしょう。
これをキーとして、ハッシュへと代入させています。今回はたまたまある行に見出しがあることを前提として title を取得させていますが、そうではない場合もあります。このときは、行番号と見出しとの対応表をあらかじめ Ruby のソースコード中に持たせて、見出しに対応する行を探す方法もあります。その方法について簡単に次の節で紹介します。
これからパースしようとする Excel 表を見ながら工夫しつつ、アプローチを考えていきましょう。
出力のところでは、Enumerable#map や Enumerable#join を使って、簡潔に書いています。
   recordset.each do |record|     puts record.map{|title,value|        "#{title}=#{value}"     }.join(",")   endHash#map は、Enumerable モジュールで定義されている map メソッドを呼び出します。 Enumerable#map メソッドは each と同じ引数をとり、要素に対してブロックを評価した結果を配列で返します。そして、Array#join メソッドによって、Hash の内容を "," でつなげています。
このような配列演算は最初は慣れないかもしれませんが、簡潔に書けるという利点があります。私は好きです。
見出しを使ったパース何列目のセルかということで、何のデータが含まれているかを決定できれば先ほどのやり方でよいですが、そう簡単な Excel ファイルばかりではありません。
例えば、1枚のワークシートに複数のテーブルがあって、同一の列が複数の使われ方をしている場合もあります。1枚のワークシートに複数の同じ形式の表があって、それぞれに対して、何列目かを列挙することが面倒な場合もあるでしょう。
このような場合は、たとえばあるセルから、同一カラムで何行か上のセルに見出しがあるので、この見出しの文字列をパースするときに利用したいということもあるでしょう。
この場合は、見出しの文字列をあらかじめ配列に入れておき、そのセルを起点に何行か上にマッチする見出しがあるかどうかを探索することで、パースするというやり方があります。
同じように先ほどの sample1.xls を使えば次のようなスクリプトになります。
excel3.rb   1| require 'win32ole'  2|   3| module Worksheet  4|   def [] y,x  5|     self.Cells.Item(y,x).Value  6|   end  7|   8|   def getTitle(y,x,titles)  9|     while y > 0 10|       v = self[y,x] 11|       if titles.include?(v) 12|         return v 13|       end 14|       y -= 1 15|     end 16|     return nil 17|   end 18| end 19|  20| def getAbsolutePath filename 21|   fso = WIN32OLE.new('Scripting.FileSystemObject') 22|   return fso.GetAbsolutePathName(filename) 23| end 24|  25| def openExcelWorkbook filename 26|   filename = getAbsolutePath(filename) 27|  28|   xl = WIN32OLE.new('Excel.Application') 29|   book = xl.Workbooks.Open(filename) 30|   begin 31|     yield book 32|   ensure 33|     xl.Workbooks.Close 34|     xl.Quit 35|   end 36| end 37|  38| openExcelWorkbook("sample1.xls") do |book| 39|   sheet = book.Worksheets.Item("Sheet1") 40|   sheet.extend Worksheet 41|    42|   recordset = [] 43|    44|   titles = ["発生日","名称","マグニチュード","死者・不明者","死者の有無"] 45|   2.upto(5) do |y| 46|     record = {} 47|     1.upto(5) do |x| 48|       v = sheet[y,x] 49|       title = sheet.getTitle(y,x,titles) 50|       record[title] = v     if title 51|     end 52|     recordset << record 53|   end 54|  55|   recordset.each do |record| 56|     puts record.map{|title,value|  57|       "#{title}=#{value}" 58|     }.join(",") 59|   end 60| end
このスクリプトの重要な部分は titles への代入と getTitle メソッドです。そして、始め処理終わり処理のイディオムを yield を使うメソッドで実現する方法も学びます。
  titles = ["発生日","名称","マグニチュード","死者・不明者","死者の有無"]で、見出しの配列をあらかじめ、 titles に代入しています。
そして Worksheet モジュールで定義された getTitle というメソッドを使って、そのセルが何に使われているかを特定しています。
  def getTitle(x,y,titles)    while y > 0      v = self[y,x]      if titles.include?(v)        return v      end      y -= 1    end    return nil  endgetTitle というメソッドでは、そのセルの位置から順に上へと見出しとなりうる値があるのかどうかを調べています。これは、今回単純に1つずつ順に上へと調べているので、行数が増えるとどんどん時間がかかることになっています。工夫の余地があります。
あなたの手元の Excel ファイルを見ながら、改良して使ってください。
このスクリプトではもう1つ新しい要素があります。始め処理終わり処理のイディオムを yield を使ったメソッドとして抜き出しているという点です。このスクリプトでは今までそのまま書き下ろしていた、ワークブックを開き、終了する動作を別のメソッドで定義しています。
def openExcelWorkbook filename   filename = getAbsolutePath(filename)    xl = WIN32OLE.new('Excel.Application')   book = xl.Workbooks.Open(filename)   begin     yield book   ensure     xl.Workbooks.Close     xl.Quit   end endこのようにすることで、始め処理と終わり処理を近い位置に書くことができます。始め処理と終わり処理は互いに関連しています。その2つが近い位置にあると見通しが良い分かりやすくスクリプトになります。
始め処理を行ったのちに必ずある終わり処理を行わなければいけない状況というのは、多いものです。このテクニックは応用範囲が広く覚える価値があります。
境界線を元にセルの情報を取得境界線がテーブルの情報を取得するのに、非常に大事な場合もあります。
きれいな Excel ファイルであれば上記の方法でパースできると思います。しかし場合によっては、Excel 表の境界線の罫線の種別が何かということをヒントにしてスクリプトを書くと、楽に書ける場合があります。
このような場合は、えてして非常に非定型な形式となります。そのため、具体的な場合については説明することはここではしません。
そこで、この節では一般的な境界線の種別を得る方法を中心に学んでいきます。この節で学ぶ内容は次のとおりです。
  • A1,B2 といった形式でセルの Range オブジェクトを取得する方法
  • COM コンポーネントで定義された定数をロードする方法
  • Win32OLE での COM コンポーネントの定数の命名規則
  • モジュールに定義された定数を取得する方法
  • ||= という初期化時のイディオム
  • %w() リテラルによる文字列配列の生成
このスクリプトは、あるセルを囲む境界線の線種や線の太さに対応する定数名を返します。
excel4.rb   1| require 'win32ole'  2|   3| module Border  4|   def linetype  5|     @@linestyles ||= nil  6|     if @@linestyles.nil?  7|       @@linestyles = {}  8|       %w(XlContinuous XlDash XlDashDot   9|          XlDashDotDot XlDot XlDouble  10|          XlLineStyleNone XlSlantDashDot).each do |linestyle| 11|         v = Border.const_get(linestyle) 12|         @@linestyles[v] = linestyle 13|       end 14|     end 15|     return @@linestyles.fetch(self.LineStyle) {|key| key} 16|   end 17|  18|   def lineweight 19|     @@lineweights ||= nil 20|     if @@lineweights.nil? 21|       @@linewights = {} 22|       %w(XlHairline XlMedium 23|          XlThick XlThin).each do |weight| 24|         v = Border.const_get(weight) 25|         @@linewights[v] = weight 26|       end 27|     end 28|     return @@linewights.fetch(self.Weight) {|key| key } 29|   end 30| end 31|  32| def getAbsolutePath filename 33|   fso = WIN32OLE.new('Scripting.FileSystemObject') 34|   return fso.GetAbsolutePathName(filename) 35| end 36|  37| def openExcelWorkbook filename 38|   filename = getAbsolutePath(filename) 39|  40|   xl = WIN32OLE.new('Excel.Application') 41|   book = xl.Workbooks.Open(filename) 42|   begin 43|     yield book 44|   ensure 45|     xl.Workbooks.Close 46|     xl.Quit 47|   end 48| end 49|  50| openExcelWorkbook("sample2.xls") do |book| 51|   sheet = book.Worksheets.Item("Sheet1") 52|   cell = sheet.Range("B2") 53|  54|   borders = cell.Borders  55|   WIN32OLE.const_load(borders,Border) 56|    57|   [["上" , Border::XlEdgeTop], 58|    ["右" , Border::XlEdgeRight], 59|    ["下" , Border::XlEdgeBottom], 60|    ["左" , Border::XlEdgeLeft], 61|   ].each do |direction,index| 62|     border = borders.Item(index) 63|     border.extend Border 64|     puts "#{direction} #{border.linetype} #{border.lineweight}" 65|   end 66| end
このスクリプトでは Worksheet オブジェクトの Range メソッドでセルの値を取得しています。
cell = sheet.Range("B2")この B2 といった記法は Excel ファイルの扱いに慣れた方であればすぐに分かるでしょう。2列目2行目のセルを表現する Range オブジェクトを取得しています。
境界線について調べるには Border オブジェクトを使用します。 Border オブジェクトを取得するには、Range オブジェクトの Borders プロパティによって取得できるコレクションオブジェクトを使います。
borders = cell.Borders   border = borders.Item(index)Borders オブジェクトはセルの上下左右の境界線を表現する Border オブジェクトを格納するコレクションオブジェクトです。
セルを囲むどの境界線を取り出すかは Borders オブジェクトの Item メソッドに何を引数として渡すかで決まります。
そして、線種や線の太さを調べるには、Border オブジェクトの LineStyle プロパティや Weight プロパティを使用します。
LineStyle プロパティの値によってその境界線の線種が分かり、 Weight プロパティによって線の太さが分かります。それぞれの線種ごとに定数が割り当てられていますので、その定数をスクリプト中に直接書くことによって線種や線の太さを調べることもできます。定数の値は、オブジェクトブラウザを使えば、簡単に調べることができます。
しかしながら、通常はもっと良い方法があります。 COM コンポーネントで定義された定数を利用する方法です。 Excel のような Office アプリケーションではさまざまな用途で定数が使われています。
COM コンポーネントで定義された定数を使うことで、より分かりやすいプログラムを書くことができます。
この COM コンポーネントで定義された定数を Ruby のクラスやモジュールにロードするために WIN32OLE::const_load メソッドを使います。
WIN32OLE.const_load(borders,Border)この行で Border モジュールに borders という COM オブジェクトで使われている定数がロードされます。
今回のスクリプトでは、セルの中でどの境界線を取り出すかを指定するときや、そして境界線の線種や線の太さを区別するために定数をロードしています。
例えば、セルの周りのどの境界線かを区別するための定数の名前と値は次のようになります。この値はオブジェクトブラウザを使うことで確認できます。
定数名説明
xlEdgeLeft左の境界線7
xlEdgeTop上の境界線8
xlEdgeBottom下の境界線9
xlEdgeRight右の境界線10
ここで定数名がすべて小文字から始まっています。 Ruby で定義される定数は Ruby の文法上 大文字から始まります。そのため、COM コンポーネントで定義された定数名の最初の文字が小文字の場合は、そのままでは Ruby の定数とできません。
この制限があるため、Win32OLEではある工夫をしています。定数名の最初の文字を大文字にしているのです。
たとえば、COM コンポーネントで "xlEdgeTop" という変数は、 "XlEdgeTop" という変数名になります。
WIN32OLE.const_load(borders,Border)WIN32OLE::const_load メソッドの第二引数は モジュール/クラスになります。 Ruby は動的なプログラミング言語で、実行中にモジュールに定数の定義を追加することもできるという特徴があります。この特徴を利用して、ここでは Border というモジュールに定数の定義を追加しています。
このようにロードした定数は、普通の Ruby の定数と同じように
Border::XlEdgeTopなどという形で使えます。
こうして得られた定数を使って、境界線ごとにその境界線の線種や線の太さを調べるのがさきほどのスクリプトとなります。
この節の最後に linetype メソッドで使っているテクニックについて簡単に説明します。
    @@linestyles ||= nilという行は、 @@linestyles が未定義か偽ならば nil を代入しています。初期化時のイディオムとしてよく使われるテクニックです。
      %w(XlContinuous XlDash XlDashDot          XlDashDotDot XlDot XlDouble          XlLineStyleNone XlSlantDashDot).each do |linestyle|%w() は文字列配列を作るためのリテラル記法のひとつです。
        v = Border.const_get(linestyle)Module#const_get メソッドを使うと、そのモジュールで定義された定数を取得できます。
入力サンプルとして使用した Excel ファイルは次のものです。
sample2.xls
カレントディレクトリに sample2.xls を置いて、このスクリプトを実行してください。このスクリプトの実行結果は次のようになります。
上 XlContinuous XlThin右 XlContinuous XlThin下 XlContinuous XlThin左 XlContinuous XlThin取得するセルを変えながらいろいろと実験してみてください。

連結セルの扱いここまで、取り上げてきませんでしたが、Excel のセルの中には複数のセルを結合させたものがあります。
結合されたセルの場合は、値を取得するときに工夫が必要です。
下記のスクリプトは、今まで利用してきた Worksheet モジュールを連結セルの値を取得するように改変したサンプルです。
excel5.rb   1| require 'win32ole'  2|   3| module Worksheet  4|   def [] y,x  5|     cell = self.Cells.Item(y,x)  6|     if cell.MergeCells  7|       cell.MergeArea.Item(1,1).Value  8|     else  9|       cell.Value 10|     end 11|   end 12| end 13|  14| def getAbsolutePath filename 15|   fso = WIN32OLE.new('Scripting.FileSystemObject') 16|   return fso.GetAbsolutePathName(filename) 17| end 18|  19| def openExcelWorkbook filename 20|   filename = getAbsolutePath(filename) 21|  22|   xl = WIN32OLE.new('Excel.Application') 23|   book = xl.Workbooks.Open(filename) 24|   begin 25|     yield book 26|   ensure 27|     xl.Workbooks.Close 28|     xl.Quit 29|   end 30| end 31|  32| openExcelWorkbook("sample2.xls") do |book| 33|   sheet = book.Worksheets.Item("Sheet1") 34|   sheet.extend Worksheet 35|   puts sheet[7,2] 36|   puts sheet[7,3] 37| end
このスクリプトで重要なのは、下記の箇所です。
  def [] y,x    cell = self.Cells.Item(y,x)    if cell.MergeCells      cell.MergeArea.Item(1,1).Value    else      cell.Value    end  endRange オブジェクトの MergeCells プロパティは そのセルが結合されていれば true を、結合されていなければ、false を返します。
Range オブジェクトの MergeArea プロパティは、セルが結合されている場合、そのセルを含む結合範囲の Range オブジェクトを返します。
結合されたセルを表現する Range オブジェクトの場合は、 Item(1,1) で取り出せるセルの値が結合されたセルの値となります。
これはこういうものだと理解してください。

Excel ファイルをパースするときの私のやり方これまで、Excel ファイルをパースする方法について学んできました。
この節では筆者が Excel ファイルをパースするときのやり方について簡単に整理して紹介しようと思います。
まず、私はパースするときの心構えとしてできる限り単純に行うということを重視します。これは、必要以上に一般的にしたり、難しく作ったりしないということです。
どれくらいが必要かは、例えばそのスクリプトが使い捨てなのか今後も継続して使うのかといったことで判断します。この違いで実装方針というのは、大きく変わります。
使い捨てであれば、遅くてもかまわないですし、多少保守性が悪くなっても問題はありません。とにかく結果が早く得られることを考えます。そこで、Excel ファイルを分析して決めうちで処理を行う実装をしたりします。 Excel ファイルに手を入れて、あらかじめ作成するスクリプトの例外事項を少なくすることも考えます。例外的な値やセルの順があると、パースするのが急に難しくなります。それを条件分岐として実装することもできますが、それよりも Excel ファイルをあらかじめ編集しておく方が楽なこともあります。
継続して使いそうな場合はまず見通しよく実装します。 Excel ファイルを事前に編集しなくても正しくパースできる程度には、実装を行います。定期的に前処理を行うことは面倒だからです。
また、Excel ファイルを作成している人と調整して、スクリプトを楽に作成できるように Excel ファイルの形式を整えてもらうこともあります。
ひたむきにプログラミングすることだけが、目的を達成するための方法ではありません。
私は単純なやり方をとります。その方が保守性も高く簡単に作れるからです。
どの方法が一番単純かは、Excel ファイルがどのような形式かで決まります。
これから私が考える単純さについて簡単に書きます。個人的な方針として、次の2点を大切にしています。
  • 条件分岐の数を減らす
  • 状態への参照を減らす
ここで「状態への参照」というのはここでの説明のために私が導入したフレーズで、説明が難しいです。例えば、あるセルがあってそのセルの見出しを同じ列から探し出すときは、そのセルの状態を参照しようとしているわけです。一行前のパースした結果を記憶して、今の行のパースに利用するような場合も状態への参照が増えているような感じがしてしまいます。
状態を参照することで柔軟性が増し、適用範囲を広げることができます。条件分岐についても同様で、多くすればそれまで例外的であった部分にも適用できるようになり、柔軟性が高まります。
条件分岐のないプログラムを書けることは稀でしょう。同じように完全に状態を参照しなくてもいいような場合もないでしょう。そういうときは、CSV に出力するだけで事が足りることも多く、改めてスクリプトを書く必要もありません。
しかしながら、あまりに多くなってくると複雑になってるな、嫌だなと感じます。例外的な場合に対応しようとすると、条件分岐を行う必要もありますし、条件分岐を行うために、今まで利用していなかった状態への参照が必要になってしまいがちです。
Excel ファイルのパースに関して言うと、今ある Excel ファイルをパースできれば充分なことが多く、それ以上に適用範囲を広げる努力はあまり報われないように思います。むしろ、突発的に生じる例外的な場合には、例外的なセルの使い方があったということが分かることの方が大事だったりします。
説明が下手ですいませんが、この節では私のやり方・指針について紹介しました。みなさんに合わない点もあるかもしれません。皆さんも自分に合ったやり方、基準を見つけていってください。




Excel ファイルの自動作成ここまで、パースする方法について学んできました。この章では、Excel ファイルを自動作成する方法について学びます。
自動作成のために必要な知識は、セルの値を更新することです。
この章で説明することは多くありません。次の項目になります。
  • ワークシートの特定の位置の値の更新
  • 現在のワークブックの保存
  • []= メソッドに複数引数が使えること
下記のスクリプトは、セルの値を更新できるように、Worksheet モジュールを拡張しています。このスクリプトを実行するときは、sample2.xls を開いている場合はすべて閉じてから行ってください。
excel6.rb   1| require 'win32ole'  2|   3| module Worksheet  4|   def [] y,x  5|     cell = self.Cells.Item(y,x)  6|     if cell.MergeCells  7|       cell.MergeArea.Item(1,1).Value  8|     else  9|       cell.Value 10|     end 11|   end 12|  13|   def []= y,x,value 14|     cell = self.Cells.Item(y,x) 15|     if cell.MergeCells 16|       cell.MergeArea.Item(1,1).Value = value 17|     else 18|       cell.Value = value 19|     end 20|   end 21| end 22|  23| def getAbsolutePath filename 24|   fso = WIN32OLE.new('Scripting.FileSystemObject') 25|   return fso.GetAbsolutePathName(filename) 26| end 27|  28| def openExcelWorkbook filename 29|   filename = getAbsolutePath(filename) 30|  31|   xl = WIN32OLE.new('Excel.Application') 32|   xl.Visible = true 33|   book = xl.Workbooks.Open(filename) 34|   begin 35|     yield book 36|   ensure 37|     xl.Workbooks.Close 38|     xl.Quit 39|   end 40| end 41|  42| openExcelWorkbook("sample2.xls") do |book| 43|   sheet = book.Worksheets.Item(2) 44|   sheet.extend Worksheet 45|  46|   sheet[2,2] = "Ruby" 47|   sheet[2,3] = "Python" 48|   sheet[2,4] = "Perl" 49|   book.Save 50| end 51|
このスクリプトは、さきほどと同じ sample2.xls をカレントディレクトリに置いて実行します。すると、2枚目のシートのセルに "Ruby", "Python", "Perl" という文字列を書き込みます。このプログラムを実行すると、Excel を終了させるので、sample2.xls を開きなおして確認してみてください。
このスクリプトで説明するべきことは []= メソッドの定義のところだけです。
  def []= y,x,value    cell = self.Cells.Item(y,x)    if cell.MergeCells      cell.MergeArea.Item(1,1).Value = value    else      cell.Value = value    end  endRuby の文法では []= メソッドは、[] メソッドと同じ様に複数の引数をとることができます。この機能を利用して、セルの値を更新できるように、[]= メソッドを定義しています。
ここで定義した []= メソッドでは、連結セルかどうかを判定しています。連結セルの場合は、その連結セルの値を更新します。
Appliction オブジェクトの ActiveWorkbook プロパティは、現在アクティブな Workbook オブジェクトを返します。そして、Workbook オブジェクトの Save メソッドを使うことで、保存できます。名前をつけて保存するときは、 SaveAs メソッドを使います。
このスクリプトが動かない場合は、すでに sample2.xls を開いていないかを確認してください。
ほかに境界線を編集する場合は、先ほども使いました Borders プロパティを使うことでできます。
筆者は、あらかじめ境界線を引いた Excel ファイルを用意して、作成するスクリプトでの処理は、そのセルの値を更新するだけにします。境界線まで自動作成するのは、手間がかかるからです。
セルの値を変更できれば、充分なことは多いでしょう。




よく使うオブジェクトとメソッドここまで、Excel ファイルをパースしたり、自動作成したりするスクリプトについて説明してきました。 Excel 関係で使用可能なオブジェクトは非常に数多くあります。しかし、その中でよく使うものは極々一部です。
そこで、よく使うオブジェクトとメソッドについてこの章で簡単に説明します。
どのような引数をとるかや、実際の使い方については Visual Basic のヘルプや Google で検索した内容を参照してください。
Excel 関係の COM オブジェクトでよく使うオブジェクトは次のものです。
オブジェクト説明
ApplicationWorkbook や Window、各種イベントの取得ができる
WorkbooksWorkbook のコレクションオブジェクト
Workbookファイルを開いたり、保存したりできる
WorksheetsWorksheet のコレクションオブジェクト
Worksheetワークシートの名前、Range を取得できる
Rangeセルの値の取得、プロパティの設定ができる。

Application オブジェクト
Visible可視属性。Visible = true とすることでウィンドウを表示できる
WorkbooksWorkbook のコレクション
WindowsWindow のコレクション
ActiveWorkbookアクティブな Workbook を返す
Worksheetsアクティブな Workbook の Worksheets を返す。

Workbooks オブジェクト
Add新しい Workbook を追加
Open既存のWorkbook を開く
Closeすべての Workbook を閉じる

Workbook オブジェクト
SaveWorkbook を保存する
SaveAsWorkbook を保存する。多数の引数がある。
PrintOutWorkbookを印刷する
CloseWorkbook を閉じる
WorksheetsWorksheet のコレクション。Sheet1 という名前の Worksheet にアクセスするには、aWorksheets.Item("Sheet1") とする。

Worksheet オブジェクト
Range セルや複数のセルの集合を返すプロパティ。引数としては、"A1"や"A1:B2"といった文字列形式。sheet.Range(sheet.Cells(1,1),sheet(2,2)といった指定もできる。他にセルに名前をつけている場合は、sheet.Range("名前") といった形もできる。
Cells ワークシートのセルの集合を返すプロパティ。
Nameワークシートの名前を取得したり、変更したりできる。

Range オブジェクト
Valueセルの値。範囲の場合はセルの値の配列。
Bordersセルの境界線を示す
Heightセルの高さを返す(Integer)
Widthセルの幅(Double)
RowRangeオブジェクトの行の座標を返す
ColumnRangeオブジェクトの列の座標を返す
MergeCellsそのセルが結合されているかどうかを返す。(true/false)
CountRangeオブジェクトに含まれた範囲のセルの数を返す
Rows行で切り出した Range オブジェクト
Columns列で切り出した Range オブジェクト
Mergeセルの結合を行うメソッド




まとめ今回は、Ruby で Excel ファイルを扱う方法について学びました。 Excel ファイルを扱う方法はここに紹介してきた以外にもいろいろあります。
これまでの内容でパースや自動作成といった場合には対応できると思いますが、Excel にはまだまださまざまな機能があります。今回紹介した内容ではまったく網羅できてはいないでしょう。
そのような場合は、前回紹介したように、オブジェクトブラウザを使ったり VBA での操作の解説のページなどを参考にしてください。また、Excel の場合は特にキーボードマクロを保存して、結果としてできた Visual Basic のスクリプトを眺めればかなり参考になります。
創意工夫しながら、自分のやりたいことを実現していきましょう。
(アドバイザー:arton、助田 雅紀)




エピローグ「できた!」この一日、あなたは、憂鬱な Excel ファイルと付き合ってきた日々とサヨナラを言うために、慣れない Win32OLE プログラミングに取り組んできました。そして今、Excel ファイルをパースするスクリプトを書き上げたのです。
セルに想定外のデータが入っていた箇所があったりしてデバッグにも苦労していましたが、今度の実行結果を見ていると、問題なく動いています。
これからは、今までのあの憂鬱な作業から解放されると思うとそれだけで気分が晴々としてきました。
「やったぞ!」
あなたがやり遂げたことを同僚に説明していると、自分が誇らしく思えてきました。単純な作業を繰り返していたときには達成感という気持ちがあることも忘れてしまっていたことに気がつきました。
そして、もっとよくしていきたいなと思いながら結果を見ていたあなたはこんなふうに思いました。
「これをデータベース化して今までよりもっと楽に簡単に扱えるようにならないかな?」
(つづく)
次号では、Win32OLE でデータベースを扱う方法について学んでいきます。ご期待ください。




作者についてcuzic が Ruby を始めたのは2000年くらいです。
cuzic は Windows + Cygwin + Emacs の環境での開発を好み、スポーツジム通いを続けるプログラマーです。
作者への連絡先は cuzic atmark cuzic.com です。




参考 
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-8-1 08:19:25 | 显示全部楼层
【第 3 回】 ADODB



プロローグあなたは、膨大にあった Excel 表を解析して、業務を少しずつ自動化していきました。自動化していくにつれて、今までの仕事がとても楽になり、満足していました。
けれど、そういう作業を続けているうちにある壁にぶつかりました。
「どうにかならないかな?」
今やっているやり方では、最新の情報に更新しようと思うたび Excel 表を毎回パースすることになります。このパースするのにかかる時間が長くどんどん面倒に思えてきたのです。
「昔かじったデータベースの知識を使って、このデータをなんとかできないかなぁ」




はじめに前回の記事では、Excel の表に入ったデータを Ruby を用いてどのように取得するかについて学びました。
Excel 表を単純にパースするだけでも、かなり業務を楽にしていくことができるでしょう。 Excel 表の値を参照するような仕事は面倒なものです。
しかし、ある程度大規模になると Excel は便利とはいえなくなってきます。例えば、Excel ではデータの入力と表示の形式に区別がないため、入力には楽な形式、表示には楽な形式というのができません。また、分析をするときにもいろいろと不便なことが多くなります。
こういう問題に対処するために、データベースを利用する方法があることはみなさんもご存知でしょう。
今回の記事で私たちは、Ruby の Win32OLE ライブラリを使って、データベースを扱う方法について学びます。




今回の目的Microsoft が提供している COM コンポーネントの中には、 ADO (ActiveX Data Object) と呼ばれる一連のコンポーネントがあります。
これらのコンポーネントを使えば、データベースに対するデータの入力や取得ができます。
ADO にはよく使うコンポーネントとして、ADODB.Connection と呼ばれるコンポーネントと ADODB.Recordset と呼ばれるコンポーネントがあります。 ADODB.Connection というオブジェクトは、データベースへの接続を表現するオブジェクトで、Recordset は、レコードセットを表現するオブジェクトです。レコードセットとは、SQL の SELECT 文を実行した結果のデータの集まりを言います。
今回の記事では、これらのオブジェクトを使って次の内容の動作を行います。
  • ADO の概要
  • SQL を用いたデータの追加
  • データベースからのデータの取得
  • ADODB.Recordset を用いたデータの更新
まず、ADO ではどのようなオブジェクトが提供されていて、利用できるのかについて学びます。
次に実用的に CSV 形式のデータをデータベースに入力するスクリプトを例にして、ADO の Connection オブジェクトの使い方を学びます。
そして、データベースからデータを取得する方法について学びます。
今回の記事では、データベースや SQL が何なのか、ということをすでに知っている人を主な対象としています。これらについて学びたい方は、別途これらについて解説した文章を参考にしてください。




ADO の概要ADO は、データベースにアクセスするための一連の COM コンポーネントです。
ADO を用いることで、データベースに保存されたデータに直接アクセスして、次の操作ができます。
  • レコードとそのデータの取得
  • レコードの追加
  • レコードの削除
  • レコードの検索
ADO のコンポーネントの中でも今回は特に、Connection オブジェクトと Recordset オブジェクトの 2 つのオブジェクトについて学びます。また、レコードの値の取得や更新を行うときに用いる Fields コレクション、 Field オブジェクトの使い方についても学んでいきます。
具体的な使い方についてはこれから一緒に学んでいきましょう。




ADO でデータを入力Excel 表でパースしたデータをまずどうしたいかと言えば、データベースに入力したいですよね。データベースに入力することで、検索や更新を簡単に行いやすくなります。
Excel 表でパースしたデータを CSV 形式にすることは簡単です。そこでこの章では、CSV 形式のデータをデータベースに追加していく方法について学んでいきます。
まず、既存のデータベースに対して接続し、そのデータベースのあるテーブルに対して一行ずつデータを追加するスクリプトを例に次の項目について学んでいきましょう。
  • 接続文字列とは何か
  • 接続の開き方
  • アクションクエリの実行
次に CSV 形式のデータを取得して、データベースに追加していくスクリプトを紹介します。

前準備今回のスクリプトを実行するには、まず操作するためのデータベースを用意してあげる必要があります。
今回は、Microsoft Office に含まれている Access の mdb 形式のデータベースをサンプルのデータベースとして用います。
このデータベースは次のようなテーブルとフィールドを持ちます。
サンプルとして、次の mdb ファイルを用意します。ダウンロードして使ってください。
sample1.mdb
一行レコードを追加するスクリプトinsert1record.rb    1|require 'win32ole'   2|   3|conn = WIN32OLE.new("ADODB.Connection")   4|connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=sample1.mdb"   5|conn.Open connstr   6|   7|begin   8|  sql = "INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +    9|    "VALUES ('関東大震災','1923/09/01',7.9,142807,TRUE);"  10|  conn.Execute sql  11|rescue  12|  STDERR.puts sql  13|  STDERR.puts $!  14|end  15|  16|conn.Close
順に説明していきます。
conn = WIN32OLE.new("ADODB.Connection")ProgID が ADODB.Connection の COM オブジェクトを作成する行です。 conn が Connection オブジェクトになります。この Connection オブジェクトを用いて、どのデータソースへの接続するかを指定したり、接続を開いたり、閉じたり、SQL を実行したりします。
cconn.Open connstrConnection オブジェクトの Open メソッドは引数に接続文字列(Connection String) をとります。接続文字列というのは、引数名=値 のフォーマットの引数列をセミコロンで連結したものです。この接続文字列の Driver を適切に設定することで Microsoft Access や SQL Server さらには ODBC を経由することで PostgreSQL に対して、というようにさまざまな DBMS に対して接続できます。
接続文字列について詳しく知りたい方は、接続文字列の作成や、 データ ソースにアクセスする を参考にしてください。
今回は、「Microsoft Access」の mdb ファイルにアクセスします。そのときは上記のように "DRIVER={Microsoft Access Driver (*.mdb)}" と指定します。 Dbq 以降の引数はデータソースドライバに対して渡される引数となります。 Microsoft Access Driver の場合は、その mdb ファイルがどこにあるのかを指定するために  dbq という名前付引数を使用します。パスを指定するのに使う引数はデータソースドライバによって異なります。詳しくは上記のリンクを参照してください。
begin  sql = "INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +     "VALUES ('関東大震災','1923/09/01',7.9,142807,TRUE);"  conn.Execute sqlrescue  STDERR.puts sql  STDERR.puts $!ensure          conn.Closeend繰り返しになりますが、この記事では SQL の構文についてはすでに知っていることを想定します。 SQL について知りたい方は Microsoft Access のヘルプや、Google で検索して調べてください。
INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +      "VALUES ('関東大震災','1923/09/01',7.9,142807,TRUE);の SQL ステートメントは、earthquake テーブルに対して、関東大震災のデータを追加する文になります。この SQL ステートメントを実際に実行しているのが次の行です。
   conn.Execute sqlこの行で conn という Connection オブジェクトが接続しているデータソースに対して SQL ステートメントを実行します。
Connection オブジェクトの Execute メソッドで、アクションクエリを実行したり、選択クエリを実行してレコードセットを得たりできます。アクションクエリとは、更新クエリ (UPDATE ~)、削除クエリ (DELETE ~)、追加クエリ (INSERT INTO ~)、テーブル作成クエリ (SELECT ~ INTO ~) の 4種類の SQL ステートメントを指します。選択クエリは、SELECT ステートメントのようなデータを取得するための SQL ステートメントです。後で詳しく説明しますが、Connection オブジェクトでは、選択クエリを実行しても、データの取得はできますが、更新はできません。
この SQL は、アクションクエリの中でも特に「追加クエリ」になります。
begin ~ rescue ~ ensure ~ end は前回も出てきました始め処理終わり処理のイディオムです。Java なら Before/After パターンと呼ばれたりします。結城浩さんのデザインパターン紹介 が参考になります。
rescue  STDERR.puts sql  STDERR.puts $!で、エラーが生じたときに実行させた SQL ステートメントを出力しています。このようにエラーがあったときに SQL を表示すれば、デバッグの手助けになります。
以上のような簡単なスクリプトで、簡単に SQL ステートメントを実行できます。

CSV からのデータを追加いよいよ、この節では、CSV からのデータの追加について説明します。
この節で学ぶ項目は次のとおりになります。
  • ARGF を使った引数で示されたファイルを扱う
  • %w() を使った文字列の配列リテラル
  • Enumerable#zip
  • Enumerable#map
それでは、サンプルのスクリプトについて見てみましょう。このスクリプトは Ruby 1.8 以上で動作します。
csv2rs.rb    1|require 'win32ole'   2|   3|def startADO filename    4|  cn = WIN32OLE.new("ADODB.Connection")   5|  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"   6|  cn.Open connstr   7|  begin    8|    yield cn   9|  ensure  10|    cn.Close  11|  end  12|end  13|  14|csvfields = %w(Day Name Magnitude NumOfDeaths DeadOrAlive)  15|insertfields =  [  16|  ['Name',false],  17|  ['Day',false],  18|  ['Magnitude',true],  19|  ['NumOfDeaths',true],  20|  ['DeadOrAlive',true]]  21|  22|startADO("sample1.mdb") do |conn|  23|  ARGF.each_line do |line|  24|    record = {}  25|    csvfields.zip(line.chomp.split(/,/)) do |field,value|  26|      record[field] = value  27|    end  28|    values = insertfields.map do |field,rawvalue|   29|      if rawvalue  30|        "#{record[field]}"  31|      else  32|        "'#{record[field]}'"  33|      end  34|    end  35|    fieldStatement = insertfields.map{|f,v| f}.join(',')  36|      37|    sql = "INSERT INTO earthquake (#{fieldStatement}) " +   38|      "VALUES ( #{values.join(',')} );"  39|    begin  40|      conn.Execute sql  41|    rescue  42|      STDERR.puts sql  43|      STDERR.puts $!  44|    end  45|  end  46|end
conn = WIN32OLE.new("ADODB.Connection")cこの 2 行については先ほど学んだところです。 conn が Connection オブジェクトで、connstr が「Microsoft Access Driver」 を使用する接続文字列です。
conn.Open connstrこの行で Connection を開きます。
csvfields = %w(Day Name Magnitude NumOfDeaths DeadOrAlive)%w() とすることで、要素が文字列の配列を生成できます。
この行は、入力する CSV が持つフィールドとその順番を与えています。 CSV から入力したいときは、どの列がテーブル上のどのフィールドに対応するかをあらかじめ与える必要があります。このスクリプトでは、そのためにこの csvfields という変数を用いています。
ARGF.each_line do |line|  ...endARGF というのは、スクリプトに指定した引数をファイル名とみなして、それらのファイルを仮想ファイルとしたオブジェクトです。簡単なスクリプトを作っているときに、引数のファイルを開く処理を記述する手間を省けるので、便利です。
  csvfields.zip(line.chomp.split(/,/)) do |field,value|    record[field] = value  endここで、先ほど用意した csvfields という文字列の配列を使って CSV の一行ずつを record というハッシュにフィールド名とその値のペアを格納させていきます。
Enumerable#zip は、Ruby 1.8 以上の場合、存在するメソッドです。 csvfields の各要素と、zip の引数の各要素を組み合わせて、ブロックに渡します。
line.chomp.split(/,/) は、分かると思います。カンマ区切りの CSV を配列にしています。
zip を使っているところを初めてみた場合は、 Ruby リファレンスマニュアルの Enumerable の説明 をよく読んでみてください。
Enumerable#zip を使うことで複数の配列を最初の要素や、2 番目の要素というように順にブロックに渡すことができるので便利です。
ここでは、csvfields のフィールド名が各要素となり、 line.chomp.split(/,/) で、対応するフィールドの値の値となります。それを |field,value| で受けて、record というハッシュに格納していきます。
zip についての説明はこれくらいにして、これから次の SQL を生成する部分について学んでいきましょう。
  values = insertfields.map do |field,rawvalue|    if rawvalue      "#{record[field]}"    else      "'#{record[field]}'"    end  endinsertfields は、二つの要素を持つ配列の配列となっています。その二つの要素とは、フィールド名と そのフィールドの値をシングルクオートで囲まず、生の値として出力する必要があるかどうかを示すフラグの二つです。
Enumerable#map は、前回も出てきましたが覚えていただけているでしょうか?
ここでは、INSERT INTO 文の VALUES 以下の句を生成するために使っています。
if rawvalue という行で、rawvalue という条件式が真かどうかを評価して、分岐しています。 C などの言語とは異なり、Ruby では制御構造は式です。つまり、Ruby の if 文は値を返します。 if 文の返す値は最後に評価した式の結果となります。
なお、Ruby では false または nil だけが偽で、それ以外は 0 や空文字列も含めてすべて真です。
そして、Enumerable#map メソッドでは各要素に対してブロックを評価した結果をすべて含む配列を返します。
説明がまわりくどくなってしまいましたが、結局、 values = insertfields.map do |field,rawvalue| ... end という一連の処理では、この if 文を評価した値を各要素とする配列を values に代入することになります。
  fieldStatement = insertfields.map{|f,v| f}.join(',')また map が出てきましたね。今度は簡単です。最初の要素の配列を作っているだけです。
  sql = "INSERT INTO earthquake (#{fieldStatement}) " +    "VALUES ( #{values.join(',')} );"今まで下準備をしてきましたが、ここで一気に SQL を組み立てあげます。こうすることで、フィールドの対応関係などを指折り数えたりすることなく、SQL を生成できるようになります。
  begin    conn.Execute sql  rescue    STDERR.puts sql    STDERR.puts $!  endここは先ほどの一行ずつレコードを追加するときのスクリプトと同じ記述です。 conn.Execute で、sql を実行しています。
それではこのスクリプトを実際に実行してみましょう。
サンプルの CSV がここにあります。 sample1.csv
1923/09/01 00:00:00,関東大震災,7.9,142807.0,true1994/10/04 00:00:00,北海道東方沖地震,8.1,0.0,false1995/01/17 00:00:00,阪神淡路大震災,7.2,6418.0,true2004/10/23 00:00:00,新潟県中越地震,6.8,37.0,true先ほど、関東大震災の行をすでに追加してしまっているときは、その行を実行するときに除いてください。
ruby csv2rs.rb sample1.csvと、実行すると csv に含まれるレコードをデータベースに追加できます。




データソースのレコードの参照前の章では、データソースに対してデータを追加していく方法について学びました。
それでは、データソースに現在格納されているデータをどのようにして、取得できるのでしょうか?
この章では、次の項目について学びます。
  • レコードセットとは
  • レコードセットの開き方
  • カレントレコードとは
  • カレントレコードの特定のフィールドの値の取得
では、具体的なサンプルスクリプトを見ていきましょう。
rs2csv.rb    1|require 'win32ole'   2|   3|def startADO filename    4|  cn = WIN32OLE.new("ADODB.Connection")   5|  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"   6|  cn.Open connstr   7|  begin    8|    yield cn   9|  ensure  10|    cn.Close  11|  end  12|end  13|  14|module Recordset  15|  def [] field  16|    self.Fields.Item(field).Value  17|  end  18|  19|  def []= field,value  20|    self.Fields.Item(field).Value = value  21|  end  22|    23|  def each_record  24|    if self.EOF or self.BOF  25|      return   26|    end  27|    self.MoveFirst  28|    until self.EOF or self.BOF  29|      yield self  30|      self.MoveNext  31|    end  32|  end  33|end  34|  35|  36|startADO("sample1.mdb") do |cn|  37|  sql = "SELECT * FROM earthquake;"  38|  rs = cn.Execute(sql)  39|  rs.extend Recordset  40|  fields = ["Day","Name","Magnitude","NumOfDeaths","DeadOrAlive"]  41|  rs.each_record do |rs|  42|    values = fields.map do |field|  43|      rs[field]  44|    end  45|    puts values.join(",")  46|  end  47|end
このスクリプトでは、earthquake テーブルの値をカンマ区切りで出力します。
まず、startADO メソッドによって、データソースへの接続を行います。
def startADO filename   cn = WIN32OLE.new("ADODB.Connection")  c  cn.Open connstr  begin     yield cn  ensure    cn.Close  endendこのメソッドは、filename を引数とします。 filename は Microsoft Access のデータベースのファイル名です。すると、引数付きブロックに、Connection オブジェクトを渡し、ブロック実行後に Connection オブジェクトを閉じます。
SQL ステートメントを実行した結果となるレコードセットを得るには Recordset オブジェクトの Open メソッドを利用します。
begin ~ ensure ~ end の構文は今まで何度もでてきた Before/After パターンのイディオムです。もう慣れてきましたでしょうか?
   sql = "SELECT * FROM earthquake;"   rs = cn.Execute(sql)   rs.extend Recordsetここでレコードセットを開きます。 Connection オブジェクトの Execute メソッドを用いて Recordset オブジェクトを作成しています。詳しくは、ADO 入門講座 の「レコードセットを開く」のページを参考にしてください。
このページにも書かれているのですが、Recordset オブジェクトを作成するには、次の 3つの方法があります。
  • Recordset オブジェクトの Open メソッドの利用
  • Connection オブジェクトの Execute メソッドの利用
  • Command オブジェクトの Execute メソッドの利用
Connection オブジェクトの Execute メソッドを利用する例については先ほど説明しましたね。前の節で実行していた SQL の INSERT 文をSELECT 文に変更すれば、 Execute メソッドでレコードセットを得ることができます。
しかしながら、Recordset オブジェクトの Open メソッドで得られる Recordset オブジェクトと、Connection オブジェクトや Command オブジェクトの Execute メソッドで得られる Recordset オブジェクトとは違う性質のものになります。 Connection オブジェクトや Command オブジェクトから作成すると読み取り専用の Recordset オブジェクトが得られます。これに対して、Recordset オブジェクトの Open メソッドを実行する場合は、レコードの追加、変更、削除も行える Recordset オブジェクトを得ることができます。
今回のスクリプトの場合は、参照しか行わないのでどちらの方法でも可能です。しかしながら、更新・削除を行う場合は、Recordset オブジェクトを作成してから Open メソッドでレコードセットを開く必要があります。
    rs.extend RecordsetObject#extend メソッドを使うことで、rs オブジェクトに Recordset モジュールで定義しているレコードセットを操作するためのメソッドを使えるようにしています。
上記スクリプトの Recordset モジュールでは、レコードセットの中のすべてのレコードのデータを取得するために each_record メソッドを定義しています。
  def each_record    if self.EOF || self.BOF      return     end    self.MoveFirst    until self.EOF || self.BOF      yield self      self.MoveNext    end  endこのモジュールでは、Recordset オブジェクトのプロパティ、メソッドを利用して、すべてのレコードを順に操作できるようなイテレータとして、each_record メソッドを定義しています。
ここで、レコードセットについて簡単に学んでいきましょう。
レコードセットというのは ADO においてデータソースのデータを取得、変更するのに使うオブジェクトになります。このレコードセットは、生成するのに使用した SQL ステートメントに対応するすべてのデータの集まりを持っています。
さらに、レコードセットは常に1つのレコードをカレントレコードとして参照します。そして、そのカレントレコードの値を、Recordset オブジェクトから取得、変更などを行うことができます。
ところが、場合によっては SQL に対応するレコードが一行もない場合があります。 SQL に対応するレコードがあるかどうか調べるのに使うプロパティが BOF や EOF です。本来、BOF プロパティはカレントレコードが最初のレコードより前にあるかどうかをしらべるときの プロパティで、EOF はカレントレコードが最後のレコードより後にあるかどうかを調べるプロパティです。
一致するレコードが一行もない場合は、EOF も BOF も true になります。一致するレコードがあれば、開いた直後 BOF も EOF も両方とも false になります。
そこで、次のようにして、一致するレコードがなければ、処理をやめています。
    if self.EOF || self.BOF      return     endここで、self が何なのかを思い出しましょう。
rs.extend Recordsetと実行したため、この変数 rs がこの Recordset モジュールで self が指すオブジェクトとなります。
次に、カレントレコードを移動する方法について学んでいきましょう。
    self.MoveFirst    until self.EOF || self.BOF      yield self      self.MoveNext    endここで、Move 系メソッドについて学びましょう。 Move 系メソッドには次の4つがあります。
  • MoveFirst 先頭のレコードに移動
  • MovePrevious 1つ前のレコードに移動
  • MoveNext 次のレコードに移動
  • MoveLast 最後のレコードに移動
これらのメソッドを組み合わせることで、カレントレコードを移動させていくことになります。
今回は MoveFirst と MoveNext を利用して先頭のレコードから順に次の行へと移動していきます。そして、カレントレコードを移動すると Recordset オブジェクトを yield して、特定の処理を行えるように引数となっているブロックに処理を渡しています。
この一連の処理を BOF プロパティか、EOF プロパティが false になるまで繰り返しています。
次は、each_record を実際に実行している部分の動作について学んでいきましょう。
  fields = ["Day","Name","Magnitude","NumOfDeaths","DeadOrAlive"]  rs.each_record do |rs|    values = fields.map do |field|      rs[field]    end    puts values.join(",")  endまず、fields にテーブルのフィールド名の配列を代入しています。そして、rs.each_record ですべての行に処理を行わせています。
    values = fields.map do |field|      rs[field]    endでは、配列に含まれた各フィールド名に対して、そのカレントレコードでのそのフィールドの値を得ています。
     rs[field]という行で、カレントレコードの field という名のフィールドに対応する値を取得しています。
Recordset モジュールの定義を見ると [] メソッドは次のようになっています。
   def [] field     self.Fields.Item(field).Value   endRecordset オブジェクトの Fields プロパティで得られる Fields コレクションは Field オブジェクトの集まりになります。これは複数形の場合は、それの単数形のオブジェクトのコレクションであるという単純な命名規則にしたがっている例です。
Fields コレクションの Item プロパティを用いると引数 field に対応する Field オブジェクトが得られます。 Field オブジェクトの Value プロパティが、そのフィールドの値です。
最後に Array#join メソッドで、","区切りで、値を連結して出力しています。




データソースに対する操作次のスクリプトの例ではデータソースに対して新しいレコードの追加を行っています。
addnew.rb    1|require 'win32ole'   2|   3|module Recordset   4|  def [] field   5|    self.Fields.Item(field).Value   6|  end   7|   8|  def []= field,value   9|    self.Fields.Item(field).Value = value  10|  end  11|end  12|  13|def startRS filename,sql  14|  cn = WIN32OLE.new("ADODB.Connection")  15|  rs = WIN32OLE.new("ADODB.Recordset")  16|  connstr = "DRIVER={Microsoft Access Driver (*.mdb)};Dbq=#{filename}"  17|  cn.ConnectionString = connstr  18|  cn.Open  19|  rs.Open sql,cn,3,3  20|  rs.extend Recordset  21|  begin   22|    yield rs  23|  ensure  24|    rs.Close  25|    cn.Close  26|  end  27|end  28|  29|sql = "SELECT * FROM earthquake;"  30|startRS("sample1.mdb",sql) do |rs|  31|  newrecords =   32|    [["Day" , "2000-10-06"],  33|    ["Name","鳥取県西部地震"],  34|    ["Magnitude","7.3"],  35|    ["NumOfDeaths","0"],  36|    ["DeadOrAlive","False"]]  37|  rs.AddNew  38|  newrecords.each do |field,value|  39|    rs[field] = value  40|  end  41|  rs.Update  42|end
レコードセットの更新・追加においては、まずレコードセットを更新・追加できるような形で開くことが必要です。
それをしているのが、次の行です。
  rs.Open sql,cn,3,3第3引数は、レコードセットのカーソルタイプを指定します。第4引数は、レコードセットのロックタイプを指定します。
カーソルタイプとロックタイプを指定するときは、次の表を参考にしてください。この値はオブジェクトブラウザを使うことで参照できます。
カーソルタイプ
定数名説明
0adOpenForwardOnlyレコードセットの先頭から後方へ移動できます。順に参照するときに高速に動作します。 (既定値)
1adOpenKeyset自由に移動できます。参照専用です。
2adOpenDynamic自由に移動できます。他のユーザの更新を参照できます。
3adOpenStatic自由に移動できます。他のユーザの更新を参照できません。
ロックタイプ
定数名説明
1adLockReadOnly読み取り専用です (既定値)
2adLockPessimistic排他ロックを行います
3adLockOptimistic共有ロックを行います
4adLockBatchOptimistic複数のレコードをバッチ更新処理します
カーソルタイプを指定することで、カレントレコードをどのように移動するかや、他のユーザの更新を考慮するかを決定できます。
ロックタイプを指定することで、データソースへのロックをどういう形で行うのかを決定できます。
第4引数に 3 を与えれば編集可能になり、レコードごとに共有ロックを行います。
前回の記事のように、WIN32OLE::const_load を使うことで COM オブジェクトで定義された定数を使うこともできます。
Recordset オブジェクトを用いてデータソースに格納されたデータを更新したい場合は、このように rs オブジェクトを作成したのちに、rs.Open メソッドでカーソルタイプやロックタイプを指定して、レコードセットを開きます。
そして、実際に更新するときは次のメソッドを使います。
  • AddNew メソッド
  • Update メソッド
AddNew メソッドはレコードを新規追加するときに用いるメソッドです。
AddNew メソッドを使うことで、カレントレコードを新規レコードにできます。つまり、AddNew メソッドを呼んでから、フィールドの値を更新することで、新規追加するレコードの値を設定できます。
SQL の INSERT INTO 文でも新規レコードの追加ができますが、 AddNew メソッドでも新規レコードの追加を行えます。
既存のレコードを更新するには、更新したいレコードに Move 系のメソッドなどで移動して、そしてフィールドの値を更新することで行えます。
それでは、INSERT INTO 文を用いる新規レコードの追加と Recordset オブジェクトを用いた方法と二つありますが、どのように使い分けると良いでしょうか?この使い分けは単なる好みで決めても特に問題がないのですが、 Field オブジェクトのプロパティをみて、条件分岐をしたい場合は、Recordset オブジェクトと AddNew メソッドを使用することになります。
詳しくは、最後のよく使うメソッド一覧の章を参照して欲しいのですが、 Field オブジェクトにはフィールド名やフィールドの型を取得するプロパティがあります。
フィールドの型や名をプログラム中で参照したい場合は、 AddNew メソッドを使う方法の方が楽にプログラムを書けます。なお、実行速度については自分が扱うデータベースで実際に処理させてみてどちらが速いか時間を測ってみた方がいいでしょう。
   def []= field,value     self.Fields.Item(field).Value = value   endRecordset モジュールのこのメソッド定義がデータの更新を行うときに重要になります。
前回の Excel シートの使い方を説明するときでも、[]= メソッドの定義を行いましたね。今回もそれと同様です。フィールド名と、その更新する値の二つを引数としてとります。
そして、field という名のフィールドの値 (Value プロパティ) を value に更新します。
そして、ここで定義されたモジュールとメソッドを次のように用いて、新規レコードの追加を行います。
  rs.AddNew  newrecords =     [["Day" , "2000-10-06"],    ["Name","鳥取県西部地震"],    ["Magnitude","7.3"],    ["NumOfDeaths","0"],    ["DeadOrAlive","False"]]  newrecords.each do |field,value|    rs[field] = value  end  rs.Updatenewrecords には、フィールド名とそのフィールドの値が代入されています。
そして、この配列を使って新規レコードの field という名のフィールドの値を value に更新しています。
  rs.UpdateUpdate メソッドは、更新した内容で確定するときに必要となります。
実は Update メソッドを明示的に呼ばなくても先ほどの Move 系のメソッドは、編集中に別のレコードに移動すると、暗黙的に Update メソッドを呼び、更新が保存されます。そのため Move系メソッドと組み合わせて利用するときは、 Update メソッドを特に記述しなくても更新された値が保存されます。
しかしながら、Update は明示的に呼ぶような習慣を持ちましょう。そうすることで、そこで一区切りということが分かりスクリプトが分かりやすくなります。それに今回のスクリプトのように Move 系メソッドを呼ばない場合には、 Update がないとエラーが発生することになります。 Update は書く習慣がある方がそのようなエラーに悩まされずに済みます。




SQL ステートメントの生成この章では、ADO と Win32OLE の組み合わせでももちろん適用できますが、一般的にデータベースを扱うときに役に立つ事柄について学んでいきます。
SQL ステートメントを生成することはデータベースを扱う上では必ず遭遇します。
しかしながら SQL ステートメントを生成するときのコードにクールと感じさせられることは多くありません。単純な文字列の連結を連ねて書いて SQL ステートメントの生成を行っているスクリプトをしばしば見かけます。
たとえば、次のようにして SQl を生成する方法があります。
sql = "INSERT INTO earthquake (Name,Day,Magnitude,NumOfDeaths,DeadOrAlive) " +     "VALUES ('#{name}','#{day}',#{magnidude},#{death},#{dead});"このような方法はパッとみて理解できるという点で優れています。
しかしながら、フィールドの数が多くなるにつれてこの方法は破綻してしまいます。フィールドの数があまりに多いと、長くなってしまい、対応関係が見てもよく分からず、1 つ抜けてバグになってしまったときに何が抜けているのかを調べるのに時間がかかってしまったりします。
もっと、簡単でいいやり方はないんでしょうか?
これまでにも何度か説明してきましたが、Ruby には Enumerable モジュールに、いくつか便利なメソッドが定義されています。 map や join です。
これらのメソッドを組み合わせながら、SQL ステートメントを生成するようなテクニックについてこの章では学んでいきます。

INSERT ステートメントの生成INSERT ステートメントを生成するためのテクニックについては CSV からのデータの追加のところで、説明しました。知りたい方についてはそちらを参照してください。

UPDATE ステートメントの生成UPDATE ステートメントもEnumerable#map や Enumerable#join を使って SQL を生成することができます。
update.rb    1|def generateUPDATE tablename,values,constraints   2|  fields = values.map{|field,value,flag| field}   3|  field_statement = fields.join(",")   4|   5|  sql = "UPDATE #{tablename} SET "   6|  vs = values.map do |field,value,rawvalue|   7|    if rawvalue   8|      "#{field} = #{value}"   9|    else  10|      "#{field} = '#{value}'"  11|    end  12|  end  13|  set_statement = vs.join(" , ")  14|  sql.concat set_statement  15|    16|  where_statement = constraints.map do |field,value,rawvalue|  17|    if rawvalue  18|      "#{field} = #{value}"  19|    else  20|      "#{field} = '#{value}'"  21|    end  22|  end.join(" AND ")  23|    24|  sql.concat " WHERE #{where_statement};"  25|  return sql  26|end  27|  28|values = [['Magnitude',7.9,true],  29|  ['NumOfDeaths',142807,true],  30|  ['DeadOrAlive',"TRUE",true]]  31|  32|constraints = [['Name','関東大震災'],['Day','1923/09/01']]  33|  34|puts generateUPDATE("earthquake",values,constraints)
generateUPDATE メソッドは、3 つの引数をとります。
  • テーブル名
  • 更新するフィールド名とその値
  • WHERE 句で指定する条件となるフィールドとその値
テーブル名と更新するフィールド名とその値というのは、 INSERT ステートメントのときと同じになります。
違いは WHERE 句で指定する条件となるフィールドとその値を引数とする点です。
これは、第2引数と同じ形式の配列です。
この WHERE 句用に指定した配列を利用して、WHERE 句を生成していきます。
このメソッドでも先ほどと同様に WHERE 句を利用します。
SET の次に続く文字列は次のように生成します。
  vs = values.map do |field,value,rawvalue|    if rawvalue      "#{field} = #{value}"    else      "#{field} = '#{value}'"    end  end  set_statement = vs.join(",")map で使っているブロック引数の代入は多重代入と同じ規則に従います。この場合 values の要素が配列のとき、その配列の数が代入される側の個数、この場合は 2個を超えている場合はそのあまった要素は無視されます。足りない場合、この場合は 1個しかない場合は、対応する要素のないブロック引数には nil が代入されます。
この記述は慣れると非常に直感的です。配列 values の 1 要素が更新したいフィールド 1 つに対応していました。 values から map メソッドを利用して、「フィールド名 = 値」の配列 vs を得ます。そして配列 vs を join メソッドを利用して、連結することで SET 句を生成します。
同様のことを WHERE 句に対しても行っています。 WHERE 句のときの違いは join メソッドで連結するときにコロン(,) ではなく " AND " で連結しているという点です。そして、次のような表記も目新しいところでしょうか?
  end.join(" AND ")values.map ~ end で配列が返されるので、そのまま join を呼ぶことでその配列の各要素を " AND " で連結できます。一旦変数に格納する手間を省けて便利です。

SELECT ステートメントの生成同様に SELECT 文を生成するスクリプトについても紹介しておきましょう。
select.rb    1|def generateSELECT tablename,fields,constraints   2|  field_statement = fields.join(",")   3|   4|  sql = "SELECT #{field_statement} FROM #{tablename} "   5|     6|  where_statement = constraints.map do |field,value,rawvalue|   7|    if rawvalue   8|      "#{field} = #{value}"   9|    else  10|      "#{field} = '#{value}'"  11|    end  12|  end.join(" AND ")  13|    14|  sql.concat " WHERE #{where_statement};"  15|  return sql  16|end  17|  18|fields = %w(Name Magnitude NumOfDeaths DeadOrAlive)  19|constraints = [['Name','関東大震災'],['Day','1923/09/01']]  20|  21|puts generateSELECT("earthquake",fields,constraints)
これはもう分かりますね。しつこく同じ内容を解説することはしません。




よく使うオブジェクトとメソッドここまで、ADO を使ってレコードの参照や更新を行う方法について学んできました。 ADO のオブジェクトの中でも特によく使うオブジェクトとそのメソッドについて簡単にこの章で説明します。
どのような引数をとるかや、実際の使い方については、 MSDN や Google で検索した内容を参照してください。
ADO 関係の COM オブジェクトでよく使うのは次のものです。
オブジェクト説明
Connectionデータソースへの接続です。
Commandデータソースに対して実行するコマンドを保持します
RecordsetテーブルやSQLステートメントを実行して返されたレコードセット
FieldRecordsetオブジェクトの FIeld オブジェクトを格納します

Connection オブジェクト
BeginTrans新規トランザクションを開始します。
Close開いている接続とこの接続に関連する Recordset オブジェクトをすべて閉じます。Recordset オブジェクトの保留中の変更はすべてロールバックされます。
CommitTransすべての変更を保存して現在のトランザクションを終了します。
ConnectionStringデータソースへの接続のために必要な情報を設定するときに使用します。
Executeクエリや SQL ステートメントを実行します。
Openデータソースへの物理的な接続を確立します。
RollbackTransすべての変更をキャンセルして現在のトランザクションを終了します。
Stateオブジェクトが開いているか閉じているかを示します。

Command オブジェクト
CommandText通常は、実行したい SQL ステートメントを設定するときに使用します。
Executeクエリや SQL ステートメントを実行します。
Stateオブジェクトが開いているか閉じているかを示します。

Recordset オブジェクト
AddNew新規レコードの作成と初期化を行います
BOFカレントレコードの位置が最初のレコードより前にあることを示します
Cancel非同期メソッドの中で保留中のものをキャンセルします。
CloneRecordset オブジェクトの複製を作成します。複数のカレントレコードを保持したいときに使います。
Deleteカレントレコードを削除するときに使います。
EditModeカレントレコードに保留中の変更があるかどうかを調べるときに使います。
EOFカレントレコードの位置が最後のレコードより後にあることを示します。
FieldsRecordset が保持する Field のコレクション
Find指定した条件を満たす行を検索します。
GetRows複数のレコードを配列に取り込みます。
GetStringRecordset を文字列として返します。
Moveカレントレコードの位置を移動します。
MoveFirst最初のレコードに移動してそのレコードをカレントレコードにします。
MoveLast最後のレコードに移動してそのレコードをカレントレコードにします。
MoveNext次のレコードに移動してそのレコードをカレントレコードにします。
MovePrevious前のレコードに移動してそのレコードをカレントレコードにします。
OpenRecordsetを開きます
RecordCountRecordsetオブジェクト内のレコード数です。
RequeryRecordset を開くときのコマンドを再実行して、データソースからもう一度取得しなおします。
Resync再同期を行います
SaveRecordset をファイルまたは Stream オブジェクトに保存します。
SeekRecordset のインデックスを検索して、指定値と一致する行にすばやくカレントレコードを移動します。
Supportsレコードの追加、変更などが可能なレコードセットかを調べるときに使います。

Field オブジェクト
Nameフィールドの名前を取得します。
Typeフィールドの型が何かを取得できます。
OriginalValue変更が行われる前のこのフィールドの値です。
Valueこのフィールドの値です。
UnderlyingValueデータベース内のこのフィールドの値です。




おわりに今回は Win32OLE を利用して ADO を扱う方法について学びました。 ADO については紹介しきれない内容がまだまだあります。例えば、テーブル構造の変更などを行うには、ADOX という COM コンポーネントを必要とします。
しかしながら、今回紹介した内容が分かれば普通にデータベースを扱うには十分でしょう。
今の多くのシステムはデータベースと連携して動作します。 ADO を扱う今回紹介したようなやり方を知っていれば、さまざまなデータベースを扱うときに役に立つでしょう。
次回は、私の記事は一旦お休みして、しむらさんによる記事を掲載していただきます。
cuzic の記事としては次々回に Outlook でメールを読む、メールを送るといった内容を扱います。
どうぞ、お楽しみ。
(アドバイザー:arton、助田 雅紀)




参考文献



著者についてcuzic は、親指シフトキーボードを自在に操る Ruby プログラマです。
風邪を引いて寝込んだときは、フルーツを食べてあったかいものを飲んで過ごします。医者には行きません。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-8-1 08:20:07 | 显示全部楼层
【第 4 回】 Adobe Illustrator



プロローグあなたは Adobe Illustrator で図を書いているうちにマウス操作になにかもどかしさを感じるようになりました。
トラックボールでもこの思いに追従してくれることはありません。
この隔靴掻痒はペンタブレットでも全然変わりありません。
「座標なんて三角関数で計算すればいいのに。コンピュータだったら座標計算くらい機械がやれよ !」
そして、思いました。
「いったいどうすれば思ったように絵が画けるんだろう ?」




はじめに実はこのように思うのは少数派のようです。
はじめまして、志村 (hs9587) と申します。
Ruby やなにかプログラミング関係のところにいったとき、自己紹介の機会があると次のように言います。ちょっとウケます。
「Adobe Illustrator を Ruby で自動操作しています」
ということで今回の記事を書くことになりました。




今回の目的
  • Adobe Illustrator のオブジェクトモデルの特徴
  • Win32OLE としての Adobe Illustrator の操作
  • 「Hello, World !」サンプルスクリプト
あたりまえのことですが、IE や Microsoft Office シリーズだけが Windows アプリケーションではありません。 Adobe Illustrator (Windows 版) だって立派な Windows アプリケーションであり、ちゃんと OLE 対応です。
今回の記事では、これまでの cuzic さんの記事とおなじように Win32OLE を使った Illustrator のオートメーションを紹介していきます。
Adobe Illustrator 自体にはさほど親しんでない読者のみなさんも多いと思いますので、すこし Illustrator そのものの説明も交えながら進めていくことにします。そのため、この記事ではあまり深く立ち入ったところまでは説明できませんし、普段 Illustrator をあつかう上で常識的なことを省略することもあります。もう一歩先のこと、さらにオブジェクトモデルの詳細や、いろいろなテクニックについては、 JavaScript や、AppleScript、VB による自動操作の頁を参考にしてください (参考リンクを最後にまとめています)。

使用環境についての注意Adobe Illustrator は CS 版にバージョンアップしました。そろそろ CS2 も発表されていますが、僕は 10.0.3 のままです (2005 年 5 月 8 日時点で CS2 英語版が発表済み)。この記事では Illustrator 10 のまま話を続けます。
IllustratorCS自動化作戦with JavaScript http://www.openspc2.org/book/IllustratorCS/ によれば、CS 版とはそれなりに相違があるそうです。今回の記事は原理的なものですので、大きな影響はないものと考えます。
RubyActiveScriptRuby を使っています。1.8 から Win32OLE が標準添付になったのはうれしいです。とはいっても、基本的に Ruby 自体に関わるような記述は多くないので、そちらの方の環境はさほど気にすることはないでしょう。

今回の目次
  • プロローグ
  • はじめに
  • 今回の目的
    • 使用環境についての注意
    • 今回の目次
  • Adobe Illustrator アプリケーションとの接続
  • 線と字「Hello, World !」
    • ドキュメントとレイヤー
    • パスとパスポイント
    • テキストアート
  • Adobe Illustrator のオブジェクトモデル
    • アプリケーションオブジェクト
      • Application の代表的なプロパティとメソッド
    • ページアイテムと各種描画アイテム
      • 移動やサイズ変更
      • GroupItem の構成要素へのアクセス
    • コレクション類
    • 直接生成すべきオブジェクト
  • 単位と位置
      • 右(左)手系の図
    • 2.834645 points = 1 millimeter
    • 三種の境界位置
  • カラー
    • 色空間: CMYK と RGB
    • カラーオブジェクト
    • カラーの変更
  • 文字とフォント
    • 文字列と文字
    • フォントの指定
  • アクションと DoScript
    • 終わるのを待って
  • ファイル操作と画像形式
    • 新規作成とファイル読みこみ
    • 保存とエクスポート
  • スクリプトメニューからの実行
    • スクリプトメニューから DOS 窓なしで
  • 問い合わせメッセージボックス
    • 「Hello, World !」いろいろ
  • まとめ (おわりに)
  • エピローグ
  • 参考
  • 著者について




Adobe Illustrator アプリケーションとの接続Adobe Illustrator の ProgID は、 'Illustrator.Application' になります。ですから、Illustrator の OLEオブジェクトはこうやって作ることになります。
require 'win32ole' #illu = WIN32OLE.new('Illustrator.Application') illu = WIN32OLE.connect('Illustrator.Application') module Illu; end WIN32OLE::const_load(illu, Illu)ここで、Illustrator はすでに起動していないとうまくいかないようです。起動できずにエラーになったり (Windows98SE、95 系)、起動するもののその後の動作が不特定になることがあります (Windows2000、NT 系)。 (.new('Illustrator.Application') の場合)
また、.new で新規に起動した場合、Illustrator 自体が Visible になりません (デフォルト動作)。 Visible 属性自体、リードオンリーなので操作できませんし、気がつかないと Illustrator プロセスが残ったままになります。不可視属性の Illustrator プロセスが残っていると、普通の Illustrator の手動操作でも、パレット類が現れないなどの不都合があり、いろいろ問題です。
そういうことですので、慣れないうちは OLEオブジェクトの作成には、 Illustrator は別途起動しておいた上で .connect でおこなうのが安全です。




線と字「Hello, World !」とりあえず、「Hello, World !」にしましょう。 Illustrator なのに文字だけというのも芸がないので、ちょっと線も引いてみます。
helloworld.rb    1|require 'win32ole'   2|illu = WIN32OLE.connect('Illustrator.Application')   3|module Illu; end   4|WIN32OLE::const_load(illu, Illu)   5|   6|doc = illu.Documents.Add   7|   8|rect = doc.PathItems.Rectangle(500+20,200-5, 192,36)   9|rect.Filled = false  10|  11|path = doc.PathItems.Add  12|pt1, pt2 = path.PathPoints.Add, path.PathPoints.Add  13|pt1.Anchor, pt2.Anchor = [200-5,500+20], [200-5+192,500+20-36]  14|pt1.LeftDirection, pt1.RIghtDirection = [pt1.Anchor]*2  15|pt2.LeftDirection, pt2.RIghtDirection = [pt2.Anchor]*2  16|  17|text = doc.TextArtItems.Add  18|text.Position = [200, 500]  19|text.Contents = 'Hello,World!'  20|text.TextRange.Size = 32  21|  22|print 'PRESS ENTER'; gets  23|  24|doc.Close(Illu::AiDoNotSaveChanges)
紙の真ん中あたりに「Hello, world !」と、それを囲んで四角い枠と対角線一本。アートボード (紙) の大きさを指定してるわけではないので、ふだんの使用状況によってはちょっとずれるかもしれません。 helloworld.pdf
せっかくなので、順に説明しましょう
require 'win32ole' illu = WIN32OLE.connect('Illustrator.Application') module Illu; end WIN32OLE::const_load(illu, Illu)前述のとおり、.connect で Applicationオブジェクトを作ります。 Illu モジュールには Illustrator で定義されている定数をロードします。 (定数一覧)
doc = illu.Documents.Add文書 (アートボード) を作ります。 Illustrator では、この描画可能領域ののことをアートボード (The artboard) といいます。厳密には、文書 (Documentオブジェクト) とは違うものなのかもしれませんが、この記事ではとくに区別せず、今後は単に文書ということにします。
rect = doc.PathItems.Rectangle(500+20,200-5, 192,36) rect.Filled = false四角を書いて、中の塗りは なしにします。座標は決め打ち。線画の類は各文書の PathItemsプロパティに依頼するわけです。
path = doc.PathItems.Add pt1, pt2 = path.PathPoints.Add, path.PathPoints.Addさっきの四角の対角線です。まず無内容の線 (PathItem) をつくり、頂点を二つ加えます。
pt1.Anchor, pt2.Anchor = [200-5,500+20], [200-5+192,500+20-36] pt1.LeftDirection, pt1.RightDirection = [pt1.Anchor]*2 pt2.LeftDirection, pt2.RightDirection = [pt2.Anchor]*2そして二つの頂点の座標を指定します。左右の Direction を指定しないでおくと面白いです、初期値があるんでしょう、なんだかとっちらかった方向に曲がっていきます。 (先ほどと画像の倍率は変えています) helloworld_hen1.pdf (どっちにどのくらい曲がるかは環境や履歴によってまちまちです)
text = doc.TextArtItems.Add text.Position = [200, 500] text.Contents = 'Hello,World!' text.TextRange.Size = 32文字を配置するのは TextArtItem です。フォントサイズの指定には、さらに TextRangeオブジェクトを呼びます。
print 'PRESS ENTER'; getsRuby の入力待ち。コマンドラインで「Enter」キーを押すまで待ってます。その間に Illustrator にできた「Hello world !」文書に触ってみてください。
注意、こらからも全般にそうですが描画中に不用意に手を出すと止まってしまいます。描画中に触るのは、Window 枠とスクロールバー、拡大縮小率ポップアップ選択くらいにしておきましょう
doc.Close(Illu::AiDoNotSaveChanges)とくに文書は保存せずに終了します。
いかがでしょうか。「Hello world !」ごときにちょっと長かったかもしれませんが、すこし様子がつかめたのではないかと思います。
Illustrator のオブジェクトモデルなどの一般論を説明する前に、ここに出てきたいくつかのオブジェクトについて説明しましょう。その前にドキュメントとレイヤーの話しもしたほうがいいですね。

ドキュメントとレイヤーAdobe Illustrator での文書の取りあつかいは Documentオブジェクト単位になります。ちなみに、ページというのはないです ( 1 ページのものしかできません)。
Documentオブジェクトにはいくつかのレイヤー (Layer) という層があり、それらが積み重なってひとつの文書になります。いろいろな図形を書くのはどこか一枚のレイヤーです、文書に直接配置するわけではありません。レイヤーを特定せずに文書になにか描画アイテムを作ると、 ActiveLayer といわれるそのときフォーカスのあるレイヤーに配置されます。
doc.Layers.item(1)
doc.Layers.item(2)
……
doc.Layers.item(doc.Layers.Count)
(doc は「Hello, World !」サンプルのときと同じく、Illustrator の Documentオブジェクトとしています)
doc.ActiveLayer ≒ doc.Layers.item(doc.Layers.Index(doc.ActiveLayer))この「≒」で前者と後者は、Ruby の Win32OLEオブジェクトとしては違う (オブジェクトIDが異なる) ものですが、 Illustrator の方では同じものを指します。 .item() とか .Index() などのメソッドについてはまたあとで説明します。

パスとパスポイント線画は、文書やそのレイヤーの PathItemsプロパティから取得できます。既存のものはコレクションのメンバーとして、新規のものは Addメソッドで作成します。直線、ベジェ曲線、各種図形などの線画は PathItemオブジェクトだというわけです。
線を引くということは、PathItemオブジェクトに頂点 (PathPointオブジェクト) を追加するということになります。サンプルのように、まず空の PathItemオブジェクトを作り、それにいくつかの頂点をつけ加えていろいろ線画を作ります。頂点の追加は、PathItemオブジェクトの PathPointsプロパティに依頼したわけです。その頂点の状態や左右の方向性を設定することで、直線やベジェ曲線を形づくります
PathItemオブジェクトには線の状態を設定する以外に、内部の塗りの設定もあります。
説明
Closed閉パスかどうか
Stroked線の表示非表示
StrokeWidth線の巾
StrokeColor線の色
……点線の設定、頂点や端の処理など
……その他オーバープリントの設定など
塗り説明
Filled中が塗られているか
FillColor塗りの色
……その他オーバープリントの設定など

テキストアート文字を描くのは TextArtItem です。このオブジェクトを作るには、いつものように文書やレイヤーの TextArtItems に依頼します。
TextArtItemオブジェクトの Contentsプロパティが表示文字列になります。もし改行するなら、Windows であっても "\r" を使ってください。文字の大きさやそのほかの属性を設定するには、Contents に入っている文字ではなく、また別のオブジェクトを呼びます。 TextArtItemオブジェクトの TextRangeメソッドで得られる TextRangeオブジェクトです。
TextRangeオブジェクトにはフォント関係のプロパティもあり、それでフォントやサイズの指定もできます。




Adobe Illustrator のオブジェクトモデル「Hello, World !」サンプルとその説明をしてきましたが、 Adobe Illustrator (OLE) のオブジェクトの様子がすこし分かったでしょうか。この節では、もうすこし一般的に Illustrator のオブジェクトモデルを見ていこうと思います。
その前にすこし注意しておきます。無理もないことなのですが以下に説明するオブジェクトで、そのメソッドやプロパティへの代入 (Rubyではこれもメソッドですね) の多くは値 (self) を返してくれません。 Ruby の言葉遣いでいえば、破壊的なメソッドのように振舞います。メソッド呼び出しの連鎖や式の値を return するときなど、いつもの Ruby のつもりで記述すると意に沿わない結果になることがあります。
ちょっと、移動と回転のメソッドを例に取ります (各メソッドについてはあとで説明します)。
# 「Hello, World !」サンプルの四角を移動して回転 # rect = doc.PathItems.item(2) # of helloworld.rb rect.Translate(50, 40)         # deltaX and deltaY rect.Rotate(30)                # degree # rect.Translate(10, 20).Rotate(30) #=> NoMethodError: undefined method `Rotate' for nil:NilClass 例のようなメソッドの連鎖はできないのですが、 doc.TextArtItems.item(1).Contents は、"Hello,World!" という文字列を返します。このような、プロパティ参照の連鎖なら大丈夫です

アプリケーションオブジェクト今回、Win32OLEオブジェクトとして最初に作ったのが Applicationオブジェクトです。 Adobe Illustrator オートメーションの基本になります。あつかう文書類へのアクセスには Documentsプロパティや ActiveDocumentプロパティを用いることになります。また、ファイルを開くのもこのオブジェクトの Open(<ファイル名>)メソッドです。
それ以外にもアプリケーション情報全般 (バージョン等) や、 Selection という文書内の選択アイテムの配列、そのとき使用可能なフォントのコレクション (TextFaces) があります。
それから、Illustrator 自体のアクション類や、別の JavaScript を実行することができます (DoScript, DoJavaScript, DoJavaFile)。各種 Illustrator アクションやパレットからの操作などには、スクリプトからは呼べないものも多いので、こうやってアクションを呼ぶのはすごく便利です(またあとで説明します)。
最後に、各描画アイテムの線型変換用の行列の演算もここで提供されています。
Application の代表的なプロパティとメソッドプロパティ
名前R/O説明
Documents文書のコレクション
ActiveDocumentカレント文書
Selectionカレント文書の選択アイテムの配列
TextFacesR/O使用できるフォント
VersionR/Oバージョン
ActionIsRunningR/Oアクションの実行中かどうか
VisibleR/O可視属性、代入不可
メソッド
名前説明
Openファイルを開く
QuitIllustrator を終了する
Redraw全体を描画しなおす
DoScriptアクションの実行
DoJavaScriptJavaScript (文字列) の実行
DoJavaFileJavaScriptファイルの実行
IsSingularMatrix正則行列じゃないかどうか
……行列関係ほか

ページアイテムと各種描画アイテムAdobe Illustrator の描画アイテム (オブジェクト) にはいくつか種類があります (「Hello, World !」サンプルでは、PathItem と TextArtItem が出てきました)。それぞれの描画アイテムは、各文書や前述のレイヤーのプロパティとして提供される種類ごとのコレクションに属しています、あるいは各コレクションの Addメソッドで生成されます。
代表的な描画アイテム(オブジェクト)
描画アイテム説明
PageItem全部の種類を代表する
PathItem線画類
TextArtItemテキストの配置
GroupItem各種描画アイテムのグルーピング
……(外部)画像データ、データ連携、複合パス、ほかいろいろ
個々の描画アイテムへのアクセスは何通りもあります。たとえば PathItemオブジェクトの場合。
  • 文書 (Documentオブジェクト) の PathItemsプロパティの所要の要素
  • 文書の適切なレイヤー (Layerオブジェクト) の PathItemsプロパティの所要の要素
  • 所属する GroupItem の PathItemsプロパティの所要の要素
  • なにかの PageItemsプロパティの所要の PageItemオブジェクトの PathItemプロパティ(R/O)
    • 要件が PathItem とは違う種類の描画オブジェクトのとき、これはエラーになります
# 「Hello, World !」サンプルで作った対角線  # PathItems doc.PathItems.item(1)  # Layer経由 doc.Layers(1).PathItems.item(1) doc.ActiveLayer.PathItems.item(1)  # 次々節のグループ化をしたら doc.GroupItems.item(1).PathItems.item(1)  # PageItems # 次々節のグループ化の前なら doc.PageItems.item(1).PathItem # 次々節でグループ化した後だとすこしずれます doc.PageItems.item(1).GroupItem.PageItems.item(1).PathItem最後の方法についてはちょっと注意が必要です。 PageItemオブジェクトは、全ての描画オブジェクトを代表しますが、あまり多態的ではありません。その PageItemTypeプロパティを調べ、それに応じて所要のプロパティを呼び出す必要があります。
path = doc.PageItem.item(1).PathItem if doc.PageItem.item(1).PageItemType == 5 # Illu::AiPathItemPageItemType には、AiPageItemType 定数に相当する整数値が返ります。その代表的な値は次のようになります。詳しくは Adobe の資料PDFを見てください。 (参考リンクにあります)
AiPageItemType (代表的なもの)説明
AiCompoundPathItem = 1複合パス
AiGroupItem = 3描画アイテムのグループ
AiPathItem = 5パス
AiRasterItem = 8画像
AiTextArtItem = 10テキスト
移動やサイズ変更PageItem をはじめ、各描画アイテムには移動やサイズ変更のメソッドが用意されています。返り値はない (selfを返さない) ので注意してください。
メソッド引数の説明
Resize(scaleX, scaleY)x,y方向の拡大縮小(百分率)
Rotate(angle)回転角度(度)
Transform(Matrix)別に定義した変換行列での一次変換
Translate(deltaX, deltaY)移動、ポイント単位
引数にはほかにも線や塗りのパターン自体も拡大するかどうかなど細かい引数が設定できます。詳しくは Adobe の資料 PDF を見てください。
回転に関してはもうひとつ注意があります。回転の中心はその描画アイテムを囲む長方形の中心です (GeometricBounds)。図形の中心 (重心) でもなければ、原点でもありません。ひとつ回転するだけならまだしも、いくつかのアイテムを同時に回転するときにはとくに注意してください。あらかじめ回転中心を計算しておくか、次節のように GroupItem に一まとめにしてから回転させるのがいいでしょう。
とくに説明しませんが、任意の点を中心に回転させるように計算しておく例をあげておきます。 rotate.rb
GroupItem の構成要素へのアクセス任意の種類の描画アイテムをひとまとめにあつかうのが GroupItemオブジェクトです。構成要素へのアクセスは、また所要のコレクション (PathItemsプロパティ、TextArtItemsプロパティなど) 経由になります。
グループに構成要素を追加するには、各描画アイテムの MoveToBeginning(Document/Layer/GroupItem)、MoveToEnd(Document/Layer/GroupItem) メソッドを使います。 Cut/Copy/Pasteメソッドだと位置が画面中央に移動してしまうので、それらを使ってグループ化するのはやめた方がいいでしょう。
# 「Hello, World !」サンプルの四角と対角線をグループ化 # rect = doc.PathItems.item(2); path = doc.PathItems.item(1) # of helloworld.rb grp = doc.GroupItems.Add rect.MoveToBeginning(grp) path.MoveToBeginning(grp)  # グループを回転、反転 grp.Rotate(90) grp.Resize(-100, 100)
コレクション類以上みてきたように、各描画アイテムへのアクセスは、上位オブジェクトの種類別コレクションプロパティ経由になります。
それらのコレクションはちょっと非力です、すくなくとも、 Ruby の配列やハッシュに慣れていると少々不便に感じます。 AppleScript、JavaScript、VB といった言語で共通に使える機能というと、どうしても限られたものになってしまうのかもしれません。
コレクションのインデックスは 1 からです (ゼロはじまりではないです)。
代表的なもの説明
Countコレクションの要素の数
Add新規描画アイテムの生成
Index(<アイテム>)所要のアイテムがコレクションの何番目か
item(key)コレクション key番目の描画アイテム
Remove, RemoveAll描画オブジェクトのコレクションからの削除
……ほかに親オブジェクトとか、所属文書など
……ものによっては、図形の追加など
コレクションのインデックスについてもう一度説明します。描画アイテムの重なりの一番上が 1 、下に向かって増えていきます。 Ruby の WIN32OLEオブジェクトは eachメソッドを与えてくれますが、この実行中でも番号は重なり順番に即座に反映されます。上下の配置を変更したり、Remove のときなどは気をつけないと変なことになります。
そういうときは逆順に実行した方がいいでしょう。 WIN32OLE に reverse_each はないので、自分で作らないといけないです。

直接生成すべきオブジェクトここまで、Applicationオブジェクトから派生するいろいろなオブジェクトとコレクションを見て来ました。ところで、Adobe Illustrator オートメーションでは、それら以外にも WIN32OLE.new で直接生成すべきオブジェクトがあります。カラー関係と、ファイルオプション、変換行列の生成です。
直接生成すべきもので代表的なものは次のようになります。それぞれの ProgID は 'Illustrator.<オブジェクト名称>' になります。
  • Color関係
    • Color
    • CMYKColor
    • GrayColor
    • RGBColor
    • 効果関係カラーほか
  • ファイル書きだしオプション
    • IllustratorSaveOptions
    • PDFSaveOptions
    • ExportOptionsPhotoshop
    • EPS, GIF, JPEG, PNG8, PNG24, SVG のオプション
  • ファイル読みこみオプション
    • OpenOptionsPDF (PDF 文書のなかの読みこむページを指定します)
  • 変換行列
    • Matrix
Illustrator 起動中であっても それぞれのオブジェクトは個別に稼動しているわけではありません。 .connect ではなくて .new を使って生成します。
cmyk = WIN32OLE.new('Illustrator.CMYKColor') cmyk.Cyan, cmyk.Magenta, cmyk.Yellow, cmyk.Black = 0,100,0,0  # マゼンタ



単位と位置今まで説明していませんでしたが、Adobe Illustrator の描画領域の座標システムは、アートボード(文書)の左下隅を原点にした右手系です。 単位はポイント。
コンピュータのグラフィックでは、左上を原点にする左手系も多いので、混乱しないよう注意してください。
その座標系で、各描画アイテムの位置指定 (Position) は、左上の座標です。描画物を囲む長方形は左上点と右下点の座標で指定されます (GeometricBounds, VisibleBounds, ControlBounds の 3 種があります)。
右 (左) 手系の図この図も Ruby で描いています。 right(left)system.rb 一応ソースもありますが、私製のライブラリや Illustratorアクションを使っているので分かりにくいですね、雰囲気だけでも伝わるでしょうか。私製ファイル類へのリンクは最後にあげておきます。

2.834645 points = 1 millimeterAdobe Illustrator スクリプトでの座標や、位置指定、サイズなどは基本的にポイント単位です。ほかの単位にすることはできませんので、事前に換算して設定することになります。
単位換算表
センチ28.346 points = 1 centimeter
インチ72 points = 1 inch
ミリ2.834645 points = 1 millimeter
パイカ12 points = 1 pica
Q (級)0.709 point = 1 Q ( 1 Q = 0.25 millimeter)
表の記述は、記事末の参考リンクにあげた Adobe のドキュメントに依る。 Adobe のドキュメントでは、今 (2005/12/10現在、CS2版のもの) に至るまで Q の millimeter 換算値が 0.23 と間違っているので注意して下さい。また、記載値の有効桁数なども Adobe ドキュメントに依拠した。
付記: Q の数値について、読者の方から間違いとのご指摘がありましたので、修正しました。ご指摘まことにありがとうございます。 2005年12月吉日 志村 弘之・Rubyist Magazine 編集部

三種の境界位置各描画アイテムの描画領域は三種類の長方形で示されます。
GeometricBounds 線巾 (StrokeWidth) がないものとして囲んだ長方形 VisibleBounds 線巾 (StrokeWidth) 込みで囲んだ場合の長方形 ControlBounds ベジェの接線指定などのコントロールポイントを含めてのもの 各長方形は、左上と右下の座標を並べた 4つの数値 (の配列) で代表されます。とくに、描画アイテムの位置 (Position) 自体は GeometricBounds の左上座標になります。




カラーこのあたりのあつかいが、CS 版になって大きく変わったところだそうです。 Colorオブジェクトを経由しないようになったということですので、 CS 版をお使いの場合はご注意ください。

色空間: CMYK と RGBAdobe Illustrator であつかう色の体系には、大きく分けて印刷用の「CMYK」と画面表示用の「RGB」があります (色空間といいます)。文書を最初に作るとき (Documents.Add) に指定するのですが、一旦文書を作ると変更できないです。また、文書内の描画アイテムの色指定は文書自体の色空間に反するものは設定できません。
CMYKAiDocumentCMYKColor = 2印刷用 4色
RGBAiDocumentRGBColor = 1画面用 3色
描画アイテムの色指定には、灰色やほかのもあり、必要に応じて使い分けることになります。
  • ほかの色 (Gradient, Pattern, Spot) の適性はまだ確かめていません。CMYKColor、RGBColor 両方との相性も考えると組み合わせも多くなります。どうなっているかは、読者のみなさんで試してみてください。

カラーオブジェクト各描画アイテムの色は付属の Colorオブジェクトになります。 PathItem であれば、StrokeColorプロパティと FillColorプロパティということになるわけです。
その Colorオブジェクトの Colorプロパティ (R/O リードオンリー) はカラーの種類です。 (CMYK, Gradient, Gray, Pattern, RGB, Spot)。この値にしたがって、それぞれの種類のカラーオブジェクトを設定することになります。
AiColor enumeration
AiColorNone = 0
AiColorCMYK = 1
AiColorGray = 2
AiColorRGB =3
AiColorSpot =4
AiColorPattern = 5
AiColorGradient = 6
前にも説明したように、設定する Colorオブジェクト類は Applicationオブジェクトから生成するのではなく、直接生成すべきオブジェクトです、ご注意ください。

カラーの変更描画アイテムの Color 系プロパティ (オブジェクト) の Colorプロパティは R/O です、変更できません。 Colorオブジェクトの各色プロパティ (CMYK, Gray, RGB など) に直接生成した各色のオブジェクトを代入します (文書の色空間に矛盾する代入はできません)。
それでは「Hello, World !」サンプルの PathItemオブジェクトの線に色をつけることを考えましょう。
# path = doc.PathItems.item(1) # of helloworld.rb cmyk = WIN32OLE.new('Illustrator.CMYKColor') cmyk.Cyan, cmyk.Magenta, cmyk.Yellow, cmyk.Black = 0,100,0,0  # マゼンタ path.StrokeColor.CMYK = cmyk CMYKColorオブジェクトは、WIN32OLE.new で直接生成します。そして各色の値を設定します、値の範囲は、CMYK、Gray は 0~100.0、RGB では 0~255.0 です、この範囲が違うことにも注意します。




文字とフォント前にも書きましたが、テキスト文字列内で改行するには "\r" を使います、"\n" ではないです。

文字列と文字TextArtItem の文字の属性は、TextRangeメソッドで部分文字列や一文字ずつの TextRangeオブジェクトを指定して、個別に色や大きさ、フォントを指定します。 (一文字を指定する Charactorオブジェクトもあります)
メソッド引数の説明
TextRange(rangeStart, rangeEnd)初めと終わりの文字位置
引数は、TextRange として取り出したい部分文字列の位置 (はじめからの文字数) です。省略すると全文字列になります。文字位置の最初は 1 です (ゼロではないです)、また、2バイト文字も 1文字とかぞえます。

フォントの指定TextArtItem の TextRangeメソッドで、適用すべきテキストの範囲を選び (設定し)、その TextRangeオブジェクトの、Fontプロパティにフォント名称を指定します。名称は基本的に英字アルファベットになります、 MS-PGothic、MS-PMincho、MS-Gothic、MS-Mincho などです。
そこで使用できるフォント名称は、Application.TextFaces で探すことになります。
require 'win32ole' illu = WIN32OLE.connect('Illustrator.Application') illu.TextFaces.each{ |textface| puts textface.Name }ではちょっと TextRange を使ってみましょう。「Hello, World !」サンプルの文字色やフォントを変えます。
# text = doc.TextArtItems.item(1) # of helloworld.rb cmyk = WIN32OLE.new('Illustrator.CMYKColor') cmyk.Cyan, cmyk.Magenta, cmyk.Yellow, cmyk.Black = 100,0,0,0  # シアン  hello = text.TextRange(1,5)       # Hello のところ hello.FillColor.CMYK = cmyk       # 色をつける  world = text.TextRange(7,11)      # World のところ world.Font = 'MS-PMincho'         # フォントを変える



アクションと DoScriptスクリプトの作業では、「矢印を引く (パスを矢印にする)」、「パスファインダで交点を求める」のようなパレット・メニューやアクションの実行で得られる機能を全部実現してやることはできません。そのようなときは、あらかじめアクションに登録しておいて、 Applicationオブジェクトの DoScriptメソッドでそのアクションを呼ぶことになります。
その際には、アクション類は選択されたオブジェクトに作用することも多いので、アクティブドキュメントの Selectionプロパティとのやり取りも必要になってきます。また、アクションの実行中に次の処理が始まってはよくないので、そのアクションが終わるのをきちんと待ってないといけません。
「Hello, World !」サンプルを使って、アクションの実行をしてみましょう。アクションの実行には、アクションの名前と、所属するアクションのセットの名前を両方指定します。 '初期設定アクション' は、Illustrator に最初からはいっているアクション集 (セット) です。
# 「Hello, World !」サンプルの対角線をひっくり返す # path = doc.PathItems.item(1) # of helloworld.rb path.Selected = true illu.DoScript('水平にリフレクト','初期設定アクション')
終わるのを待ってアクションの実行には結構時間がかかることがあるので、そこできちんと待たないと期待する動作をしないことになります。たとえば、アクションの実行が終る前に必要なオブジェクトの選択を解除してしまってはいけないですね。それには Applicationオブジェクトの ActionIsRunningプロパティでチェックします。
# 「Hello, World !」サンプルの対角線をひっくり返す、その後選択解除 # path = doc.PathItems.item(1) # of helloworld.rb path.Selected = true illu.DoScript('水平にリフレクト','初期設定アクション') sleep 0.23 while illu.ActionIsRunning  # 0.23に深い意味はない doc.Selection = []                     # 選択解除(本当は、アクション実行用の選択の前にも全選択の解除をしておいた方がいいです)




ファイル操作と画像形式Adobe Illustrator のファイル操作です。文書をファイルに書きだすときにはいろいろなファイル形式を選べます、 Illustrator形式 (拡張子 .ai)、 PDF、Photoshop、 EPS、GIF、JPEG、PNG8、PNG24、SVG です。いちど読みこんでからファイル形式を指定して書きだせば、画像形式の変換にもなります。

新規作成とファイル読みこみいままでにもありましたが、文書を新規作成するのは、Documentsコレクションオブジェクトの Addメソッドです、文書の色空間や大きさも指定できます。
メソッド引数の説明
Add(<色空間>, width, height)色空間を指定する整数値と、巾、高さ
色空間については前に説明しました、CMYK なら 2、RGB なら 1 を指定します、巾と高さはポイント単位。引数は省略できます、そのときは履歴やふだんの使用にあわせて既定値がはいります。
ファイルを開くのは、Applicationオブジェクトの Openメソッドです。
メソッド引数の説明
Open(<ファイル名>, <オプション>)ファイル名と、色空間か PDFOpenOptions
画像形式は、拡張子などから Illustrator が自動判別します、色空間も大体それで決まります。オプションは省略可能、読み込むファイルの名前の指定はフルパス名が安心です。
PDF を読みこむときには何ページ目を読むかを指定します。 PDF 文書はたくさんページがあることが多いのですが、Illustrator は 1ページのものしかあつかうことができません。そのためどのページを読むかの指定が必要になります。 (省略すると最初のページです)
openpdf = WIN32OLE.new('Illustrator.PDFOpenOptions') openpdf.PageToOpen = 7 # ページ指定
保存とエクスポート文書の書きだしには二種類のメソッドを使います。 Documentオブジェクトの SaveAsメソッド (Illustrator 形式、EPS、PDF)、Exportメソッド (それ以外) の二つです。こころみに、先ほどの「Hello, World !」サンプルの画像を PNG 形式と PDF に書きだしてみましょう。
png8export = WIN32OLE.new('Illustrator.ExportOptionsPNG8') fname = File.join(Dir.pwd, 'helloworld8.png') doc.Export(fname, Illu::AiPNG8, png8export)  pdfsave = WIN32OLE.new('Illustrator.PDFSaveOptions') fname = File.join(Dir.pwd, 'helloworld.pdf') fname.gsub!(File::SEPARATOR, File::ALT_SEPARATOR) if File::ALT_SEPARATOR doc.SaveAs(fname, pdfsave) helloworld.pdf helloworld2png.rb
書きだし先のファイル名はフルパス名 (ドライブレター付き) です。 Exportメソッドでは、画像形式を指定しておく必要がありますが、そのときオプションは省略できます。とくに SaveAsメソッドでは、ファイルパス名の区切り文字にも注意してください。
AiExportType
AiJPEG = 1
AiPhotoshop = 2
AiSVG = 3
AiPNG8 = 4
AiPNG24 = 5




スクリプトメニューからの実行今まで書いてきた rubyスクリプトは Adobe Illustrator の外から実行していました (コマンドプロンプト等)。それだけではなくて、Illustrator のスクリプトメニューからの呼出しもできると便利そうです。しかし、Illustrator (Windows版) は、拡張子 .vbs、.js、.exe しか認識しません。それでもなんとか実行したいですね、WScript.Shell.Run や WScript.Shell.Exec によって rubyスクリプトを呼び出すだけの VBScriptを書いてやります (拡張子 .vbs)。 (べつに、JavaScript でもいいです)
Dim WshShell, oExec Set WshShell = CreateObject("WScript.Shell") Set oExec = WshShell.Exec( <rubyスクリプトのフルパス名> )または
Dim WshShell Set WshShell = CreateObject("WScript.Shell") WshShell.Run( <rubyスクリプトのフルパス名> )Execメソッドは、標準入出力などと相互作用できて便利なのですが、 WSH (Windows Scripting Host) のバージョン によってはありません。ないときは Runメソッドを使います。
あるいは exerb によって .exe 実行ファイルにするのもいいです。
さて、Illustrator の [ファイル]-[スクリプト]-[参照] メニューでの初めに参照するフォルダは (標準的なインストールでは)
C:\Program Files\Adobe\Illustrator 10.0.3\Support Files\Contents\Windows\になります、そのあたりかサブフォルダを作っていれてやればいいでしょう。ここのフォルダには、.vbs、.js、.exe へのショートカット を置いても参照されます。
また、[ファイル]-[スクリプト]-メニューから直接呼び出せるようにするには、
C:\Program Files\Adobe\Illustrator 10.0.3\プリセット\スクリプト\にそのファイルを置きます (標準的なインストールの場合)。ここはショートカットを置いてもダメです。実際にこのメニューに登録するには、Illustrator を再起動する必要があることに注意します。

スクリプトメニューから DOS 窓なしでこれでスクリプトメニューからの呼び出しができるようになります。そうは言っても、.vbsファイルからの呼び出しのたびに DOS 窓が現れるのを煩わしく感じる場合もあるでしょう。そのときは、.vbsファイルでの呼び出しを次のようにするといいです。 '""' は、VBScript文字列での '"' のエスケープです。また rubyスクリプトの指定もフルパス名が安心です。
WshShell.Run( "WScript.exe ""<rubyスクリプトのフルパス名>"" ")



問い合わせメッセージボックスさて、スクリプトの実行中になにかメッセージを表示したいことがあります。それから終了メッセージも表示できるといいですね。 Adobe Illustrator 自体のスクリプトには用意されていない (OLE) ので、そのようなメッセージボックスは別の手段で実現することになります。おおむね次のような手段があると思います。
  • Rubyの (添付) ライブラリ
    • VisualuRuby などの GUIライブラリを使用する
    • Win32API で Windows のメッセージボックスやダイアローグを直接操作
  • WScript.Shell の Popup
    • やはり Win32OLE で WScript.Shellオブジェクトを呼んで、その Popupメッセージ
  • JavaScript の alertメソッド
    • DoJavaScript で JavaScript の alertメソッドを実行
require 'vr/vruby' frm = VRLocalScreen.newform frm.messageBox('メッセージ', 'Title', 0 + 48 + 0x40000) wsh = WIN32OLE.new('WScript.Shell') wsh.Popup('メッセージ', 0, 'Title', 0 + 48 + 0x40000) illu.DoJavaScript('alert("メッセージ");')DoJavaScript での alert は、Illustrator 自体の機能を使うのはいいのですが、 alert 自体の返り値がないのはすこし不便です。 WScript.Shell の Popup と、VisualuRuby の messageBox は、結局同じものを呼んでるので大きな相違はないです。 [OK][Cancel] や [Yes][No] ボタンの返り値も便利です (ボタン表示や返り値の詳細は、Windows の メッセージボックスAPI について参照してください)。 VisualuRuby やそのほかの Ruby の GUIライブラリを用いれば、いろいろきめ細かい応答画面が作成できます。

「Hello, World !」いろいろここまでのいろいろな操作を「Hello, World !」サンプルにつけ加えてみました。 helloworld_sample.pdf helloworld_sample.rb




まとめ (おわりに)いかがだったでしょうか。
今回は、ちょっと趣向をかえて、Adobe Illustrator をあつかうことを紹介しました。線を引いたり字を書いたり、ファイル操作や画像形式の変換など、基本的なことは説明できたと思います。でも、Adobe Illustrator での実作業では、ここでは説明できなかった内容がまだまだあります。プログラミングの範囲をこえ、デザインや組版の技術について考えさせられる事もたくさんあります。
しかしながら、今回紹介したような内容でも、なにかの手がかり、出発点になるのではないかと思います。
それから、事は Adobe 製品だけに限りません、もちろん Microsoft 関連だけでもありません。どんなアプリケーションソフトでも Ruby で自動化してみましょう (OLE対応)。
次回からはいつものように cuzic さんの記事に戻る予定です。
どうぞ、お楽しみ(僕も楽しみにしています)。




エピローグRuby活用事例集にもあげたのですが、下記のような図を描くのに使いました。ぜひご覧ください、もし手にはいるようなら実際に手に取っていただけるとうれしいです。
Ruby活用事例集




参考



著者について志村 (hs9587) は印刷会社に勤める (Web 系) プログラマーで、SF ファンです。時々 SF 系の折り紙を折ります。
あるときその折り紙の図が欲しいと頼まれました。後工程のイラストレーターさんやデザイナーさんのために、 Adobe Illustrator 形式のファイルが望ましいと言われました。
もう、Ruby でやるしかないと思いました。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-8-1 08:20:43 | 显示全部楼层
【第 5 回】 Outlook



プロローグ他部署からの一連のメールに対してデータベースを検索した結果を返すような業務はだいぶ楽になっていました。
それでもこう心の中でこうつぶやいていました。「検索したりするのはずっと快適になったんだけどなぁ」
「だけど」に隠された意味は、メールでやってきた要求から、コピーアンドペーストを繰り返したり、一件ずつメールで送信していったりするのは面倒だということです。
Ruby と Win32OLE の活用の仕方についてまだあまり知らなかったときは、ただ、我慢するしか方法がありませんでした。
しかし、今は我慢するのではなく、打開する手段があります。 Ruby と Win32OLE を使って。




はじめに前回は志村さんに代わってもらって Illustrator を Win32OLE で制御する話をお伝えしました。
今回はまた私 cuzic が筆をとって、あなたと一緒に Win32OLE の活用のしかたを学んでいきます。
今回は、Microsoft Office の中でのメールやスケジュール等を管理するアプリケーションソフト Outlook を Ruby で扱っていきます。
今回の記事では、Outlook の数多くある機能の中でも日常的な業務ではもはや欠かせない存在となった電子メールを扱う機能について紹介していきます。
今回の記事は次の構成になっています。
  • Ruby で電子メールを扱う方法について
  • Outlook のオブジェクトモデル
  • Ruby から Outlook を操作する
    • 受信トレイにあるメールを一覧表示する
    • 特定条件に合ったメールを検索する
    • メールのプロパティについて
    • 新規メールの編集・送信する
では、今回も一緒に学んでいきましょう。




Ruby で電子メールを扱う方法について今回では、Ruby で電子メールを送信・受信する方法について学びます。
Ruby で電子メールを扱う方法としては、直接 SMTP や POP などのメール送受信のプロトコルを扱う方法が、一番よく使われているのではないでしょうか。 Net::SMTP や Net::POP といったライブラリは、Ruby に標準添付されています。これらのライブラリは Unix でも Windows でも同じように扱え便利です。さらに TMail のようなライブラリを使えば、メールを非常に抽象的に扱え、電子メールクライアントを Ruby で書き上げることもできるかと思います。
この記事では、Net::POP や Net::SMTP については説明せず、Outlook を Win32OLE を使って、制御する方法について学びます。 Outlook を活用する方法の利点には次のようなものがあります。
  • メールの送信・受信などのプロトコルレベルの詳細を知らなくて良い
  • 過去の送信メール・受信メールの履歴について、統合的に管理できる。
  • メールの送信のときに、目視確認しながらメールを送信できる。
  • Outlook では、SMTP 以外の Exchange Server,Hotmail なども扱える。
重要なのは、3番目と4番目の理由でしょう。 Net::SMTP を使うと 自動的にメールを送信できます。しかしながら、メールの送信というのは失敗が許されにくい部分があります。なんらかの失敗があったかどうかをあらかじめ確認したいのが人情です。
また、一部に例外的な場合があって、人手で編集を加えてからメール送信するような運用になってしまわざるを得ない場合もあるでしょう。もちろんすべての例外的な場合も含めて実装を行うという方向性もあるわけですが、それだけの手間が必ずしも見合うものになるわけではありません。
多くの場合はそのまま送信するものの、一部のメールについては自分で確認して編集を加えたいような場合は、電子メールクライアントとして完全な機能を持ち編集などを簡単に行える Outlook を活用すると便利です。
また、SMTP 以外のプロトコルを扱えるということは大きなメリットです。会社のメールサーバーが一般的なSMTPポートを開放していず、わけのわからない Exchange Serverのインターフェイスしか見せていない、そんな状況でどうやって機械的なメール送信を行えばよいのでしょうか?そう、ローマのものはローマに、というではないですか。マイクロソフトのものはマイクロソフトで、つまりOutlookを使えばよいのです。
逆に Outlook を使うことによる欠点もあります。例えば次のようなものです。
  • メールの送信時やメールアドレスの取得時などに確認ダイアログが表示される。
  • 詳細な電子メールヘッダの情報を得ることが難しい (標準ではできない)。
  • Outlook はなんとなく嫌いで、これを使うのは負けた気がする。
確認ダイアログの表示については純粋に利便性の問題な場合もありますが、致命的な場合もあります。例えばサーバから直接メール送信させるような動作を行いたいときは、この点は致命的な問題になるでしょう。
現在、ウィルスメールなどがはびこっている現状からして、Outlook がメール送信時に確認ダイアログを表示するのはセキュリティ対策として妥当な措置ながら、利便性を大きく損なっていることは確かです。
2番目の項目、詳細な電子メールヘッダについては得ることは可能ですが、それは Outlook を標準でインストールしたときではできません。電子メールヘッダの取得に Collaboration Data Object という Outlook とは別の COM オブジェクトを使う必要があります。しかしながら非常に残念なことに、Collaboration Data Object を使用するには、標準インストールではダメで、追加インストールが必要です。
追加インストールなんて特に問題ではないという方にとっては、この問題は障害にはならないかもしれません。しかしながら管理者権限を持っていないパソコンでも使えるようにする必要がある場合は、難しくなります。
電子メールの送受信については、これらの利点や欠点を検討した後に、 Net::SMTP などを使うか、Outlook で実装するか、またそれ以外の方法を使うかを決定しましょう。




Outlook のオブジェクトモデルそれでは、この節では Outlook のオブジェクトモデルについて説明します。
まず、下記のクラス図を参照してください。
Outlook でよく使い、今回使用するオブジェクトは次のものです。 Search と Results は Outlook 2002 で追加されたオブジェクトになります。お手持ちの Outlook のバージョンによっては、使えないかもしれません。
  • Application
  • Inspector
  • NameSpace
  • Folders
  • MAPIFolder
  • Items
  • MailItem
  • Search
  • Results
順に説明していきましょう。
Application オブジェクトは、Outlook アプリケーションそのものを表現するオブジェクトになります。
Inspector オブジェクトは、Outlook のアイテムを表示するウィンドウを表現するオブジェクトです。
NameSpace オブジェクトは、任意のデータソースの抽象的なルートオブジェクトを表現します。受信トレイ等の既定フォルダへのアクセスにこのオブジェクトを利用します。現在のところ使用できる NameSpace は MAPI だけです。 Application オブジェクトの GetNameSpace("MAPI") メソッドを使用することで、NameSpace オブジェクトを取得できます。
Folders オブジェクトは Outlook のフォルダのコレクションのようなものです。この説明は正確さに欠けますが、こういうように理解してください。
MAPIFolder オブジェクトは Outlook のフォルダを表現します。 Outlook のフォルダは Outlook アイテムや他の Outlook のフォルダを含めることができます。そのため、MAPIFolder オブジェクトではその階層での Outlook のアイテムや Outlook のフォルダを取得できます。また、上位の階層の Outlook フォルダに移動することもできます。
Items コレクションは フォルダ内の Outlook アイテムオブジェクトのコレクションです。Outlook アイテムというのは、メール、予定、連絡先、仕事など、Outlook で扱うアイテムをさします。
MailItem オブジェクトはメール一通を表現するオブジェクトです。
Search オブジェクトは Outlook のアイテムに対して実行された個々の検索に関する情報を含みます。 Application オブジェクトの AdvancedSearch メソッドを使用したときにこのオブジェクトを利用します。
Results コレクションは Search オブジェクトと AdvancedSearch メソッドで取得されたデータおよび結果を格納します。
これらのオブジェクトは上記の図のような階層構造になっています。このオブジェクトモデルを理解すれば、Outlook アプリケーションを操作する方法についてすばやく学ぶことができるでしょう。




Ruby から Outlook を操作するさて、いよいよこれから Ruby から Outlook を操作する方法について学んでいきましょう。私が持っている Outlook のバージョンが 2002 のみですので、今回の記事では基本的に Outlook 2002 であることを前提に解説を行います。

受信トレイにあるメールを一覧表示するまずは、受信トレイにあるメールを一覧表示する方法について学びます。
次のスクリプトで、受信トレイにあるメールの件名を一覧表示します。
inbox.rb    1|require 'win32ole'   2|   3|def each_mail   4|  ol = WIN32OLE::connect("Outlook.Application")   5|  myNameSpace = ol.getNameSpace("MAPI")   6|  folder = myNameSpace.GetDefaultFolder(6)   7|  folder.Items.each do |mail|   8|    GC.start   9|    yield mail  10|  end  11|end  12|  13|each_mail do |mail|  14|  puts mail.Subject  15|end
Outlook に保存されたメールは次のようになります。
今回は関西 Ruby 勉強会のメーリングリストの履歴をメールのサンプルとして使用しています。この勉強会にはこの記事の著者である cuzic も参加しており、 RubyOnRails など勉強会参加者が興味を持つ内容について一緒に勉強しています。この記事を今、読んでいる方も興味がありましたら是非ご参加ください。詳しくは、Ruby 勉強会@関西 を参照してください。
このようなメールが保存されている場合、上記のスクリプトの実行結果は、次のようになります。必ず Outlook の実行中に上記のスクリプトを実行するようにしてください。
[learn-ruby-in-kansai:295] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:297] 入会希望[learn-ruby-in-kansai:299] メーリングリスト参加希望[learn-ruby-in-kansai:307] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:308] ここでコケました。[learn-ruby-in-kansai:300] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:298] Re: 入会希望[learn-ruby-in-kansai:302] Re: メーリングリスト参加希望[learn-ruby-in-kansai:305] Re: 第3回Ruby 関西勉[learn-ruby-in-kansai:312] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:301] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:304] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:313] Re: 第3回Ruby 関西勉強会ご案内/出欠確認/中間集計 05-10 9:30am[learn-ruby-in-kansai:310] Re: 入会希望[learn-ruby-in-kansai:311] Re: メーリングリスト参加希望[learn-ruby-in-kansai:309] Re: ここでコケました。[learn-ruby-in-kansai:306] Re: 第3回Ruby 関西勉強会ご案内/出欠確認[learn-ruby-in-kansai:303] Re: 第3回Ruby 関西勉強会ご案内/出欠確認受信トレイの内容が表示できていますね。
このスクリプトを起動するときに Outlook を起動していなかった場合は次のエラーメッセージが出力されます。
inbox.rb:4:in `connect': OLE server `Outlook.Application' not running (WIN32OLERuntimeError)     HRESULT error code:0x800401e3       操作を利用できません      from inbox.rb:4:in `each_mail'それでは、このスクリプトがどのように動作しているのかについて学んでいきましょう。
このスクリプトでは、each_mail というメソッドを定義しています。
def each_mail   ol = WIN32OLE::connect("Outlook.Application")   myNameSpace = ol.getNameSpace("MAPI")   folder = myNameSpace.GetDefaultFolder(6)   folder.Items.each do |mail|     GC.start     yield mail   end end  ol = WIN32OLE::connect("Outlook.Application")という行で、Outlook アプリケーションに接続しています。 Outlook の ProgID は Outlook.Application です。
WIN32OLE::connect というのは、志村さんの回に出てきましたね。 WIN32OLE::connect を使うことで、実行中の Outlook アプリケーションのオブジェクトを取得できます。
  myNameSpace = ol.getNameSpace("MAPI")という行は一種のおまじないです。これで、NameSpace オブジェクトを取得します。
NameSpace オブジェクトはメールを取得したり、フォルダを取得するときに使用するオブジェクトになります。詳しく知りたい場合、最後のよく使うメソッドの一覧を参考にしてください。
ここでは、NameSpace オブジェクトの GetDefaultFolder メソッドを使っています。
  folder = myNameSpace.GetDefaultFolder(6)6 というのは、受信トレイの Outlook フォルダ取得するときに用いる定数です。 Outlook のタイプライブラリの中では olFolderInbox という定数で定義されています。
簡単によく使うフォルダについてどんな定数なのか示しておきます。
フォルダ名定数名
削除済みアイテムolFolderDeletedItems3
送信トレイolFolderSentMail5
受信トレイolFolderInbox6
下書きolFolderDrafts16
NameSpace オブジェクトの GetDefaultFolder メソッドは MAPIFolder オブジェクトを返します。
  folder.Items.each do |mail|    GC.start    yield mail  endMAPIFolder オブジェクトは、Outlook のフォルダを表現するオブジェクトで、そのフォルダに関する情報を取得・設定できます。
上記のように書くことで、各メールについて、一通ずつ処理することができます。
GC.start というのは Ruby のガーベッジコレクションを動作させるために書いています。メールの数が非常に多くなった場合は、ガーベッジコレクションを動作させないとエラーになる場合があります。
Win32OLE ライブラリを使って、大量のオブジェクトを扱うとよくメモリが足りなくなることがあります。こういうときは、明示的に GC.start を実行させると、うまく動くようになります。
    yield mailmail をブロックパラメータとして、呼び出し元のブロックを実行します。この mail は MailItem オブジェクトになります。
これで、受信トレイの中のメールを順に処理する方法が分かりました。次に each_mail メソッドを呼び出す側についてみていきましょう。
each_mail do |mail|  puts mail.Subjectendやっていることは、MailItem オブジェクトの Subject プロパティを順に表示させているだけです。
MailItem オブジェクトは、一通のメールを抽象化したオブジェクトです。 MailItem オブジェクトを使うことで、メールの件名や本文などの多様な情報を取得・設定できます。そして、それだけでなく、返信メールの作成や、メールの送信も MailItem オブジェクトを用います。
MailItem オブジェクトの主要なメソッドとプロパティについて最後にまとめましたので、参考にしてください。
MailItem オブジェクトにはあて先のメールアドレスを取得できるプロパティとして To があります。ここで、each_mail の呼び出しのところを次のように変更するとあて先のメールアドレスを取得できるのでしょうか?
each_mail do |mail|  puts mail.Toendこのように変更すると、Outlook は次のようなダイアログボックスがポップアップします。
これは、プログラムから電子メールアドレスを取得する場合に表示されます。このダイアログボックスが表示されることが、Outlook で電子メールを扱うときに障害になる場合があります。特に、サーバで自動的に行いたいような場合では、致命的でしょう。このことを念頭に置きながら電子メールの送信の扱いを Outlook で行うかどうかを判断してください。

特定条件に合ったメールを検索する次に特定条件に合ったメールを検索し、そのメールに対して、順にメールを処理する方法を学びます。
処理すべきメールかどうかの判断を行う方法には次の3つがあります。
  • あらかじめ、処理すべきメールを指定のフォルダに置いておく。
  • 全メールに対して処理すべきかどうかを判断するルーチンを Ruby で書く。
  • 検索条件にマッチするメールだけを処理するように Application オブジェクトの AdvancedSearch メソッドを使用して書く。
1番目の方法は一番簡単です。Outlook の仕訳ルールを駆使すればできます。他に Items コレクションの Restrict や Find メソッドを使えば、ある フォルダの中で特定の条件を満たすメールだけを処理することもできます。
2番目の方法は、Ruby で細かい分岐を書きたい場合に向いています。
3番目の方法は Outlook にある高機能検索の機能を使う方法です。
1番目、2番目の方法は、この記事を読んでいる方であれば自分で工夫してできるのではないでしょうか?
今回は 3番目の方法を説明していきます。
次のスクリプトは指定した条件にマッチしたメールを別ウィンドウで表示します。
search.rb    1|#! ruby -Ks   2|   3|require 'win32ole'   4|require 'singleton'   5|   6|class Outlook    7|  include Singleton   8|  def initialize    9|    @ol = WIN32OLE::connect("Outlook.Application")  10|  end  11|  12|  def each_mail_filtered folder,subject  13|    events = WIN32OLE_EVENT.new(@ol,"ApplicationEvents_11")  14|    search_done = false  15|    events.on_event("AdvancedSearchComplete") do |search|  16|      results = search.Results  17|      count = results.Count  18|      break if count == 0  19|      1.upto(count) do |i|  20|        yield results.Item(i)  21|      end  22|      search_done = true  23|    end  24|  25|    if subject !~ /\%/ then  26|      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject = '#{subject}'")  27|    else  28|      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject LIKE '#{subject}'")  29|    end  30|    WIN32OLE_EVENT.message_loop until search_done  31|  end  32|end  33|  34|  35|outlook = Outlook.instance  36|  37|str = "%入会希望%"  38|outlook.each_mail_filtered("Inbox",str) do |mail|  39|  mail.GetInspector.Activate  40|end
今回のスクリプトは初めて紹介する内容があるかと思います。順に説明していきます。
実は、Win32OLE 活用法で私が書いてきた今までのスクリプトでは class を一度も使ってきませんでした。今回は今までとは少し気分を変えて、class を使っています。
今回のスクリプトでのクラス定義は次のようになります。
class Outlook   include Singleton  def initialize     @ol = WIN32OLE::connect("Outlook.Application")  end  def each_mail_filtered folder,subject    events = WIN32OLE_EVENT.new(@ol,"ApplicationEvents_11")    search_done = false    events.on_event("AdvancedSearchComplete") do |search|      results = search.Results      count = results.Count      puts count      return if count == 0      1.upto(count) do |i|        yield results.Item(i)      end      search_done = true    end    if subject !~ /\%/ then      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject = '#{subject}'")    else      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject LIKE '#{subject}'")    end    WIN32OLE_EVENT.message_loop until search_done  endend  include Singletonという行で Singleton モジュールを include しています。この行により、Outlook.instance で、Outlook クラスの唯一のインスタンスを取得できるようになります。
  def initialize     @ol = WIN32OLE::connect("Outlook.Application")  endOutlook クラスの初期化です。特に説明の必要はないでしょう。
each_mail_filtered メソッドの定義が今回のスクリプトで主に解説する部分になります。 each_mail_filtered というメソッドでは、検索を行うフォルダと件名を引数として、検索条件に合ったメールを yield します。
  def each_mail_filtered folder,subject    events = WIN32OLE_EVENT.new(@ol,"ApplicationEvents_11")    search_done = false    events.on_event("AdvancedSearchComplete") do |search|      results = search.Results      count = results.Count      puts count      break if count == 0      1.upto(count) do |i|        yield results.Item(i)      end      search_done = true    end    if subject !~ /\%/ then      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject = '#{subject}'")    else      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject LIKE '#{subject}'")    end    WIN32OLE_EVENT.message_loop until search_done  endこのメソッドでは、イベントを使っています。この例ではイベントを使うことで、検索が終了したときの処理を記述しています。イベントドリブンのスタイルが主流である Windows プログラミングでは、イベントの使い方を学ぶことは大切です。
    events = WIN32OLE_EVENT.new(@ol,"ApplicationEvents_11")イベントを使用したいときは、WIN32OLE_EVENT.new メソッドによって、 WIN32OLE_EVENT クラスのインスタンスを取得する必要があります。第1引数は WIN32OLE クラスのインスタンスで、第2引数はイベントのインタフェース名になります。
このスクリプトは、お使いの環境次第では動かないことがあるかと思います。特に次のようなエラーメッセージが出てしまう場合があるのではないでしょうか?
search.rb:13:in `initialize': interface not found (RuntimeError)     HRESULT error code:0x80004002       インターフェイスがサポートされていません  from search.rb:13:in `new'このようなときは "ApplicationEvents_11" を "ApplicationEvents_10" に変更してください。
このようなイベントの インタフェース名を調べるには一種のノウハウが必要です。
イベントのインタフェース名を調べる方法について ruby-list:39137 で、arton さんが書かれた方法が非常に参考になります。簡単な方法は助田さんが作成された Simple OLE Browser や Python Object Browser を用いることでしょう。
オブジェクトブラウザについては Win32OLE 活用法 第 1 回 でより詳しく説明していますので、そちらを参考にしてください。
Python Object Browser を用いると、次の画面のようにどのようなイベントが定義されているか容易に調べることができます。
実は筆者はイベントのインタフェース名を調べる方法が分からなくてかなりの時間を費やした経験があります。この記事を読んでいる人はそのような無駄な時間を費やさず効率的に開発を行ってください。
    search_done = false    events.on_event("AdvancedSearchComplete") do |search|      results = search.Results      count = results.Count      break if count == 0      1.upto(count) do |i|        yield results.Item(i)      end      search_done = true    endここで、AdvancedSearch メソッドを実行し、終了したときに行う処理を記述しています。
イベントに対応して動作するような処理を記述するには、 WIN32OLE_EVENT#on_event メソッドを使います。
AdvancedSearchComplete イベントの引数である Search オブジェクトは、Ruby ではそのイベントを処理するブロックのブロックパラメータとなります。
Search オブジェクトは AdvancedSearch メソッドによる検索そのものを表現するオブジェクトです。検索時のパラメータなどを設定・取得できます。検索結果は Results オブジェクトで、Search オブジェクトの Results プロパティによって取得できます。
AdvancedSearchComplete イベントは、AdvancedSearch メソッドが完了したときに発生します。 AdvancedSearch メソッドでは、検索が終了しているかどうかを知るためにはAdvancedSearchComplete イベントを使用するしかありません。詳しくは、AdvancedSearch メソッド を参照してください。
search_done という変数は後の処理のメッセージループで、検索が終わったかどうかを表すフラグとして使用するために用意しています。
Results オブジェクトの Count プロパティは 検索にマッチしたメールの数を表します。
      1.upto(count) do |i|        yield results.Item(i)      end      search_done = trueここで、条件にマッチしたメールを呼び出し元のブロックに yield します。そして処理終了後 search_done フラグを true にしています。
次に AdvancedSearch メソッドの使い方について学んでいきましょう。
    if subject !~ /\%/ then      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject = '#{subject}'")    else      @ol.AdvancedSearch(folder,"urn:schemas:mailheader:subject LIKE '#{subject}'")    endApplication オブジェクトの AdvancedSearch メソッドの詳しい使い方については Searchオブジェクトの使用を参照してください。
第一引数は「検索を実行するフォルダの名前」を指定し、第2引数には、「検索のロジックを定義する DASL 文字列」を指定します。 DASL とは、DAV Searching And Locating の略で、DASL 文字列を用いてどのような検索フィルタを実行するかを指定します。 DASL 文字列は、「Microsoft Exchange 2000 Web Storage」のクエリと同じ文法に従います。
と言われてもこの「"urn:schemas:mailheader:subject = '#{subject}'"」というような式の作り方はよく分からないですよね?
特に前半の "urn:schemas:mailheader:subject" というような文字列がよく分からないと思います。
このようなときのために、Outlook のメニューからこの SQL の条件の書き方に似た DASL文字列を簡単に作成する方法があります。 Outlook 2002 には標準で存在する機能です。「表示」→「並べ替え」→「現在のビュー」→「現在のビューの編集」で、「ビューのカスタマイズ」のダイアログボックスから「フィルタ」を選択して、フィルタのダイアログボックスというものがあります。
これを使うと次の画面のように、簡単にフィールドに対する検索条件から、生成される文字列がどのようになるのかについて確認できます。
AdvancedSearch の機能を駆使したい場合は、便利な機能です。
この検索フィルタを書くときは SQL と同じように % はあいまい検索のときにワイルドカードとして用います。 LIKE ではなく、 = を使うと完全一致になります。
そう言われると簡単ですよね?
    WIN32OLE_EVENT.message_loop until search_doneの行では、until 修飾子を使っています。
式を until 修飾することで、until 以下の条件が真になるまで左辺の式を繰り返すことができます。
この場合は、検索が終了するまで、WIN32OLE_EVENT の message_loop を繰り返すということです。
メインのルーチンは次のようになっています。
outlook = Outlook.instancestr = "%入会希望%"outlook.each_mail_filtered("Inbox",str) do |mail|  mail.GetInspector.ActivateendOutlook クラスは Singleton ですので、 Outlook.instance で Outlook クラスの唯一のインスタンスを取得できます。
MailItem オブジェクトの GetInspector プロパティによって、 Inspector オブジェクトを取得できます。 Inspector オブジェクトは Outlook のメールなどのアイテムを表示しているウィンドウを表現しています。
この Inspector オブジェクトの Activate メソッドによって、そのメールのウィンドウを表示できます。

新規メールの編集・送信する最後にメールの送信の仕方について学びます。
次のスクリプトを見てください。このスクリプトでは mail.To は、存在しないメールアドレスにしていますが、実際に動かすときは 自分のメールアドレスに変更していただけますようお願いします。
send.rb    1|#! ruby -Ks   2|require 'win32ole'   3|require 'singleton'   4|   5|class Outlook    6|  include Singleton   7|  def initialize    8|    @ol = WIN32OLE::connect("Outlook.Application")   9|    WIN32OLE.const_load(@ol,self.class)  10|  end  11|  12|  def new_mail  13|    mail =  @ol.CreateItem(OlMailItem)  14|    mail.BodyFormat = OlFormatPlain  15|    return mail  16|  end  17|end  18|  19|outlook = Outlook.instance  20|mail = outlook.new_mail  21|mail.To = 'rubima@cuzic.com'  22|mail.Subject = 'WIN32OLE test mail'  23|mail.Body = 'WIN32OLE を使って Outlook でメールを送信するテストです。'  24|mail.GetInspector.Activate  25|mail.Send
このスクリプトではメールの送信を行います。
#! ruby -Ksこの行はこのスクリプトは Shift JIS として処理することを指定しています。 Outlook を操作するときは、文字化けを防ぐために 文字コードとして、Shift JIS を指定するようにしましょう。
次がクラス定義です。
class Outlook   include Singleton  def initialize     @ol = WIN32OLE::connect("Outlook.Application")    WIN32OLE.const_load(@ol,self.class)  end  def new_mail    mail =  @ol.CreateItem(OlMailItem)    mail.BodyFormat = OlFormatPlain    return mail  endend    WIN32OLE.const_load(@ol,self.class)初期化の処理で、今回は定数をロードするようにしました。 Outlook と書く代わりに self.class と書くことで、クラス名が Outlook から変わったときでも、コードの修正が一箇所で済みます。これは、Don't Repeat Yourself の原則に従っています。 DRY と短くして呼ばれたりします。複数の箇所を繰り返し修正する可能性があるようなコードを書くことを戒める原則です。
なお、Object#class は、そのオブジェクトのクラスを返すメソッドです。Ruby は動的型言語で、クラスは Class クラスのインスタンスです。そして、あるオブジェクトのクラスは Object#class メソッドで取得できます。
このように、定数をロードしたのち、その定数を new_mail メソッドで用いています。 new_mail メソッドを使うことで、プレインテキストの新規メールアイテムを生成できます。
   def new_mail     mail =  @ol.CreateItem(OlMailItem)     mail.BodyFormat = OlFormatPlain     return mail   endApplication オブジェクトの CreateItem メソッドは新規アイテムを作成するためのメソッドです。 olMailItem という定数を使うことで、メールのアイテムを作成できます。
メールの本文の形式がデフォルトでは、HTML になってしまうので、text/plain にするために MailItem オブジェクトの BodyFormat プロパティに olFormatPlain と設定しています。
そして、メインの処理について説明しましょう。
outlook = Outlook.instancemail = outlook.new_mailmail.To = 'rubima@cuzic.com'mail.Subject = 'WIN32OLE test mail'mail.Body = 'WIN32OLE を使って Outlook でメールを送信するテストです。'mail.GetInspector.Activatemail.Sendoutlook.new_mail によって、新規メールを取得しています。そして、MailItem オブジェクトの To,Subject,Body といったプロパティに適宜設定しています。
メールの送信には、 MailItem オブジェクトの Send メソッドを用います。これだけで簡単にメールを送信することができます。
しかしながら先ほどと同じように、メールの送信時には Outlook アプリケーションに警告メッセージが表示されます。
ここで、OK をクリックすると、メールの送信が可能になります。




各オブジェクトのメソッドとプロパティ今回の記事には書ききれませんでしたが、Outlook にはまだまだ極めて多くの機能があります。たとえば、フォルダーの整理や SPAM メールの削除、添付ファイルの保存等を自動的に行いたいような場合があると思います。
具体的なコードについては紹介しませんでしたが、筆者がよく使うと思うメソッドやプロパティについて下記のように紹介します。
自分で開発を行うときの参考にしてください。

Application オブジェクトOutlook アプリケーションそのものを表現するオブジェクトです。
メソッド・プロパティ説明
ActiveInspector現在アクティブなアイテムを表示するウィンドウを取得
AdvancedSearch高機能な検索を実行
CreateItem新規 Outlook アイテムを生成
CreateItemFromTemplate新規 Outlook アイテムをテンプレートから生成
GetNamespaceNameSpace オブジェクトを返す。引数として "MAPI" しかサポートされない。
Inspectorsアイテムを表示するウィンドウのコレクションを取得
QuitOutlook アプリケーションを終了

Attachments オブジェクトOutlook アイテムに添付された添付ファイルの集合を表現するオブジェクトです。
メソッド・プロパティ説明
AddOutlook アイテムに指定されたファイルを添付ファイルとして追加します。
Remove指定された添付ファイルを Outlook アイテムの添付ファイルから削除します。

Attachment オブジェクトOutlook アイテムに添付されたある 1 個の添付ファイルを表現するオブジェクトです。
メソッド・プロパティ説明
FileNameファイル名を取得
SaveAsFile添付ファイルを指定したフォルダに保存します。

Folders オブジェクトOutlook フォルダの集合を表現するオブジェクトです。
メソッド・プロパティ説明
Add指定した名前の Outlook フォルダを追加します。
GetFirst最初の Outlookフォルダを取得します。
GetNext次の Outlook フォルダを取得します。
Itemあるインデックスの Outlook フォルダを取得します。
Removeあるインデックスの Outlook フォルダを削除します。

Inspector オブジェクトOutlook アイテムを表示するウィンドウを表現するオブジェクトです
メソッド・プロパティ説明
CurrentItem今表示している Outlook アイテムを取得します。
Displayそのウィンドウを表示します。
EditorTypeその Outlook アイテムが テキストか、HTML かなどを指定します。

Items コレクションOutlook アイテムのコレクションです。
メソッド・プロパティ説明
AddOutlook アイテムを追加します。
ClassOutlook アイテムの種別を取得します。
Find指定した条件にあったOutlook アイテムを返します
FindNextFind メソッドの実行後に、次の Outlook アイテムを返すときに使います。
GetFirst最初の Outlook アイテムを返します。
GetNext次の Outlook アイテムを返します。
Itemあるインデックスに対応した Outlook アイテムを返します。
Removeあるインデックスに対応 Outlook アイテムを削除します。
Restrictあるフィルタにマッチするすべての Outlook アイテムを含む Items コレクションを返します。
SetColumns特定のプロパティに高速なアクセスをするために、キャッシュします。
Sort特定のプロパティによって、ソートします。

MailItem オブジェクトOutlook のメールメッセージを表現するオブジェクトです。
メソッド・プロパティ説明
Bodyメールの本文
CCメールの CC
Closeメールを閉じます。
Displayそのメールを表示します。
EntryIDEntryID は Outlook でそのアイテムを管理するIDです。NameSpace オブジェクトの GetItemFromID メソッドで使用します。
Forwardそのメールを転送する MailItem を返します。
GetInspectorInspectorオブジェクトを取得します。
Moveそのアイテムを指定した Outlook フォルダに移動します。
Replyそのメールに返信する MailItem を返します。
ReplyAll元のメールの全受信者に返信する MailItem を返します。
Sendそのメールを送信します。
SenderEmailAddress送信者のメールアドレスを返します。
SenderName送信者の名前を返します。
SentOn送信時刻を返します。
Subjectメールの件名を返します。
Toメールの受信者を返します。

MAPIFolder オブジェクトMAPIFolder オブジェクトは Outlook のフォルダを表現します。
メソッド・プロパティ説明
EntryIDフォルダの EntryID を返します。
Foldersそのフォルダに含まれる Outlook フォルダを含む Folders オブジェクトを返します。
Parentそのフォルダの親の階層のオブジェクトを返します。

NameSpace オブジェクトNameSpace オブジェクトは、任意のデータソースの抽象的なルートオブジェクトを表現します。
メソッド・プロパティ説明
GetDefaultFolder受信トレイなど特殊なフォルダにアクセスするときに使います。
GetFolderFromIDEntryID から Outlook フォルダを取得します。
GetItemFromIDEntryID から Outlook アイテムを取得します。

Results コレクションResults コレクションは Search オブジェクトと AdvancedSearch メソッドで取得されたデータおよび結果を格納します。
GetFirst最初の検索フィルタに適合した Outlook アイテムを取得します。
GetNext次の検索フィルタに適合した Outlook アイテムを取得します。
Item指定したインデックスの Outlook アイテムを取得します。

Search オブジェクトSearch オブジェクトは Outlook のアイテムに対して実行された個々の検索に関する情報を含みます。 Application オブジェクトの AdvancedSearch メソッドを使用したときにこのオブジェクトを利用します。
Filter検索に用いる DASL 文字列
Results検索結果である Results オブジェクトを取得します。
Scope検索を実行する Outlook フォルダ
SearchSubFoldersサブフォルダも検索の対象とするかどうかのフラグ
Tag非同期の検索を行うときに使うタグ




終わりに今回は、Outlook を Ruby から扱う方法について学びました。
メールはもはや不可欠のツールとなってしまいました。メールの送信やメールの処理を自動的に行う方法について学ぶことで、今までよりずっと効率よく仕事を行えるでしょう。
また、メールの送信や受信以外にも Outlook には非常にさまざまな機能があります。今回では紹介しきれなかったさまざまな機能を利用したい場合は、最後に掲載したオブジェクトとメソッドの一覧を参照したり、オブジェクトブラウザを用いることや、MSDN を参照することで調べてください。
次回は、Windows Scripting Host を使って、既存の COM 対応ではないアプリケーションを制御する方法について書きたいと思います。
(アドバイザー:arton、助田 雅紀)




エピローグ「これで全自動になったなー」あなたは、いつもの仕事を今までよりはるかに効率よく、自動的に処理をしているのを確かめながら、満足げにつぶやきました。
今までのメールの中の必要な部分をコピーアンドペーストする業務がなくなって、ずっと楽になったのです。
けれど、今の感覚はただ楽になったのが嬉しいというようなこと以上のものでした。
「どうしてもっと早くから、楽にするためにプログラムを作ることをしなかったんだろうな?」
あなたは不思議そうにこうつぶやきます。プログラミングをすることそのものに怖れを抱いていたのはそれは昔のことになってしまっていました。 (つづく)




参考文献



著者についてcuzic は 関西 Ruby 勉強会でみんなと一緒に勉強をしている Ruby プログラマです。一緒にワイワイガヤガヤ勉強しながら、日々の仕事をシアワセなものにしていきましょう。 cuzic にコンタクトをとりたい方は、cuzic atmark cuzic.com にお願いします。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-8-1 08:21:22 | 显示全部楼层
【第 6 回】 Web 自動巡回



はじめにこれまで Excel, ADODB, Outlook などのアプリケーションを Ruby で扱う方法について学んできました。
今回は Web の自動巡回をテーマにしながら、Windows Script Host と Internet Explorer の COM オブジェクトを扱う方法について学んでいこうと思います。
この記事では Web の自動巡回を Internet Explorer を制御することによって行います。 IE を制御するための方法として、Internet Explorer の COM オブジェクトを操作する方法と、キーボードをシミュレーション、つまりキーボード操作をすでに特定のアプリケーションに送ることによって制御する方法について学んでいきます。




Web 自動巡回の方法についてそもそも Web の自動巡回をしたいだけのときに、Internet Explorer を操作する必要があるとは限りません。
SPIDERING HACKS に掲載されているような方法を用いることで、ウェブを縦横無尽に自動収集させることで、自分の必要な情報を集めていくような方法があります。
Internet Explorer を使う方法以外にも Ruby には Net/HTTPopen-uri があります。他にも RAA には http-access2 が登録されています。これらのライブラリを利用する方法は、ページを取得して必要なデータを抽出するというようなスパイダリングのような用途だけであれば、Internet Explorer を使うよりもずっと向いています。 Win32OLE を用いる方法はスパイダリングにはあまり向いていません。
それではどのようなときに Win32OLE を用いて Internet Explorer を制御することが有効になるでしょうか?
まず、ブラウザを表示させたいときがあります。自分の欲しい情報のところだけを切り出して、スパイダーに収集させたいというときもあるでしょうが、単純に順繰りに Web ページを巡回したいだけのときもあるでしょう。また、ユーザ名とパスワードの入力のみを自動化させたいというような用途もあるかもしれません。ユーザ名とパスワードの入力を自動化させることによって、ちょっとした面倒さをなくしたり、エンドユーザにパスワードを教えないまま操作可能にしたいという要望を満たしたりできます。
他に Java Script を用いているようなサイトから情報を引き出したいときが考えられます。このような場合はどんなに Net/HTTP などのライブラリを使ったとしても効果がありません。
最近とみに流行している Ajax を使ったサイトなどの場合は、モダンなブラウザを用いてテキストボックスに入力などを行わないと必要な情報の取得はできません。
この記事では、執筆時点で Ajax がホットな話題ということもありますので、 Ajax サイトからのデータの取得方法について説明していこうと思います。




今回学ぶ内容について今回学ぶ方法は次のとおりです。
  • Internet Explorer の COM オブジェクトを Ruby から生成・制御
    • テキストボックスの値を取得
    • HTTP の POST メソッドを使ってページを表示。
  • Windows Script Host を用いてキーボードシミュレーションを行い IE を制御する方法
    • クリップボードを使っての情報の受け渡し
COM オブジェクトを操作する方法というのは今までずっと学んできましたね。 Excel や ADODB や Outlook のオブジェクトを操作することでそれらのアプリケーションを自動制御することができました。
この COM オブジェクトを操作する方法について Internet Explorer についても学びます。このときに、テキストボックスの値の取得や設定、他に HTTP の Post メソッドでのページの移動について学んでいきます。
あと、もう1つ学ぶのは、Windows Script Host を用いて、自動制御する方法です。この記事をお読みならばご存知の方も多いのではないかと思いますが、Windows Script Host には、SendKeys というメソッドがあります。これを用いることで、定型的な入力を自動的に行い、外部のアプリケーションを自動制御することができます。
その外部のアプリケーションとデータのやりとりを行うときは、 Windows のクリップボードの機構を用いることで、相互にデータをやりとりし、分岐することも可能になります。




Ajax サイトでの自動制御
COM オブジェクトとしての操作Internet Explorer のオブジェクトを操作することによって、Java Script を用いているサイトを自動的に巡回するような方法についてここでは学びます。
今回、自動制御する対象とするのは、Ajax サイトとして非常に有名な Ajax を使った郵便番号検索 を例に、Java Script を使われているサイトでの巡回について説明します。
説明が遅れましたが、Ajax というのは Asynchrous JavaScript + XML の略です。 JavaScript + CSS を使った Dynamic HTML の機構と XMLHttpRequest を用いてサーバ側のアプリケーションを組み合わせた技術を Ajax と呼ぶようです。 Google Maps で用いられてその画面遷移なしでのナビゲーションを行うことが実に画期的なテクノロジーです。
Ajax は今までの Web ブラウジングに全く新しいエクスペリエンスをもたらしました。 Ajax は非常に快適な操作性が特徴ですが、その反面、いままでの Web ブラウズとは全くことなるパラダイムにしたがって動作するため、従来と同じ方法で Web ページの巡回を行えません。
Ajax を用いたサイトは、モダンなブラウザで閲覧することが前提となるからです。
このようなサイトを自動的に巡回するにはまさにそのモダンなブラウザを自動制御することが有効な方法です。Java Script や CSS を使ったアクションを勝手に行ってくれるからです。そして、私たちはきわめて自動制御に向いたブラウザをすでに持っています。 Internet Explorer です。
IE は COM を用いることで、自動的にテキストボックスの入力や JavaScript の命令を実行させることができます。それではこれからそのための方法について学んでいきましょう。

COM を使った方法COM を用いることで、郵便番号検索のサイトを自動的に制御させるスクリプトは次のものです。
このスクリプトでは郵便番号に適当な郵便番号を自動入力させて、その都道府県や市区町村を標準出力に出力させています。
zip_com.rb    1|require 'win32ole'   2|   3|ie = WIN32OLE.new("InternetExplorer.Application")   4|ie.Navigate "http://apollo.u-gakugei.ac.jp/~sunaoka/ajax/ajaxzip/"   5|   6|ie.Visible = true   7|   8|while ie.Busy == true   9|  sleep 1  10|end  11|  12|zip = ie.Document.getElementByID("zip")  13|zip.Value = "1000001"  14|zip.FireEvent("onkeyup")  15|  16|sleep 1  17|%w(pref city ville).each do |name|  18|  puts "#{name} #{ie.Document.getElementByID(name).Value}"  19|end
Win32OLE活用法第一回ですでに Internet Explorer を使った例について紹介しています。
そのときは、ほとんど説明は行いませんでしたが勘の良い方は説明がなくても理解できているかもしれません。
Internet Explorer の使い方を調べるときは、2つのタイプライブラリをオブジェクトブラウザで調べることになります。
"Microsoft Internet Controls" と "Microsoft HTML Object Library" です。自分で Internet Explorer を制御するときはこれらのタイプライブラリを調べてください。
DHTML の機構に従うと、getElementByID を用いて特定のエレメントを ID を用いて取得できます。このスクリプトでは ID が "zip" のエレメントを取得しています。
zip = ie.Document.getElementByID("zip")ID が "zip" のエレメントは、 Ajax を使った郵便番号検索 のページの HTML ソースを見るとどれか分かります。郵便番号という文字列の右にあるテキストボックスが ID が zip のエレメントになります。
zip というオブジェクトを第一回でやったように WIN32OLE#ole_obj_help メソッドを用いて調べると、
irb(main):009:0> zip = ie.Document.getElementByID("zip")=> #<WIN32OLE:0x1016e728>irb(main):010:0> zip.ole_obj_help=> DispHTMLInputElementDispHTMLInputElement というオブジェクトであることが分かります。
DispHTMLInputElement は、オブジェクトブラウザで調べるときは、 DispHTMLInputElement というオブジェクトはでてきませんので、 HTTMLInputElement を調べる必要があります。 Simple OLE Browser や Python Object Browser で調べる場合は、 DispHTMLInputElement を調べることができます。そうすると、このエレメントが提供するメソッドやプロパティ、イベントが分かります。
筆者が既存の Web ページを自動巡回するようなスクリプトを作るときは、 irb を使いつつ WIN32OLE#ole_obj_help メソッドを駆使して、 ie.Document.getElementByID で取得したエレメントがどのオブジェクトなのかを調べ、"Microsoft HTML Object Library" のタイプライブラリからオブジェクトブラウザでそのオブジェクトが持つメソッド等を調べるという方法をよくします。
ここでは zip エレメントの Value プロパティを設定することで、値を更新しています。オブジェクトブラウザで調べると、HTMLInputElement オブジェクトは FireEvent というメソッドを持っています。このメソッドを用いることで、JavaScript で定義されているイベントを発生させることができます。 Ajax を使った郵便番号検索 の HTML ソースを見ると "onKeyUp" イベントを用いて Ajax を実現しているようなので、FireEvent メソッドの引数に "onKeyUp" を与えることで、onKeyUp メソッドを発生させています。
zip.FireEvent("onKeyUp")FireEvent メソッドを実行することで、onKeyUp イベントで登録されている JavaScript が動作して、都道府県や市区町村のところのテキストボックスを更新できています。
そしてイベントの実行終了を待ったのち、更新されたエレメントの値を取得しています。
sleep 1%w(pref city ville).each do |name|  puts "#{name} #{ie.Document.getElementByID(name).Value}"endこのスクリプトによって、Ajax によって更新されたテキストボックスの値の取得ができています。
これで、Net/HTTP などではうまくいかない情報を IE を用いることで自動的に取得し、収集できました。

考えられる応用と発展的な話題ここで紹介したテクニックは、結局画面を閲覧しなければならない場合、たとえばオークションサイトの巡回や、ショッピングなどを簡単に行いたい場合に非常に応用が利きます。
今回は単純に流行しているトピックを扱いたいという理由で Ajax サイトを例に説明しましたが、Ajax でない場合でも今回説明した内容は参考にできます。紙面の関係で説明しませんが、HTMLInputButtonElement オブジェクトなどには click メソッドがあり、クリックしたときの動作を行わせることもできます。
また、今回は全く説明しませんし、筆者自身試したことはありませんが、 HTA (HTML Applications) のようなスタンドアロンな GUI のスクリプトアプリケーションを Internet Explorer のオブジェクトを操作することで作成できます。 HTA については HTML Applications 概要が参考になります。また、arton さんが書いた Ruby を 256 倍使うための本 邪道編も参考になります。
CGI 出身の方が Windows GUI を作るときなどは、それまでの蓄積がそのまま活きるため検討の価値があるでしょう。
IE が COM に対応しているため、COM で自動制御を行うような応用が発生することが予想される場合は便利です。普通にスタンドアロンアプリケーションを作成したときと比べて、 COM 対応させる手間が省けます。
これはテストの自動化を行いやすいという長所があります。そのため、アジャイルな開発プロセスで GUI のテストを最初に作りたい場合などに便利かもしれません。




キーボードシミュレーションによる Internet Explorer の自動操縦ここまで、IE のオブジェクトを操作することで、Ajax ページを自動的に制御する方法について学びました。
次は、Windows Script Host の SendKeys メソッド等を用いて、自動制御させる方法について学びます。
この方法は一般にとても美しくないとされており、嫌っている方も多い方法です。しかしながら、とても応用範囲が広く強力です。他に方法はないときにこのような方法があると、自分のレパートリを増やしておくことは有益でしょう。この節は、一般的なアプリケーションに対してキーボードショートカットを駆使して、自動制御を行うときに役に立ちます。
では、サンプルのスクリプトを見てみましょう。
zip_wsh.rb    1|require 'win32ole'   2|require 'Win32API'   3|   4|class Clipboard   5|  OpenClipboard = Win32API.new('user32', 'OpenClipboard', ['I'], 'I');   6|  CloseClipboard = Win32API.new('user32', 'CloseClipboard', [], 'I');   7|  EmptyClipboard = Win32API.new('user32', 'EmptyClipboard', [], 'I');   8|  IsClipboardFormatAvailable = Win32API.new('user32', 'IsClipboardFormatAvailable', ['I'], 'I');   9|  10|  GetClipboardData = Win32API.new('user32', 'GetClipboardData', ['I'], 'I');  11|  12|  GlobalLock = Win32API.new('kernel32', 'GlobalLock', ['I'], 'P');  13|  GlobalUnlock = Win32API.new('kernel32', 'GlobalUnlock', ['I'], 'I');  14|    15|  CF_TEXT = 1;  16|    17|  def self.GetText  18|    result = ""  19|      20|    while OpenClipboard.Call(0) == 0  21|      sleep 1  22|    end  23|    begin  24|      if (h = GetClipboardData.Call(CF_TEXT)) != 0  25|        if (p = GlobalLock.Call(h)) != 0  26|          result = p;  27|          GlobalUnlock.Call(h);  28|        end  29|      end  30|    ensure  31|      CloseClipboard.Call  32|    end  33|    return result;  34|  end  35|end  36|  37|url = "http://apollo.u-gakugei.ac.jp/~sunaoka/ajax/ajaxzip/"  38|wsh = WIN32OLE.new("Wscript.Shell")  39|wsh.Run("explorer.exe #{url}",1,true)  40|sleep 2  41|wsh.AppActivate("Ajax")  42|sleep 2  43|wsh.SendKeys("%D")  44|wsh.SendKeys("{TAB}{TAB}{TAB}1000001")  45|sleep 1  46|wsh.SendKeys("{TAB}^c")  47|puts Clipboard::GetText()  48|wsh.SendKeys("{TAB}^c")  49|puts Clipboard::GetText()  50|wsh.SendKeys("{TAB}^c")  51|puts Clipboard::GetText()
このスクリプトも先ほどと同様に郵便番号を自動的に入力させて、都道府県と市区町村を標準出力に出力させています。
このスクリプトでやっていることは、次のとおりです。
  • Ajax を使った郵便番号検索のページを表示
  • アクティブなウィンドウを「郵便番号検索」のウィンドウにする。
  • TAB キーを3回送ることで、郵便番号のテキストボックスにフォーカスを移動。
  • 都道府県と市区町村のテキストボックスを順にフォーカスを移動し、クリップボードにコピーと取得。標準出力への出力を繰り返す。
クリップボードの操作については、可搬性の高い方法が執筆時点で見つからなかったために自分で実装しました。次の節で Win32API を直接たたく以外の方法を説明します。それぞれの環境に合わせてもっと楽な方法での利用を検討してください。
Win32API ライブラリ を参考にクリップボードを操作しています。 Clipboard クラスについては後で詳しく解説します。
ここではキーボードシミュレーションを行っている箇所について説明します。
url = "http://apollo.u-gakugei.ac.jp/~sunaoka/ajax/ajaxzip/"wsh = WIN32OLE.new("Wscript.Shell")wsh.Run("explorer.exe #{url}",1,true)sleep 2ここで、Ajax を使った郵便番号検索 を表示しています。すでに 「Ajax を使った郵便番号検索」のウィンドウを開いている状態であれば、フォーカスのあったエレメントが違い、必要なタブ数がずれる可能性があるので、正しく動作しません。そのため、必ずまだ開いていない状態で動かしてください。今回は、explorer.exe を使いましたが、Internet Explorer が標準のブラウザではない場合がありえますので、 C:\Program Files\Internet Explorer\iexplore.exe の方がいいかもしれません。
wsh.AppActivate("Ajax")sleep 2タイトルバーに Ajax の文字列があるウィンドウをアクティブにします。
wsh.SendKeys("{TAB}{TAB}{TAB}1000001")sleep 1TAB を一度押すことをシミュレーションするときは wsh.Sendkeys("{TAB}") と表現します。このような特殊なキーの入力については、SendKeysメソッド を参考にしてください。
このスクリプトでは TAB を3回おして、郵便番号のテキストボックスにフォーカスをあわせています。そして、郵便番号を入力しています。イベントの実行終了等を待つためにスリープしています。
wsh.SendKeys("{TAB}^c")puts Clipboard::GetText()そして、次のテキストボックスにフォーカスを合わせます。 ^c は Ctrl+C を押すことを表しています。 Ctrl+C というのはクリップボードへのコピーを意味するショートカットですので、これでテキストボックスの値をクリップボードにコピーしています。
そして、Clipboard::GetText() によってクリップボードの値を取得して、標準出力に出力しています。以下市区町村について同じことを行っています。
wsh.SendKeys("{TAB}^c")puts Clipboard::GetText()wsh.SendKeys("{TAB}^c")puts Clipboard::GetText()このようにすることでキーボードで制御できるようなことならどんなことでもコントロールできます。

キーボードシミュレーションを行うときのコツキーボードの操作をシミュレーションする方法、キーボードの操作を送ることと待ち合わせを行うだけなので、従来のキーボードでの操作さえ理解できれば実装できます。その意味で作りやすいかもしれません。
この方法は適切な箇所で適切な時間 待ち合わせを行うことがコツになります。これはいろいろと試行錯誤を行うしか方法がありません。
今回行ったキーボードシミュレーションは、一方通行で分岐などがない場合でした。このような処理で十分なときがありますが、単純にそれだけの場合は、わざわざ Ruby を使わなくても、キーボードシミュレーション専用のソフトがあります。そちらのソフトに慣れていれば、その方がいいかもしれません。
Ruby で行うことによって、最も意味があるのは、正規表現、ハッシュなど Ruby ならば標準で用意されているライブラリが必要になる場合でしょう。 Ruby の備えた高度なライブラリなら簡単にできるけれども専用ツールでは実現が困難な場合というのはあるものです。

クリップボードの操作に関してこのスクリプト上では、クリップボードの値を取得するコードが少々長いのが気になります。これは、Cygwin 版と mswin 版の Ruby で可搬性のあるクリップボードを扱う機構が執筆時点で見当たらなかったので、自力で Clipboard クラスを実装したからです。詳しい方、教えてください。
mswin版では、VisualuRuby 計画 の vr/clipboard や Ruby Library Report の第4回 で紹介された win32-utils の win32::clipboard があります。
cygwin 版であれば、`cat /dev/clipboard` を実行するという方法や `getclip` というコマンドを実行する方法が簡単です。他に、Win32のクリップボード操作用モジュール を使えばクリップボードを簡単に扱えますが、標準ではないため採用しませんでした。
さらにクリップボードを扱う可搬性がある方法として、Win32OLE を使って Internet Explorer の COM オブジェクトを経由する方法があります。
ie = WIN32OLE.new("InternetExplorer.Application")ie.Navigate("about:blank") sleep 1 until ie.Busy == falseputs ie.Document.parentWindow.clipboardData.getData("Text")でクリップボードの値を取得でき、
ie.Document.parentWindow.clipboardData.settData("Text","hoge")でクリップボードの値を設定できます。
しかしながら、IE のオブジェクトを使わずに、キーボードシミュレーションを行って IE を制御するという趣旨なので、採用しませんでした。
あと他の方法として、AutoIt を用いる方法もあります。これを使うと COM オブジェクトの形で簡単にクリップボードを扱うことができます。 AutoIt は今回紹介するようなキーボートシミュレーション以外に、マウスの動作もシミュレーションの機能もあり、単独で役に立つアプリケーションです。今回は、Ruby でキーボードシミュレーションを行う方法を紹介するという趣旨なのでこのソフトについてあまり説明しませんが、キーボードシミュレーションを行う場合は調査する価値があるソフトです。キーボードシミュレーションのソフトとして他に、 AutoHotKeyもあります。
zip_wsh.rb では、まずこれまで説明したとおり、このように直接 Win32API をたたくことでクリップボードの値の取得しています。しかしながら普通は今まで説明したようになんらかのライブラリや COM オブジェクト、 /dev/clipboard などを用いてクリップボードの操作を行うようにしましょう。
Clipboard クラスの詳細についてせっかく Win32API ライブラリを使用したクラスを書きましたので Clipboard クラスについて解説しましょう。それでも Clipboard の操作のための Win32API の詳細は別の文献に譲ることにしてここでは、Ruby の Win32API ライブラリの使い方など、Ruby 的な箇所について学んでいきます。
  OpenClipboard = Win32API.new('user32', 'OpenClipboard', ['I'], 'I');の行を例に Win32API を呼び出す方法について学びましょう。ここで、Win32API を直接呼び出せるように Win32API クラスのインスタンスを生成しています。 Win32API クラスでは、いくつかのパラメータで初期化することで、Win32API を直接呼び出すことができます。
OpenClipboard の MSDN のページを参照しながら、読むと理解が進むかもしれません。 OpenClipboard を Visual Basic で呼び出すときは次のように Win32API を定義しています。
Private Declare Function OpenClipboard Lib "user32" (ByVal hwnd As Long) As Long'user32' は、dll の名前です。 'OpenClipboard' は、呼び出したい関数の名前。 ['I'] というのは、'OpenClipboard' という関数の引数の型を指定しています。 ['I'] の場合は、整数の引数を1つだけとるということを意味しています。最後の引数の 'I' も整数の引数をとるということを意味しています。
このような Win32API をいくつか定義した後に、実際にクリップボードの値の取得を行っています。
  def self.GetText    result = ""    while (h = OpenClipboard.Call(0)) == 0      sleep 1    end    begin        if (p = GlobalLock.Call(h)) != 0          result = p;          GlobalUnlock.Call(h);        end      end    ensure      CloseClipboard.Call    end    return result;  endここで、getText というクラスメソッドを定義しています。 クラスメソッドの定義に、さまざまなクラスメソッドの定義の方法について解説されています。
Win32API クラスのインスタンスの使い方は、これを見るとある程度想像がつくでしょうか?
    while (h = OpenClipboard.Call(0)) == 0      sleep 1    endのように Win32API#Call メソッドを用いて、Win32API の呼び出しを行います。

考えられる応用と発展的話題COM は Windows ではきわめて広範なアプリケーションに実装されていますが、すべてのアプリケーションに実装されているわけではありません。たとえば、Outlook Express には実装されていませんし、個別のアプリケーションで実装されていないものは多いでしょう。
それでも自動制御をしたいような作業がある場合においてはキーボードシミュレーションを行うことが有効でしょう。
また自分で作ったアプリケーションの GUI のテストを行いたいという需要もあるでしょう。こういう場合もキーボードシミュレーションを行うことで、テストの自動化が可能です。このような用途は多いでしょう。
さきほど説明した AutoIt を用いることができれば、マウスの操作もシミュレーションできます。そのため、テストスクリプトの作成に大活躍するかもしれません。
それぞれの開発の参考にしてください。
ここでは紹介しませんでしたが、Wscript.Shell というコンポーネントは、他にもさまざまな便利な機能があります。たとえば、Popup メソッド は、Ruby のアプリケーションを作っているときにユーザに何かを確認する必要があるときなどに便利です。また、Windows Scritp Host にはネットワークドライブの割当などさまざまな機能が提供されています。 @IT のWindows管理者のためのWindows Script Host入門 などが参考になります。活用してください。




Internet Explorer での POST メソッドWeb 巡回のテーマの最後に少し私自身が以前ハマったこととして、Internet Explorer で Post メソッドを用いて CGI に値を渡す方法について紹介したいと思います。
GET メソッドや CGI ではない単純なページでは、普通に URL を指定するだけなのですが、 POST で CGI を呼び出すときは、COM の型と Ruby の型との間の変換の関係で、少し複雑な処理が必要になります。その複雑な処理についてここで解説しようと思います。
次のスクリプトは「人力検索サイトはてな」に自動的にログインを行うスクリプトです。
hatena.rb    1|require 'win32ole'   2|include WIN32OLE::VARIANT   3|   4|ie = WIN32OLE.new("InternetExplorer.Application")   5|ie.Visible = true   6|   7|url = "https://www.hatena.ne.jp/sslregister"   8|def ie.navigate_post url,query_string   9|  header = "Content-type: application/x-www-form-urlencoded"  10|  postdata = query_string.unpack("c*")  11|  navi = self.ole_method("Navigate2")  12|  ret = self._invoke(navi.dispid, [url, nil, nil, postdata, header], [VT_BYREF|VT_VARIANT, VT_BYREF|VT_VARIANT, VT_BYREF|VT_VARIANT, VT_ARRAY|VT_UI1, VT_BYREF|VT_VARIANT])  13|  @lastargs = WIN32OLE::ARGV  14|  ret  15|end  16|  17|query_string = "mode=login&backurl=http%3a%2f%2fwww%2ehatena%2ene%2ejp%2f&key=myname&password=mypassword"  18|ie.navigate_post url,query_string
key=myname&password=mypassword のところに自分のはてなのユーザ名とパスワードを入力してください。
このスクリプトでは次の特異メソッドの定義が一番重要なところになります。
include WIN32OLE::VARIANTdef ie.navigate_post url,query_string  header = "Content-type: application/x-www-form-urlencoded"  postdata = query_string.unpack("c*")  navi = self.ole_method("Navigate2")  ret = self._invoke(navi.dispid, [url, nil, nil, postdata, header], [VT_BYREF|VT_VARIANT, VT_BYREF|VT_VARIANT, VT_BYREF|VT_VARIANT, VT_ARRAY|VT_UI1, VT_BYREF|VT_VARIANT])  @lastargs = WIN32OLE::ARGV  retendまず、特異メソッドについて軽くおさらいしましょう。
Win32OLE 活用法の第2回ですでに使っていますが、Ruby にはインスタンスごとに固有のメソッドを定義してあげる方法があります。一般にメソッドは、クラスに対して定義し、そのクラスに属するすべてのオブジェクトに対してそのメソッドが定義されます。この特異メソッドの定義では、その特異メソッドを定義したその単体のオブジェクトに対してのみ、そのメソッドは定義されます。
この場合では、ie というオブジェクトに対してのみ、navigate_post というメソッドを定義しています。 ie は、WIN32OLE クラスのインスタンスなわけですが、WIN32OLE クラスのすべてのインスタンスに対して、 navigate_post が定義されたわけではありません。それが、特異メソッドの意味です。
特異メソッドは、あるインスタンスにのみメソッドを追加定義したい場合に便利です。
特異メソッドとして定義した navigate_post メソッドは2つの引数をとります。 CGI の url と POST する文字列 query_string です。
ここで、navigate_post メソッドの中で実際に Navigate2 メソッドを呼び出し、ページの移動を実行する _invoke メソッドの呼び出しについて学んでいきましょう。
  ret = self._invoke(navi.dispid,[url, nil, nil, postdata, header], [VT_BYREF|VT_VARIANT, VT_BYREF|VT_VARIANT, VT_BYREF|VT_VARIANT, VT_ARRAY|VT_UI1, VT_BYREF|VT_VARIANT])WIN32OLE#_invoke メソッドは自分で Ruby の型から COM の型への変換を指定したいときに使用できるメソッドです。
WIN32OLE#_invoke dispid,args,types と _invoke は3つの引数をとります。dispid はメソッドの dispid で WIN32OLE_METHOD#dispid で取得できます。
args は引数の配列を指定し、types は WIN32OLE::VARIANT の定数で args の各々の引数の COM の型を指定します。types は args と同じ長さの配列になります。 VT_~~ という定数は WIN32OLE::VARIANT で定義された定数になります。
Navigate2 というメソッドは、第4引数に Post するデータのバイト配列を渡す仕様になっています。しかしながら、Win32OLEリファレンスにありますように MFC の特殊な仕様のため、バイト配列は VARIANT にラップすることが推奨されています。その結果、適切な型情報を得ることができないため、文字列をバイト配列に変換すべきことを正しく判定できません。 Ruby の文字列を VARIANT に変換するときは、デフォルトでは BSTR という COM の型に変換します。バイト配列には自動的に変換してくれないのです。
そのため、明示的に Ruby の文字列をバイト配列に変換する必要があります。それが、VT_ARRAY|VT_UI1 という記述です。このように指定することで、第4引数に VARIANT のときのデフォルトの BSTR ではなくバイト配列を渡すことができます。
ではどうやって、Ruby の文字列からバイト配列を得ることができるのでしょうか?
  postdata = query_string.unpack("C*")という行で行っています。String#unpack("C*") と実行することで、 pack テンプレート文字列で説明されているように文字列は 8bit 符号なし整数の配列になります。
Navigate2 メソッドの第5引数では、ヘッダを送ることになっています。 Post メソッドのときは "Content-type: application/x-www-form-urlencoded" を渡します。これは一種のおまじないで POST メソッドのときはこうすると覚えてください。

考えられる応用と発展的話題バイト配列に関係する落とし穴は Win32OLE をやっているとときおり現れます。今回は Internet Explorer の例でしたが、他の COM オブジェクトでもバイト配列を渡すには同様の方法が必要になります。
VARIANT が BSTR を受け付けるようになっているのか、バイト配列を受け付けるようになっているのかは、タイプライブラリを調べるだけでは判別不能です。そのため、MSDN などでその COM オブジェクトのドキュメントを調べられる場合は、そうすれば分かりますが、ドキュメントが充実していない場合などは試行錯誤する以外に方法はないことがあります。こういうときは、irb を使えばインタラクティブにいろいろと実験ができます。いろいろと試しながら動かしていってください。
POST データを送ることができるようになれば、Web 巡回の幅が広がるでしょう。
また、VARIANT の型渡し関係で、トラブルが起こることはあります。そのようなときは、WIN32OLE#_invoke で引数の型を明示的に指定することで呼び出し可能かどうか試行錯誤してみると解決できる可能性があります。




おわりに今回は、Web の自動巡回を題材に Windows Script Host と Internet Explorer の制御について紹介しました。
Windows Script Host にはここで紹介したキーボードシミュレーション以外にも、非常に多くの機能があります。たとえば、ネットワークドライブのマウントや特殊フォルダへのアクセスなどが可能です。 Windows Script Host に関するドキュメントは非常に豊富です。いろいろと調べてください。
次回は最終回です。Python や Perl など他の言語で COM を扱うときどのような違いがあるのかについて紹介していこうと思います。




 参考サイト



著書についてcuzic です。
関西 Ruby 勉強会などで積極的に Ruby コミュニティの活動をしています。前回は、50人以上の参加者があつまり、非常ににぎやかで楽しかったです。
cuzic ははてなを使ってブログを執筆中です。興味のある方はここをクリックしてください。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-8-1 08:22:15 | 显示全部楼层
【第 7 回】 ほかの言語での COM



はじめにcuzic です。
とうとう、今回の記事でこの Win32OLE の連載も最終回を迎えます。感慨深いです。皆さんの応援や素晴らしいアドバイザーの方々の助言、そしてみなさんのご愛読の賜物であると思っております。
今回のこの記事はこれまでの連載の中でもかなり毛色が変わった記事になります。というのは、実は Ruby のことではなくて、Ruby 以外の他の言語において、OLE/COM/ActiveX の技術がどう利用できるのかという内容について説明するものだからです。
海外に行くと日本の文化や人間同士のコミュニケーションについて意外な点に気づかされた経験なんかはありませんか?他の会社に行った昔の友達と話したとき、自分の会社がどんな会社であるかに改めて気づくことができたりしますよね。
今回は、あえて Ruby 以外の言語での COM サポートについて紹介することで、Ruby での COM がどういう実装なのかということや COM という技術がどういう技術なのかということについて理解を深めてもらうということを狙っています。
今回の記事では次の各種言語での COM 技術について紹介しようと思っています。
  • VBScript の場合
  • JScript の場合
  • Python の場合
  • Perl の場合
  • Tcl の場合
  • Java の場合
  • Lua の場合
  • Ruby の場合
  • その他の言語の場合
それでは、順に説明していきましょう。
なお、今回の記事は各種言語において現状で筆者が調査できた範囲での COM 技術について紹介することが目的です。筆者の調査の限界や言語への理解などの不足によって、十分に正確でない記述があるかもしれないことを最初にお詫びしておきます。

比較する内容について各言語での COM 実装で、COM の機能の中でどれだけ使えるかについてこの記事では比較していきます。例えば、イベントの実装方法、COM サーバを作成できるか、コレクションの要素をイテレートする方法、タイプライブラリで定義された定数の利用法などについて説明していこうと思います。
さらに、Internet Explorer を使ってイベントなどを扱うような簡単なスクリプトを提示して、比較していきます。
この記事を読んでいる人の多くは今回扱う言語をすべて知っているわけではないでしょうが、それぞれの言語の文法などについての解説は行いません。言語の文法について知りたい方は各自でそれぞれの言語の解説を行っているページなどを参考にしてください。また、言語の比較などについて知りたい方は、 Ruby versus Smalltalk versus Objective-C versus C++ versus Java versus Python versus CLOS versus Perl5 を参照してください。




ほかの言語での COM
VBScript の場合VBScript は、Microsoft 社が提供しているスクリプト言語で、 Visual Basic と非常によく似た言語体系になっています。
VBScript の特徴は、Windows 標準で使える WSH (Windows Script Host) でサポートされているスクリプト言語である点でしょう。そのため、どこでも動くということが必要とされる場合で COM の技術を使いたい場合に重宝します。
説明はこれくらいにして、VBScript での COM を使うスクリプトの例を紹介しましょう。
ie.vbs    1|Sub ie_DownloadComplete()   2|     WScript.Echo "Download Complete"   3|End Sub   4|   5|Dim ie   6|Set ie = WScript.CreateObject("InternetExplorer.Application","ie_")   7|   8|ie.Visible = True   9|ie.GoHome()  10|While ie.ReadyState <> 4  11|  WScript.Sleep(1000)  12|Wend  13|  14|ie.Navigate("http://www.ruby-lang.org/")  15|While ie.ReadyState <> 4  16|  WScript.Sleep(1000)  17|Wend  18|  19|Dim element  20|Dim count  21|count = 0  22|For Each element In ie.Document.all  23|  count = count + 1  24|Next  25|  26|WScript.Echo("complete" & vbCrLf & count & "elements found")
この例はとても簡単な例です。 VBScript で Internet Explorer を起動して、ホームページ(ブラウザで表示される最初のページ)を表示させて、その表示が完了すればその次に Ruby のページを表示させています。このスクリプトではイベントハンドラを使っています。 DownloadComplete というイベントが発生する度に 「Download Complete」というメッセージをポップアップさせています。
これからもこのスクリプトと同様の動作をするスクリプトを用いて、各種言語での COM 実装について説明していきます。この動きについてはよく理解しておきましょう。
VBScript で COM を利用するときは、WScript.CreateObject メソッドを呼び出します。他に CreateObject 関数を使う方法もあります。この 2 つは、よく似ています。しかしながら、 CreateObject が VBScript で用意されている関数であるのに対して、 WScript.CreateObject は WSH のメソッドです。
この2つは COM オブジェクトを返す点は同じですが、とる引数が違います。
1番目の引数は同じように ProgID をとりますが 2 番目の引数は、違います。 WScript.CreateObject メソッドは、2 番目の引数として イベント処理を行う関数の名前のプレフィックスをとります。 CreateOjbect 関数ではオブジェクトを生成するコンピュータの名前を指定します。
このスクリプト中では、WScript.CreateObject メソッドを使うことで、イベント呼び出しを行うプレフィクスを指定しています。これで、イベントが発生したときは、「ie_イベント名」という名前のメソッドを探して、あればそれを実行します。
VBScript では、COM のタイプライブラリで定義された定数を利用することはできません。そのため、オブジェクトブラウザなどであらかじめ調べて、その値を直接スクリプト中に書く必要があります。
例えば、次のところでは、READYSTATE_COMPLETE という定数を使おうとしています。4 は、READYSTATE_COMPLETE です。
While ie.ReadyState <> 4  WScript.Sleep(1000)Wendタイプライブラリで定義された定数をどうしても使いたい場合は、拡張子を WSF として XML 形式のファイルを作成する方法があります。
このようにすれば、先ほどの 4 という数字が直書きされていた場合に、 READYSTATE_COMPLETE という定数で記述できます。
上と同じ処理を実行するスクリプトの場合は次のようになります。 (注、スクリプトを含んだファイルが添付できないようだったので、直書きです)
<package><job id="test"><object id="ie" progid="InternetExplorer.Application"/><reference object="InternetExplorer.Application"/><script language="VBScript">Sub ie_DownloadComplete()     WScript.Echo "Download Complete"End SubDim ieSet ie = WScript.CreateObject("InternetExplorer.Application","ie_") ie.Visible = Trueie.GoHome()While ie.ReadyState <> READYSTATE_COMPLETE  WScript.Sleep(1000)Wend ie.Navigate("http://www.ruby-lang.org/")While ie.ReadyState <> READYSTATE_COMPLETE  WScript.Sleep(1000)WendDim elementDim countcount = 0For Each element In ie.Document.all  count = count + 1NextWScript.Echo("complete" & vbCrLf & count & "elements found")</script></job></package>XML 形式でどの ProgID を使うのかについて明示的に指定することで、定数を記述できるようになります。
なお、上記の XML は 「<」「>」「&」というような記述がそのまま使われている点が気になる方もいらっしゃると思います。今回のスクリプトはこのままで動作します。
それは、WSF の形式として、一番最初に XML 宣言を記述しなければルーズに解釈するからです。詳しくはWindows Script Components <?XML ?>を参照してください。
本来記事として公開するものとしては、XML の形式に厳格な方が好ましいのかもしれませんが、今回は読みやすさを重視してルーズな書き方をしました。ご了承ください。

JScript の場合JScript というのは、Netscape の JavaScript に互換性がある言語として Microsoft が実装した言語です。 JScript の特徴は、JScript Feature Informationで簡潔にまとまっています。
一般に HTML に埋め込んで Web ページを動的にするために用いられる JScript ですが、COM クライアントを作成するための言語としても使うことができます。
VBScript とともに Windows Script Host で標準でサポートされている言語です。
文法上の違いはもちろんありますが、JScript を COM で利用するときの特徴は VBScript の場合ととてもよく似ています。似ている例として、イベントハンドラの定義の仕方や、タイプライブラリで定義された定数を使用することについてよく似ています。
VBScript のところと同じ処理を行うコードの例は次のとおりです。
ie.js    1|var ie = WScript.CreateObject("InternetExplorer.Application","ie_");   2|   3|ie.Visible = 1;   4|ie.GoHome();   5|while (ie.ReadyState != 4) {   6|  WScript.Sleep(1000);   7|}   8|   9|ie.Document.onclick = function (){  10|  WScript.Echo("click!!");  11|};  12|  13|  14|ie.Navigate('http://www.ruby-lang.org/');  15|while (ie.ReadyState != 4) {  16|  WScript.Sleep(1000);  17|}  18|  19|var element,count=0;  20|for(element in ie.Document.all){  21|  count += 1  22|}  23|  24|WScript.Echo("complete\n" + count + " elements found\n");  25|  26|function ie_DownloadComplete() {  27|  WScript.Echo("Download Complete");  28|}  29|
文法上の違いを除いてはほとんど VBScript と同じような流れで記述可能であることが分かると思います。
個人的に JScript が面白いと思う点としては次のようなコードを記述可能な点です。
ie.Document.onclick = function (){  WScript.Echo("click!!");};JScript では関数はオブジェクトの一種ですので、変数に代入することもできます。そして、上記のように無名関数を生成して、Document オブジェクトのイベントハンドラとして使用するために、イベントである onclick というプロパティに代入できます。

Python の場合Python は動的型付けのオブジェクト指向スクリプト言語の1つです。 Ruby と比較されることが多いため、ご存知の方も多いと思います。 Python の公式サイトは Python Programming Language です。
Python での COM 技術は、とても幅広くサポートされています。特筆すべき点として、COM サーバが作成可能であることです。 Python での COM サーバの作成については洋書で良ければ参考書籍もあり、 Google で調べれば様々な情報が手に入ります。
また、COM MakePy ユーティリティによって、アーリーバインディングがとても簡単に行えます。さらに、ctypes というライブラリを利用することによって、型変換が非常に手軽に行えます。
では、VBScript で行ったのと同様の例を Python で説明しましょう。
ie.py    1|import win32com.client   2|from win32com.server import util   3|import pythoncom   4|   5|class WebBrowserEvent:   6|    def OnDownloadComplete(self,*args,**kwds):   7|        print "Download Complete"   8|    def OnQuit(self,*args,**kwds):   9|        exit  10|  11|def com_collection_iter(collection):  12|    return (collection.Item(index)  13|            for index in range(1, collection.Count+1))  14|  15|#ie = win32com.client.Dispatch("InternetExplorer.Application")  16|ie = win32com.client.DispatchWithEvents("InternetExplorer.Application",WebBrowserEvent)  17|ie.Visible = True  18|ie.GoHome()  19|while ie.ReadyState != win32com.client.constants.READYSTATE_COMPLETE:  20|    pass  21|  22|ie.Navigate('http://www.ruby-lang.org/')  23|  24|while ie.ReadyState != win32com.client.constants.READYSTATE_COMPLETE:  25|    pass  26|  27|count = 0  28|for element in ie.Document.all:  29|    count += 1  30|print "complete\n %d elements found\n" % (count)
Python で、COM オブジェクトを作成するには、単に win32com.client.Dispatch というメソッドを使います。今回は、イベントハンドラを定義して、そのイベントハンドラを使う形で COM オブジェクトを作成しようとします。イベントハンドラを指定して COM オブジェクトを作成するときは、win32com.client.DispatchWithEvents を使います。
Python では、タイプライブラリで定義された定数を利用できます。 Ruby では、指定したモジュール内に定義されますが、Python では win32com.client.constants という名前空間で定義されます。
COM の定数を利用するには、あらかじめ MakePy ユーティリティを使って、利用可能な状態にしておく必要があります。 MakePy ユーティリティは、PythonWin というツールからメニューで選択できます。
Python でのイベントハンドリングは、上記のスクリプトのようにあらかじめイベントハンドラのためのクラスを定義することによって行います。
この節は ActiveState からダウンロードできる ActivePython のバージョン 2.4.1 で検証しました。

Perl の場合Perl をご存知でない方はおそらくいないでしょう。 Perl についての情報はPerl.com から得られます。 Ruby の中には Perl を参考にしてもたらされた機能がいくつもあります。
Perl では、COM オブジェクトを作成するには、Win32::OLE というモジュールを使います。Win32OLE ライブラリの作者の助田さんによると、このライブラリ名は Perl の Win32::OLE という名前を参考にしてつけられたらしいです。
Perl で同様に InternetExplorer を制御するスクリプトを紹介すると次のようになります。
ie.pl    1|use Win32::OLE qw(EVENTS in);   2|use Win32::OLE::Const ('Microsoft Internet Controls');   3|   4|$| = 1;   5|my $ie = Win32::OLE->new('InternetExplorer.Application');   6|Win32::OLE->WithEvents($ie,"WebBrowserEvents","DWebBrowserEvents2");   7|$ie->{Visible} = 1;   8|$ie->GoHome();   9|Win32::OLE->MessageLoop();  10|while($ie->ReadyState() != READYSTATE_COMPLETE){  11|}  12|  13|$ie->Navigate('http://www.ruby-lang.org/');  14|  15|Win32::OLE->MessageLoop();  16|while($ie->ReadyState() != READYSTATE_COMPLETE){  17|}  18|  19|my $count = 0;  20|foreach my $element (in $ie->Document->all){  21|    $count++;  22|}  23|print "complete\n ${count} elements found\n";  24|  25|package WebBrowserEvents;  26|sub DownloadComplete {  27|    my ($obj,@args) = @_;  28|    print "Download Complete\n";  29|}  30|sub NavigateComplete2 {  31|    my ($obj,@args) = @_;  32|    Win32::OLE->QuitMessageLoop();  33|}
my $ie = Win32::OLE->new('InternetExplorer.Application');という行で InternetExplorer のオブジェクトを生成しています。
Win32::OLE->WithEvents($ie,"WebBrowserEvents","DWebBrowserEvents2");で、イベントを WebBrowserEvents というパッケージに定義されたイベントハンドラで処理させることができます。
あと、Perl の Win32::OLE->MessageLoop() は、Ruby の WIN32OLE_EVENT.message_loop とは動作が違います。 Perl では、Win32::OLE->QuitMessageLoop() が呼び出されることで、メッセージループを抜けます。 Ruby では、一瞬メッセージループを通ったのちにまたすぐに処理が Ruby 側に戻ります。
あと、Perl では定数をロードするために、最初に
use Win32::OLE::Const ('Microsoft Internet Controls');と宣言しています。これを書くことによって、スクリプト中に READYSTATE_COMPLETE という定数を使うことができます。
この節は、ActiveState からダウンロードできる ActivePerl の バージョン 5.8.7.813 で検証しました。

Tcl の場合Tcl というのは、Tcl/Tk に採用されているプログラミング言語として有名です。 Tcl Developer Siteで様々な情報が得られます。 Tcl には tcom というライブラリがあり、 tcom を使うことで Tcl を使って、COM のテクノロジを利用できます。 Tcl で、COM のサーバも作成可能です。
今までと同様の処理を行うスクリプトを紹介しましょう。
ie.tcl    1|package require tcom   2|   3|proc sink {method args} {   4|    if {$method eq "DownloadComplete"} then {   5|        puts "event $method $args"   6|    }   7|}   8|   9|set ie [::tcom::ref createobject "InternetExplorer.Application"]  10|::tcom::bind $ie sink  11|$ie Visible 1  12|$ie GoHome  13|while {[$ie ReadyState] != 4} {  14|}  15|  16|$ie Navigate "http://www.ruby-lang.org/"  17|  18|while {[$ie ReadyState] != 4} {  19|}  20|  21|set count 0  22|set elements [[$ie Document] all]  23|::tcom::foreach element $elements {  24|    incr count  25|}  26|puts "complete\n $count elements found"
Tcl で、COM オブジェクトを作成するには、
set ie [::tcom::ref createobject "InternetExplorer.Application"]と実行します。
そして、イベントハンドラを使うには、次のように実行します。
::tcom::bind $ie sinkTcl の場合は、様々な COM のイベントをひとつのプロシージャで処理することになります。
あと Tcl の場合は、foreach のループを Tcl の言語が用意しているものではなく、 tcom のライブラリで提供しているものを使う必要があるという点が特筆すべきかもしれません。
Tcl で COM の定数が使えるのかどうかについては調査不足で調べきれませんでした。
この節は、ActiveTcl バージョン 8.4.11.0 で検証しました。

Java の場合Java を使えるプログラマの数はとても多いことでしょう。 Java という文化は 100% Pure であり Microsoft の技術を使わないことにこだわる部分があるので、意外に思われる方がいるかもしれませんが、 Java でも COM のテクノロジを利用できるようにするプロジェクトがあります。もちろん、J++J# のような Microsoft が関与している Java の処理系で COM を使えますし、それ以外にも jacobjcom という Java で COM を利用可能にするライブラリがあります。
ここでは jacob を使った例を紹介します。今回のプログラムで行う処理は今までと全く同じわけではありませんが、ご了承ください。
InternetExplorer.java    1|import com.jacob.com.*;   2|import com.jacob.activeX.*;   3|   4|public class InternetExplorer   5|{   6|    public static void main(String[] args)   7|    {   8|        ActiveXComponent iecom = new ActiveXComponent("InternetExplorer.Application");   9|        Object ie = iecom.getObject();  10|        Dispatch.put(ie, "Visible", new Variant(true));  11|        IEEvents ieE = new IEEvents();  12|        DispatchEvents de = new DispatchEvents((Dispatch) ie,ieE,"InternetExplorer.Application");  13|        Dispatch.callSub(ie,"GoHome");  14|        try {  15|            while(Dispatch.get(ie,"Busy").toBoolean() == new Boolean(true)){  16|                Thread.sleep(4000);  17|            }  18|            Dispatch.call(ie,"Navigate",new Variant("http://www.ruby-lang.org/"));  19|            while(Dispatch.get(ie,"Busy").toBoolean() == new Boolean(true)){  20|                Thread.sleep(4000);  21|            }  22|            Dispatch document = Dispatch.get(ie,"Document").toDispatch();  23|            Dispatch all = Dispatch.get(document,"all").toDispatch();  24|            int num_element = Dispatch.get(all,"length").toInt();  25|            System.out.println("complete\n" + num_element + "elements found");  26|        }catch(InterruptedException e){  27|            e.printStackTrace();  28|        }finally{  29|            iecom.invoke("Quit",new Variant[]{});  30|        }  31|    }  32|}  33|  34|class IEEvents  35|{  36|    public void DownloadComplete(Variant[] args) {  37|        System.out.println("DownloadComplete");  38|    }  39|}
このプログラムで、Java から COM を呼び出すことができます。 Java の場合は、今まで紹介した言語と違って、静的型付け言語である点が色濃く反映されたコードになっています。
つまり、メソッドを動的に呼び出すことができないため、Dispatch.get のようなメソッドを用いて、プロパティの値を取得したり、その取得した値を明示的に型変換したりする必要があります。
COM オブジェクトの作成は次のようにします。
       ActiveXComponent iecom = new ActiveXComponent("InternetExplorer.Application");       Object ie = iecom.getObject();イベントを処理するクラスを次のように定義します。
class IEEvents{    public void DownloadComplete(Variant[] args) {        System.out.println("DownloadComplete");    }}上記のクラス定義を見ると分かるように IEEvents クラスは、なんらかのインタフェースを implememts するというような記述を書く必要がありません。これは、Java らしいスタイルではなく少し意外な感じがします。
タイプライブラリに定義された定数の取得は、私が調査した限りではできないようでした。
この節は、Sun の JSK 1.5.0_14 、 jacob Version 1.9 で検証しました。

Lua の場合プログラミング言語 Lua は、ブラジルで開発されたプログラミング言語で、非常に軽量で組み込み用途に向いている言語です。 Lua は手続き記述型の簡素な言語ですが、オブジェクト指向的な仕組みを提供し、動的な型付けやガベージコレクションなど便利な機能があります。最近個人的に注目している言語です。 LuabindLuanet、など、興味深いプロジェクトが精力的に活動しています。 リファレンスマニュアルの日本語訳 もあります。
そして、Lua の一種として COM の技術を利用可能にした LuaCOM もあります。
LuaCOM は、COM サーバーも作成可能でかつ、COM クライアントも作成可能な Lua のライブラリ兼実行形式です。
では、他の言語と同じように Lua で IE を操作するデモを紹介しましょう。
ie.lua    1|require("luacom")   2|   3|local ie = luacom.CreateObject("InternetExplorer.Application")   4|local events_handler = {}   5|function events_handler:DownloadComplete()   6|  print "Download Complete"   7|end   8|local cookie = luacom.Connect(ie,events_handler)   9|  10|ie.Visible = 1  11|ie:gohome()  12|while ie.ReadyState ~= 4 do  13|end  14|  15|ie:Navigate("http://www.ruby-lang.org/")  16|  17|while ie.ReadyState ~= 4 do  18|end  19|count = 0  20|for index,element in luacom.pairs(ie.Document.all) do  21|  count = count + 1  22|end  23|print(string.format("complete \n%d elements found",count))
次の行で Lua での COM オブジェクトの作成を行っています。
local ie = luacom.CreateObject("InternetExplorer.Application")Lua でのイベントの取り扱いについては、イベントの実装を行ってるテーブルを COM オブジェクトに接続することによって行います。次の 4 行がこの処理に該当します。なお、テーブルというのは Lua の基本的データ構造です。テーブルは Ruby でのハッシュや配列、はたまたクラスを表現するときにも使える非常に強力なデータ構造です。
local events_handler = {}function events_handler:DownloadComplete()  print "Download Complete"endlocal cookie = luacom.Connect(ie,events_handler)LuaCOM には LoadConstants という定数をロードするためと思われる API があるのですが、いかんせん使い方がよく分からなかったので使っていません。
Lua でコレクションの各要素に対してイテレーションを行うには、 luacom.pairs という関数呼び出しを使います。
for index,element in luacom.pairs(ie.Document.all) do  count = count + 1endなお、
local cookie = luacom.Connect(ie,events_handler)の cookie という返り値は使っていませんが、これを利用して、 LuaCOM ではコネクションを開放できます。
この節は、LuaCOM Version 1.5 で検証しました。

Ruby の場合最後に Ruby の場合に上記の例がどうなるのかを紹介しましょう。
ie.rb    1|require 'win32ole'   2|   3|module InternetExplorer   4|end   5|   6|ie = WIN32OLE.new('InternetExplorer.Application')   7|WIN32OLE.const_load(ie,InternetExplorer)   8|   9|event = WIN32OLE_EVENT.new(ie,"DWebBrowserEvents2")  10|event.on_event("DownloadComplete") do   11|  puts "Download Complete."  12|end  13|  14|ie.Visible = true  15|  16|ie.GoHome  17|while ie.ReadyState != InternetExplorer::READYSTATE_COMPLETE  18|  WIN32OLE_EVENT.message_loop  19|end  20|  21|ie.Navigate 'http://www.ruby-lang.org/'  22|while ie.ReadyState != InternetExplorer::READYSTATE_COMPLETE  23|  WIN32OLE_EVENT.message_loop  24|end  25|  26|count = 0  27|ie.Document.all.each do   28|  count += 1  29|end  30|puts "complete\n#{count} elements found"
Ruby での Win32OLE には次のような特徴があります。
  • 定数をロードする名前空間を用意するために InternetExplorer モジュールを定義
  • COM オブジェクトの作成は、WIN32OLE.new(progid)
  • タイプライブラリの定数をロードする処理はメソッド呼び出しで行われる
  • イベントの作成は WIN32OLE_EVENT クラスのインスタンスを生成して行う
  • イベントハンドラの登録は命名規則に沿ったメソッド定義ではなく on_event メソッドの呼び出しで行う
このスクリプトを見て思う点は、Ruby は非常に柔軟で動的な言語であるということです。 Ruby の定数が動的にロードできるという点や、ブロック付メソッド呼び出しが文法として用意されている点が十分に活用されて、Win32OLE のライブラリが作られているということを感じます。




その他の言語での場合ここで紹介した以外の言語でも、COM は使えます。
たとえば、Microsoft が提供している数多くの言語で COM は利用可能です。 .NET Framework では、タイプライブラリをインポートすることで COM の呼び出しが可能になることは有名だと思います。その機能を利用することによって、.NET Framework 上で動くさまざまな言語で COM の技術を使用できます。もちろん C# でも使えます。たとえば毛色の変わったところで .NET Framework で動く OCaml ベースの関数型言語である F# のような言語で COM を使えます。関数型言語で自由にプログラミングできるとうきうきします。
また、Groovy という最近注目されている Java 上でのスクリプト言語でもこの記事でも取り上げた jacob と組み合わせて COM を操ることができる Scriptom という Groovy モジュールがあります。
ただ、Scriptom は筆者が調査した限りではイベントハンドリングなどいくつかの点でまだまだ心許ない部分が多いようでした。
変わりどころでは、xyzzy という Emacs と似た操作性を持つエディタの Lisp 環境でも COM を使うことができます。
xyzzy で COM を扱う簡単なデモを紹介すると次のような感じです。
(require "ole")(defvar ie (ole-create-object "InternetExplorer.Application"))(setf #{ie.Visible} t)(ole-method ie 'navigate "http://www.ruby-lang.org/")エディタからブラウザや Excel はたまた iTunes などの COM 対応のアプリケーションをぐいぐい動かせると思うと楽しいですね。
なお、xyzzy の COM/OLE Automation の機能が実装される際には Ruby の Win32OLE を参考にされたようです。そう聞くと少し親近感がわいてきます。
ただ、xyzzy というエディタは実際使おうとするとドキュメントがどこにあるのかよく分からないところがあるので、注意が必要です。
同様にエディタから COM 呼び出しができる例として、EmEditor があります。興味があれば調べてみると面白いかもしれません。
今回紹介しませんでしたが、最近 Ruby コミュニティで注目されている Haskell という言語でも COM/ActiveX を利用できる HaskellScript があります。




他の言語からの Ruby 呼び出し最後にもう少し Ruby についての話題に触れておきます。他の言語から Ruby のメソッドを呼び出す方法として、 ActiveScriptRuby を使う方法があります。
次のデモのコードが分かりやすいかもしれません。
ruby.vbs    1|Dim sc   2|   3|Set sc = CreateObject("ScriptControl")   4|sc.Language = "GlobalRubyScript"   5|   6|sc.AddCode("def add x,y;x + y;end")   7|   8|WScript.Echo(sc.Run("add",2,3))
Ruby で定義した add というメソッドを Visual Basic から利用しています。 ScriptControl という COM オブジェクトと ActiveScriptRuby を使用することで、 COM を使用できる任意の言語に Ruby を埋め込んで使うことができます。 ActiveScript として使える言語としては私の調べた限り、JScript、VBScript、Ruby、Python、 Perl、Tcl、PHP があります。つまり、COM を使用できる任意の言語から、JScript/VBScript/Ruby/Python/Perl/PHP などを埋め込んで使うことができます。次の EmEditorのマクロを様々なActiveScriptで書いてみる で行われているようなことが可能です。
Excel VBA などを書いているときにどうしても Visual Basic で実現できないことを Ruby でやりたいときなどに便利かと思います。




発展的な話題COM は様々な言語で利用可能なようにインタフェースを定めており、そのおかげで COM のインタフェースを用意するだけで、開発者は自分の好きな言語でその COM サーバを制御するスクリプトを書くことができます。
また、COM サーバも様々な言語で書くことができ、 Python や Tcl、Lua のような言語で作った COM サーバを他の様々な言語で呼び出して使うことができます。他にも Windows スクリプト コンポーネント を利用すれば、Perl や Ruby といった ActiveScript に対応した言語ならば実は COM サーバを作成可能です。 Perl の場合は、ActivePerl Documentation の Windows Script Componentsの説明が参考になるでしょうし、 Ruby の場合は Ruby を256倍使う本 邪道編 が参考になるでしょう。
Microsoft は、COM で実現した様々な言語を混在して使える環境をさらに発展させたものとして、現在 .NET と総称して呼ばれる一連の環境を提供しています。
Microsoft のロードマップにしたがって言うと、この OLE/COM/ActiveX のテクノロジは .NET ベースの言語にこれから置き換えられていくことと思います。 .NET は最初スクリプト言語への対応が弱かったのですが、だんだんと面白いものが提案されてきました。 IronPythonRuby/.NET Bridge など興味深いです。とくに Microsoft Research で研究されている言語は面白いです。プログラム言語が好きなら、 Programming Principles and Tools で何が行われているかは、追いかけて損はないと思います。
また、日本の未踏ソフトウェア創造事業で取り上げられたものとして Ruby.NET コンパイラの開発 もあります。




終わりに今回でこれまで連載を続けてきました Win32OLE 活用法は終わります。最終回は Ruby の雑誌なのに Ruby のスクリプトがあまり出てこない不思議な回でした。楽しんでもらえましたでしょうか?
つまらない日常の仕事は、ちょっとしたプログラミングをすることでもっと楽に行えるようにできると思います。そのためにこの記事が一助になれば幸いです。




参考サイト



著者についてcuzic は、プログラミング言語について調べることが好きです。新しい未知の言語の特徴について調査したり、 OLE/COM/ActiveX や .NET 、luabindBoost.Python といった言語を横断するような技術に強い興味があります。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注~册

本版积分规则

小黑屋|手机版|咖啡日语

GMT+8, 2024-5-12 15:55

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表