Archive for 12月, 2017

Common Lispからweb browserを操る

水曜日, 12月 27th, 2017

Web browser (以下、ブラウザ) 上でのちょっとした作業を自動化したいことは時折あると思います。そういった自動化をCommon Lispで出来ないものかと思い、色々と調べながら試してみました。

Selenium

テキトーに調べた所、Seleniumというのを使うと簡単にブラウザ上での作業を自動化できるようです。ブラウザを動かしたいマシンでSelenium Standalone Serverとやらを動かして、そこにHTTPリクエストを投げるとそれに応じてブラウザが色々やってくれるとかなんとか。とにかく自分で書く自動化用のプログラムはHTTPリクエストを投げるだけで、難しいところはSeleniumが全部やってくれると。簡単。詳しくは詳しい人に聞いてください。

CL Selenium WebDriver

Common Lispで直接HTTPリクエストを投げてもいいのですが面倒なので、便利なライブラリがないか探してみた所、CL Selenium WebDriverというものを見つけました。サンプルコードを見る限り簡単そうです。

(with-session ()
  (setf (url) "http://play.golang.org/?simple=1")
  (let ((elem (find-element "#code" :by :css-selector)))
    (element-clear elem)
    (element-send-keys elem *code*))
  (let ((btn (find-element "#run")))
    (element-click btn))
  ...)

READMEにある通り、Gitリポジトリを ~/quicklisp/local-projects/ でcloneしてQuicklispで (ql:quickload :selenium) としてやればすぐ使えます。
ちなみにQuicklispにはcl-seleniumというプロジェクトが登録されており、何もせずに (ql:quickload :selenium) を評価すると別のものがインストールされます。 ~/quicklisp/local-projects/ 以下に同名のプロジェクトがある場合、そちらが優先されるようです。詳しくは詳しい人に聞いてください。

試してみた

試しにブラウザを起動してAmazonで検索をするプログラムを書いてみました。

> (ql:quickload :selenium)
(:SELENIUM)

> (selenium:with-session ()
  (setf (selenium:url) "https://www.amazon.co.jp/")  ; Amazonを開いて
  (let ((box (selenium:find-element "field-keywords" :by :name))
        (btn (selenium:find-element "input.nav-input")))
    (selenium:element-send-keys box "ご注文はうさぎですか?")  ; 検索ボックスに文字列を入力し
    (selenium:element-click btn)))  ; 検索ボタンをクリック
T

見事動くのですが、コレだけだとTが得られるだけであまりおもしろくありません。せっかくだからスクリーンショットでも取ろうかと思ったら、なんとCL Selenium WebDriverにはスクリーンショットを撮るための機能が提供されていません。Selenium serverに簡単なGETリクエストを送るとスクリーンショットが撮られ、base64エンコードされたpng画像が返ってくるようなので、自分でスクリーンショットを撮る関数を書いてみました。

(ql:quickload :cl-base64)

(defun screenshot (file &key (session selenium::*session*))
  (with-open-file (stream file :direction :output :if-exists :supersede
                          :element-type '(unsigned-byte 8))
    (map
     nil
     #'(lambda (x) (write-byte x stream))
     (cl-base64:base64-string-to-usb8-array
      (selenium::http-get-value (selenium::session-path session "/screenshot"))))))

他人のパッケージだろうと自由にイジれるのがCommon Lispのいいところ(publicおじさん並感)。CL Selenium WebDriverの関数やら変数やらを好き勝手使いGETリクエストを送り、base64デコードしてファイルに書き込みます。これで無事スクリーンショットが撮れました。

小学生並みの感想

動くには動いたのですが、見ての通りCL Selenium WebDriverの機能が足りなかったり、さらにはバグもあったりするので、宗教上の問題がない限り、素直にPythonでも使ったほうが簡単です。