@m_seki の

I like ruby tooから引っ越し

Mac miniをサーバーにするぞ #4

Safaribotを作るぞ

せっかくMac miniをサーバーにするので、いつもと違う方法でWebページを読みたいぞ。

いつもは?

いつもはopen-uriだったり、typhoeus(ぜんぜんスペルが覚えられない)を使って読んでいます。 SPAなどのように内部でfetchしてくるページは、ちょっと解析してそのURLにアクセスしてます。

SafariAppleScript(というかApple Event)でコントロールする

macOSSafariApple Eventでいろいろ操作できます。 Safariを制御できるで、ふだん人間が操作したかのように、自動処理できます。 ログイン処理を人間がしておいて、そのままのタブを使えば認証にまつわる面倒な処理を書かずにしみますし、 動的なページでJSの諸々の処理が実行されたあとのDOMを取り出したりすることもできます。

以下のコードは AppleScriptSafariにページを読ませて、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になっても、動的なページの構築が終わったかどうかはわからないのが罠。

あわせて買いたい