HaskellでSeleniumを使ってみる@勉強会

2014/12/14

休日を利用して京都に帰って【CAMPHOR- × サイボウズ】Selenium 勉強会に参加してきました。主催してくださったCAMPHOR-とサイボウズの皆さん、楽しい勉強会をありがとうございました。 勉強会ではハンズオン形式でSeleniumを学んだのですが最初のスライドに非公式だけどHaskellでも使えるとあったので挑戦してみました。単身でのHaskell参戦でしたがせっかくなのでたまった知見を書き起こしておこうと思います。

Seleniumはプログラムから自動的にブラウザを操作することが出来るツールです。 WebサービスのUIテストに使われているようで勉強会では歴史的なとこからわかりやす解説してもらいました。

主にテストに使われているらしいのですが自動的にブラウザを操作することが出来るならもっと色々使えそうで夢が広がります。 そういえば友達がソシャゲの毎日ログインを自動化しようとしてたのでSeleniumをオススメできますね。その時はPhantomJSだけでゴリゴリ書いてたみたいですが… FirefoxとかChromeとかのブラウザを使ってテストができるのもSeleniumのいいとこですね!

Seleniumが公式でサポートしている言語はDocumentを見てみると[Java, C#, Python, Ruby, PHP, Perl, JS]のあたりのようです。これ以外にも非公式でライブラリが用意されていてかなり多くの言語から使うことができそうです。

さて肝心のHaskellからSeleniumを使う方法ですが「Haskell Selenium」でググると一番上に(2014/12/15調べ)出てくるwebdriverを使えば良さそうです. PCはMacでブラウザはGoogleChrome、以下今回の記事で使うバージョン一覧です。

name version
webdriver 0.6.0.3
Selenium Server 2.44.0
ChromeDriver 2.13

環境構築

作業用ディレクトリを作ります。

$ mkdir selenium-test
$ cd selenium-test

Selenium ServerとChromeDriverをここにダウンロードしてきてください

$ tree .
.
├── chromedriver
└── selenium-server-standalone-2.44.0.jar

さっそくSelenium Serverを起動しておきましょう

$ java -jar selenium-server-standalone-2.44.0.jar

webdriverを導入します。cabalコマンドで一発です。

$ cabal install webdriver

10分ぐらいかかるのでお茶でも飲みましょう。

Hello Selenium

環境構築が終わったらさっそくSeleniumを使っていきましょう! webdriverのREADMEに書いてあるコードをそのまま実行してみましょう。Chromeで使う想定なのでmyConfigの定義は修正しています。

$ vim hello.hs
{-# LANGUAGE OverloadedStrings #-}
import Test.WebDriver

myConfig :: WDConfig
myConfig = defaultConfig { wdCapabilities = (defaultCaps {browser = chrome})}

main :: IO ()
main = runSession myConfig $ do
  openPage "http://google.com"
  searchInput <- findElem (ByCSS "input[type='text']")
  sendKeys "Hello, World!" searchInput
$ runghc hello.hs

バックグラウンドでブラウザが起動して“Hello, World!”をググっていれば成功です!

コードを簡単に説明すると

myConfig :: WDConfig
myConfig = defaultConfig { wdCapabilities = (defaultCaps {browser = chrome})}

はWebDriverの基本設定を表す関数です。WDConfigはHostとかPortとかを設定するレコードになっています。今回変更しているのはwdCapabilitiesのところでこれはCapabilitiesというレコードです。Capabilitiesはブラウザの種類を設定したりアラートを制御できるようにするかとかWebStorageを制御できるようにするかとか許可系の設定をするレコードになっています。今回はブラウザをChromeに設定しています。

main :: IO ()
main = runSession myConfig $ do

runSessionの型はWDConfig -> WD a -> IO aになっていて設定と実行手順を渡せば実行してくれる関数です。 Seleniumの命令は基本的にはWDモナドを返す関数なのでdo構文を使って命令を組み立てていきます。

openPage "http://google.com"

まずhttp://google.comをブラウザで開きます。

searchInput <- findElem (ByCSS "input[type='text']")

次にCSSセレクタで“input[type=‘text’]”なる要素を取得してsearchInputに束縛しています。

sendKeys "Hello, World!" searchInput

最後にsearchInputに“Hello, World!”と打って検索を実行しています。

webdriver で使える命令

webdriverはSelenium公式ではないので(?)Seleniumのドキュメントに載ってる命令と違う名前の関数が多々あります。例えばさっきの例のopenPageは公式のライブラリではgetと書く関数と同じです。眺めてみるとwebdriverの方が関数名はわかりやすくなっている気がします。 Seleniumのドキュメントを読んでだいたいどんな命令が使えるのかを把握してからwebdriverのHackageを読んで実際どういう関数で使えるのかを調べるのがいいかもしれませんね。 命令はTest.WebDriver.Commandsにだいたい載っています。以下によく使いそうな命令を列挙してみます。なお以下の命令表では全て型宣言の頭にWebDriver wd =>が付いているものとします

ナビゲーション

命令 説明
openPage :: String -> wd () 指定されたURLのページを開く
forward :: wd () 次へ進む
back :: wd () 前に戻る
refresh :: wd () 更新

ページ情報

命令 説明
getCurrentURL :: wd String 開いてるページのURLを取得
getSource :: wd Text 開いてるページのソースを取得
screenshot :: wd ByteString 開いてるページのスクリーンショットを撮る

要素の検索

命令 説明
findElem :: Selector -> wd Element セレクタ(後述)から要素を取得
findElems :: Selector -> wd [Element] セレクタから全ての要素を取得
findElemFrom :: Element -> Selector -> wd Element セレクタから要素下の要素を取得
findElemsFrom :: Element -> Selector -> wd [Element] セレクタから要素下の全ての要素を取得

要素へのアクション

命令 説明
click :: Element -> wd () 要素をクリック
submit :: Element -> wd () 要素をsubmit
getText :: Element -> wd Text 要素のテキストを取得

要素へのキー入力

命令 説明
sendKeys :: Text -> Element -> wd () 要素にテキストを入力してEnter
clearInput :: Element -> wd () 要素のテキストをクリア

要素の情報

命令 説明
attr :: Element -> Text -> wd (Maybe Text) 要素の属性を取得
cssProp :: Element -> Text -> wd (Maybe Text) 要素のCSSを取得
elemPos :: Element -> wd (Int, Int) 要素の位置を取得
elemSize :: Element -> wd (Word, Word) 要素のサイズを取得
isSelected :: Element -> wd Bool 要素が選択されているかどうかを取得
isEnabled :: Element -> wd Bool 要素が有効かどうかを取得
isDisplayed :: Element -> wd Bool 要素が表示されているかどうかを取得
tagName :: Element -> wd Text 要素のタグ名を取得
activeElem :: wd Element 現在フォーカスされている要素を取得

ざっと挙げて見ましたがかなり色々できそうですね!この他にも複数Windowを制御したりクッキーをいじったりもできるので一度Test.WebDriver.Commandsを読むことをおすすめします。

さて上に挙げた命令にちょくちょく出てくるSelectorですが重要なので定義を挙げておきます

data Selector = ById Text
              | ByName Text
              | ByClass Text
              | ByTag Text
              | ByLinkText Text
              | ByPartialLinkText Text
              | ByCSS Text
              | ByXPath Text
              deriving (Eq, Show, Ord)

型構築子を見るとわかると思いますが、様々な方法で要素を選択する型ですね。 ById "someid" は大体 document.getElementById("someid") ByClass "someclass" は大体 document.getElementsByClassName("someclass") に対応してるわけです。さっきのhello.hsではByCSSを使ってましたね! 中でも便利なのがByLinkTextでこれは特定のテキストのaタグを取ってくることができる便利関数です!ハンズオンでも使って盛り上がっていました。

ググってリンクを踏んでみる

次の例としてGoogleで検索して1件目のリンクを踏んでURLを取得するシナリオを考えてみましょう。 Googleで検索して検索結果を利用する時の問題として検索結果が非同期で取得されるという事があります。ちょっと考えると特定のDOMが生成されるまで待てばいいことはわかるのですがwebdriverではどのように実装すればいいでしょう。答えを言ってしまうと、こういう時はTest.WebDriver.Commands.WaitにあるwaitUntilを使います。

{-# LANGUAGE OverloadedStrings #-}
import Test.WebDriver
import Test.WebDriver.Commands.Wait

myConfig :: WDConfig
myConfig = defaultConfig { wdCapabilities = (defaultCaps {browser = chrome})}

main :: IO ()
main = do
  url <- runSession myConfig $ do
    openPage "http://google.com"
    searchInput <- findElem (ByCSS "input[type='text']")
    sendKeys "CAMPHOR" searchInput
    waitUntil 30 $ findElem (ById "search")
    search <- findElem (ById "search")
    links <- findElemsFrom search (ByPartialLinkText "CAMPHOR-")
    click $ head links
    url <- getCurrentURL
    closeSession
    return url
  print url

実行して"https://camph.net/"が返ってくれば成功です! (ずっとトップに出て欲しいものです)

$runghc search.hs
"https://camph.net/"

コードを見て行きましょう。"CAMPHOR"で検索するところまではhello.hsと同じですね。

waitUntil 30 $ findElem (ById "search")

waitUntil はその名の通り条件が満たされるまで待つ関数です。

waitUntil :: WDSessionState m => Double -> m a -> m a

第一引数にはタイムアウト時間を設定します。 第二引数には条件を記述するのですが

ような関数を渡します。findElemは指定されたセレクタが存在しない時にNoSuchElementの例外を投げるので条件として使えるわけです。

search <- findElem (ById "search")
links <- findElemsFrom search (ByPartialLinkText "CAMPHOR-")
click $ head links

検索結果が表示されたらまずid="search"の要素をsearchに束縛します。次にsearchの下にあるリンクの要素でテキストに“CAMPHOR-”を含むものを取ってきてlinksに束縛します。linksには条件に合う要素が全て束縛されているのでheadで最初の要素を取り出してclickでクリックします。

url <- getCurrentURL
closeSession
return url

リンクを飛んだらURLを取得してcloseSessionでブラウザを閉じてしまいます。 最後にreturn urlで取得したURLを返却します。

url <- runSession myConfig $ do
    ...
    return url
print url

全体として上記のようになっているのでreturn urlで返したURLは外側のurlに束縛されてそれをprintしています。

以上SeleniumをHaskellで使う簡単な方法を見てきました。Haskellで書くとSeleniumの操作がWDモナドの統一されたインターフェースで記述することができてかなり読みやすく書くことが出来ると思います。今後は勉強会で得たSeleniumの知識を活かしてソシャゲの自動化をどんどん進めていきたいです(`・ω・´)

このエントリーをはてなブックマークに追加