Safariでbotを作るぞ
せっかくMac miniをサーバーにするので、いつもと違う方法でWebページを読みたいぞ。
いつもは?
いつもはopen-uriだったり、typhoeus(ぜんぜんスペルが覚えられない)を使って読んでいます。 SPAなどのように内部でfetchしてくるページは、ちょっと解析してそのURLにアクセスしてます。
SafariをAppleScript(というかApple Event)でコントロールする
macOSのSafariはApple Eventでいろいろ操作できます。 Safariを制御できるで、ふだん人間が操作したかのように、自動処理できます。 ログイン処理を人間がしておいて、そのままのタブを使えば認証にまつわる面倒な処理を書かずにしみますし、 動的なページでJSの諸々の処理が実行されたあとのDOMを取り出したりすることもできます。
以下のコードは AppleScript でSafariにページを読ませて、3秒待ってページの中身をHTMLで取り出す例です。
ちなみに「スクリプトエディタ」で編集/実行できます。
tell application "Safari"
tell window 1
tell tab 1
set URL to "https://masaki.druby.work/2SSppR-Y59aOp-yMX3SU"
delay 3.0
get do JavaScript "(new XMLSerializer()).serializeToString(document)" as text
end tell
end tell
end tell
タブのURLプロパティにURLにsetするとロケーションバーでURLをタイプしたように動きます。
このWebページはSPAぽい実装、つまりロードが終わったら中身を動的にfetchして組み立てる、よくある方式です。 この完了をちゃんと待つうまい方法がなかったので、delayで雑に3秒待ってます。
その後do JavaScritp命令でタブの環境でJSを実行して結果をもらいます。JSの内容は、documentからなるDOM一式をHTMLにしています。 これは、ロード後の処理でいろいろDOMを組み立てたあとの内容を取得できるのです。便利ー!
RubyからAppleScriptする
AppleScriptで動くのはわかったけど、Rubyで書きたいですよね。 rb-scpt gemを使うとRubyからApple Eventが使えるようになります。難しいけど!
こんな感じ。
require 'rb-scpt'
require 'nokogiri'
require 'uri'
class Safari
def initialize(wait_sec=1.0)
@app = Appscript.app('safari')
@target = @app.windows[1].tabs[1]
@sec = wait_sec
end
def load(url, wait_sec=nil)
@target.URL.set(url.to_s)
# pp @target.do_JavaScript("document.readyState") == 'complete'
sleep(wait_sec || @sec)
@target.do_JavaScript("(new XMLSerializer()).serializeToString(document)")
end
end
safari = Safari.new
html = safari.load("https://masaki.druby.work/2SSppR-Y59aOp-yMX3SU")
document = Nokogiri::HTML.parse(html)
Nokogiriで作った木を操作すればページの解析ができます。
コメントアウトしてある問い合わせはロードが遅いページには役に立つかも。
document.readyState
がcompleteになっても、動的なページの構築が終わったかどうかはわからないのが罠。

