R & WebDriver & Firefox & Ubuntu24 でスクレイピングしてみる


## なんでこんなことを？ニッチすぎやしないか？
昔、 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 を使うことを想定しています。あと、検証コードなのでステータスの確認とかエラー処理とかは一切していないので、要注意です。

```R:WebDriverを操作する検証コード
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](https://cran.r-project.org/) にアクセスして検索してみたいと思います。流れは以下のとおりです。

### テストの流れ
[The Comprehensive R Archive Network](https://cran.r-project.org/) にアクセス
↓
左側フレーム内の「Search」をクリック
↓
右側のフレーム内の「search.r-project.org」をクリック
↓
「abc」で検索
↓
結果を取得


```R:テスト
#開始＆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
 ```

```text:結果
                                                               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 をバックグラウンドで実行するものの、それっきりになっていますので、必要に応じて後片付けする必要あるかもです。

## おわりです
とりあえず動いたという感じです。実際に使えるかはなんともですが、意外と簡単に操作できたので、必要な操作があれば追加も簡単そうです。とりあえずはあえずめでたし、めでたし、ということで。