いとこんのメモ帳

コードの断片を置いたりします

MacRuby で Pasteboard 監視・HTML スクレイピング・WebView 操作

どういうのを作るか

英文のリソースを読んでいる最中、わからない単語が出てきたらオンラインの Cobuild (ここ) で検索してるんだけど、いちいちキーボード叩いたり、コピペするのはめんどくさい。

これを、Pasteboard を監視し、単語がコピーされたら自動でその単語の意味を表示出来たら難しい英文読むときに捗るかなと思い、ツールを作ってみることに。

どうやら、さくっと OS XGUI アプリを書きたい時に MacRuby がなかなかよいらしいので、これでちょっと組んでみる。OS X のバージョンは 10.7

MacRuby のインストール法、プロジェクトの作成は省略。

WebKit.framework の追加

WebView を使うには、WebKit.framework が必要になるので、これへのリンクを張る。

f:id:ito_konnyaku:20130117021245j:plain

アクセサの準備

WebView と連携するためでもある、アクセサを準備。AppDelegate.rb にはもう既に attr_accessor :window と記述されてるので、同じように attr_accessor :browser とかしてアクセサを準備する。

WebView 配置、アウトレット接続

こんな感じに InterfaceBuilder でさくっと WebView を配置。

f:id:ito_konnyaku:20130117022237j:plain

後は左ペインの "AppDelegate" から "WebView" へ右クリックしながらドラッグするなどして、先ほど追加したアクセサに接続。

Pasteboard 監視

OS X での Pasteboard は、コピーした瞬間に通知を受け取れるような API がなさそうなので、ポーリングをかけるとする。AppDelegate.rb にこんな感じで Pasteboard を監視するコードを書く。

def applicationDidFinishLaunching(a_notification)
    beginObservingPasteboard
end

def beginObservingPasteboard
    @changeCount = NSPasteboard.generalPasteboard.changeCount
    
    NSTimer.scheduledTimerWithTimeInterval(1,
                                           target:self,
                                           selector:"observePasteboard:",
                                           userInfo:nil,
                                           repeats:true)
end

def observePasteboard(timer)
    pboard = NSPasteboard.generalPasteboard
    if pboard.changeCount > @changeCount then
        pasteboardChanged pboard
    end
    
    @changeCount = pboard.changeCount;
end

def pasteboardChanged(pboard)
    classes = [ NSString ]
    objects = pboard.readObjectsForClasses(classes, options:nil)
    copied_str = objects[0]
    openURL("http://dictionary.reverso.net/english-cobuild/#{copied_str}");
end

beginObservingPasteboard メソッドで、タイマを登録。selector はただ単にメソッド名の文字列を渡せばよい模様。そして observePasteboardクリップボードのアイテムが増えたか判定、増えてたら (= コピーされたら) pasteboardChange を call, コピーされた文字列を取り出して、Web サイトアクセス開始する。openURL については後述

HTML スクレイピング

ただ単に Web サイトを表示するだけならここまでで OK だが、上述のサイトは不要な部分が多いので、ごにょごにょすることにする。

XML パーサでいろいろとやるのもいいけど、WebView に一旦コンテンツを読み込ませてから、内部の DOM ツリーを操作できるようなので、このアプローチでいくことに。

まずは HTML を読み込ませる、非表示の WebView を作成。AppDelegate の applicationDidFinishLaunching で作ってインスタンス変数にでも格納しておく。

@domParser = WebView.new
webFrameLoadDelegate = WebFrameLoadDelegate.new
webFrameLoadDelegate.browser = self.browser
@domParser.setFrameLoadDelegate(webFrameLoadDelegate)

この domParser には、フレームのロード完了通知を受け取るため、WebFrameLoadDelegate を登録する。この WebFrameLoadDelegate クラスは、外部のファイルに切り出している。WebFrameLoadDelegate には、結果表示も任せたいため、browser への参照を渡しておく。

前述の openURL メソッドで、HTML コンテンツのロードを開始する。

url = NSURL.URLWithString(url)
urlRequest = NSURLRequest.requestWithURL url
        
@domParser.mainFrame.loadRequest(urlRequest)

フレームのロードが完了すると、Objective-C の場合は、webView:didFinishLoadForFrame: が呼ばれるらしい。Obj-C ではこれに対応するため、プロトコルを導入して〜とかしなければならなかったが、MacRuby では Ruby の動的性を生かして、ただ単に def webView(sender, didFinishLoadForFrame:frame) というようなメソッドを定義するだけでよいみたい。らくちん。

以下 didFinishLoadForFrame の中身

document = frame.DOMDocument
htmlDivElement = document.getElementById("DivMainResults")
if (htmlDivElement != nil) then
    html = "<!doctype html><html><head></head><body>#{htmlDivElement.innerHTML}</body></html>"
    mainFrame = browser.mainFrame
    mainFrame.loadHTMLString(html, baseURL:NSURL.URLWithString("/"))
end

ここまで来ると、あとは WebView の Frame から、DOMDocument が取得してこれるので、こいつで欲しい部分 (ここでは "DivMainResults" を id にもつ部分ツリー) を取得し、適当なコンテナ HTML に流し込み、結果表示先の WebView (ここでは browser 変数) に読み込ませる。

以上で、クリップボードを監視し、コピーされた単語を Web サイトにリクエストし、結果から必要な部分を取り出し表示する、ということが出来るようになった。ちなみにスクリーンショット:

f:id:ito_konnyaku:20130117031543j:plain

おわりに

Ruby で書けるようになっているとはいえ、Objective-C のシンタックスを置き換えたような感じなので、Cocoa などの知識が必要になる。また、MacRuby のドキュメントもあまり充実しているとは言えないため、Apple 本家の Objective-C ドキュメントを読む必要があり、Cocoa Objective-C プログラミングしたことないような人には若干ハードル高いかなーと感じた。ただ、本ツールでは 60 行ほどしかコード書いてないので、経験のあるような人は楽に GUI アプリ書けるだろうなーとか思いました。