なんでこんなことを?ニッチすぎやしないか?
昔、 RSelenium を使ってスクレイピングしたとき、 Google Chrome と対応する WebDriver を用意したり、 Java や Selenium をセットアップしたり、最後に RSelenium をインストールしたりと、いろいろ準備が面倒だった覚えがあって。
そんなこともすっかり忘れて、最近、 Ubuntu24 に環境を変えて、何気なく snap info firefox
って打ってみたら、 firefox.geckodriver が表示されることを発見!さらに firefox.geckodriver
って打ってみたところ、 localhost:4444 ポートで待機する状態になることをさらに発見。なんと Firefox の WebDriver は標準でインストールされていたという衝撃的な事実、しかも Ubuntu22 でもそうだったかもしれないという、さらなる衝撃的な事実が。※何かの過程でインストールされただけで標準じゃないかもですが、その時はそう思ったって事で。
で、過去の RSelenium 使ったコードを見返してみたら、 rvest あたりを併用してたせいか、 RSelenium の機能はほんのちょっとしか使ってなかった。てことは「R から Firefox の WebDriver にアクセスするコードを書けば、事前準備ゼロで同じようなことができちゃうんじゃない?うん、これはいいぞ、おもしろそうだ」てな話です。
WebDriverの情報はどこに?
https://w3c.github.io/webdriver/ や https://w3c.github.io/webdriver/ あたりにそれぽいことが書いてあったので、そのあたりを参考に試してみようと思います。
で作ってみた(検証コードだけど)
で、作ったのが以下のとおりです。 WebDriver ではクリック等の操作のみで、それ以外は rvest を使うことを想定しています。あと、検証コードなのでステータスの確認とかエラー処理とかは一切していないので、要注意です。
library(dplyr)
library(httr)
library(rvest)
library(glue)
start_webdriver <- function() {
stop_webdriver()
system("firefox.geckodriver &")
Sys.sleep(3)
}
stop_webdriver <- function() {
system("pkill geckodriver")
Sys.sleep(1)
}
webdriver_url <- function() {
return('localhost:4444')
}
new_session <- function() {
httr::set_config(verbose())
httr::POST(glue("{webdriver_url()}/session")
,body = list(capabilities = list(alwaysMatch = list(acceptInsecureCerts = T)))
,encode = "json"
,timeout(30)
) %>% content %>% "$"(value) %>% "$"(sessionId) %>% return()
}
delete_session <- function(session_id) {
httr::DELETE(glue("{webdriver_url()}/session/{session_id}"))
}
navigate_to <- function(session_id, url) {
httr::POST(glue("{webdriver_url()}/session/{session_id}/url")
,body = list(url = url)
,encode = "json"
)
}
get_page_source <- function(session_id) {
httr::GET(glue("{webdriver_url()}/session/{session_id}/source")
) %>% content %>% "$"(value) %>% return()
}
get_current_url <- function(session_id) {
httr::GET(glue("{webdriver_url()}/session/{session_id}/url")
) %>% content %>% "$"(value) %>% return()
}
find_element <- function(session_id, using, value) {
httr::POST(glue("{webdriver_url()}/session/{session_id}/element")
,body = list(using = using, value = value)
,encode = "json"
) %>% content %>% "$"(value) %>% unlist %>% return()
}
find_elements <- function(session_id, using, value) {
httr::POST(glue("{webdriver_url()}/session/{session_id}/elements")
,body = list(using = using, value = value)
,encode = "json"
) %>% content %>% "$"(value) %>% unlist %>% return()
}
switch_to_frame <- function(session_id, id) {
if (id %>% is.character()) {
id <- list("element-6066-11e4-a52e-4f735466cecf" = id)
}
httr::POST(glue("{webdriver_url()}/session/{session_id}/frame")
,body = list(id = id)
,encode = "json"
)
}
switch_to_parent_frame <- function(session_id) {
httr::POST(glue("{webdriver_url()}/session/{session_id}/frame/parent")
,body = '{}'
)
}
send_keys_to_element <- function(session_id, element_id, text) {
httr::POST(glue("{webdriver_url()}/session/{session_id}/element/{element_id}/value")
,body = list(text = text)
,encode = "json"
)
}
click_element <- function(session_id, element_id) {
httr::POST(glue("{webdriver_url()}/session/{session_id}/element/{element_id}/click")
,body = "{}"
)
}
テストしてみるよ
テストとして The Comprehensive R Archive Network にアクセスして検索してみたいと思います。流れは以下のとおりです。
テストの流れ
The Comprehensive R Archive Network にアクセス
↓
左側フレーム内の「Search」をクリック
↓
右側のフレーム内の「search.r-project.org」をクリック
↓
「abc」で検索
↓
結果を取得
#開始&Rのページ(The Comprehensive R Archive Network)を開く
start_webdriver()
Sys.sleep(3) #ちょっと待つ
session_id <- new_session()
navigate_to(session_id, url="https://cran.r-project.org/")
Sys.sleep(3) #ちょっと待つ
#左フレームの「Search」をクリック
find_elements(session_id, using="xpath", value='//frame')[2] %>%
switch_to_frame(session_id, .)
find_element(session_id, using="xpath", value='//A[text()="Search"]')[1] %>%
click_element(session_id, .)
Sys.sleep(3) #ちょっと待つ
#右フレームの「search.r-project.org」をクリック
switch_to_parent_frame(session_id)
find_elements(session_id, using="xpath", value='//frame')[3] %>%
switch_to_frame(session_id, .)
find_element(session_id, using="xpath", value='//A[text()="search.r-project.org"]') %>%
click_element(session_id, .)
Sys.sleep(3) #ちょっと待つ
#キーワードに「abc」を入力し「Search」をクリック
find_element(session_id, using="xpath", value='//input[@id="omega-autofocus"]') %>%
send_keys_to_element(session_id, ., "abc")
find_element(session_id, using="xpath", value='//input[@type="submit" and @value="Search"]') %>%
click_element(session_id, .)
Sys.sleep(3) #ちょっと待つ
#検索結果を取得
switch_to_parent_frame(session_id)
get_page_source(session_id) %>% read_html %>%
html_elements(xpath="//table[1]//td[2]/b/a") %>% as.character %>%
data.frame() %>% rename(t = 1) %>%
rowwise() %>%
mutate(title = t %>% read_html() %>% html_text()) %>%
mutate(url = t %>% read_html() %>% html_elements("a") %>% html_attr("href")) %>%
ungroup %>%
mutate(absolute_url = url_absolute(url, get_current_url(session_id))) %>%
select(title, absolute_url) %>% as.data.frame
title absolute_url
1 R: Class '"abc"' https://search.r-project.org/CRAN/refmans/forams/html/abc-class.html
2 R: the ABC procedure for model selection https://search.r-project.org/CRAN/refmans/lamme/html/abc.html
3 R: ABC https://search.r-project.org/CRAN/refmans/inventorize/html/ABC.html
4 R: Convert Simulation Results to abc's Parameter Format https://search.r-project.org/CRAN/refmans/coala/html/create_abc_param.html
5 R: ABC algorithm for network reverse-engineering https://search.r-project.org/CRAN/refmans/networkABC/html/abc.html
6 R: ABC analysis https://search.r-project.org/CRAN/refmans/tsutils/html/abc.html
7 R: An automated analysis applying all ABC.RAP functions in one... https://search.r-project.org/CRAN/refmans/ABC.RAP/html/process.ABC.RAP.html
8 R: Summaries of posterior samples generated by ABC algortithms https://search.r-project.org/CRAN/refmans/abc/html/summary.abc.html
9 R: Convert Simulation Results to abc's Summary Statistic Format https://search.r-project.org/CRAN/refmans/coala/html/create_abc_sumstat.html
10 R: ABC Customer Satisfaction https://search.r-project.org/CRAN/refmans/Gifi/html/ABC.html
後片付けも忘れずに
今回のコードは、検証って言う位置づけなので、作成したセッション削除していませんし、 firefox.geckodriver をバックグラウンドで実行するものの、それっきりになっていますので、必要に応じて後片付けする必要あるかもです。
おわりです
とりあえず動いたという感じです。実際に使えるかはなんともですが、意外と簡単に操作できたので、必要な操作があれば追加も簡単そうです。とりあえずはあえずめでたし、めでたし、ということで。