RSelenium - docker-compose でSeleniumコンテナを複数立ち上げるとWebクロールが捗るヨ！ ダウンロードもできるよ！

# 【2024/4追記】
こちらで使用している `RSelenium` ですが、Selenium4での json-wire プロトコル対応終了に伴って<a href = "https://github.com/ropensci/RSelenium/issues/280">開発終了</a>となりました。
Selenium3では引き続き動作するのですが、長期的に考えると新規プロジェクトでの採用は避けたほうが良いかもしれません。

後継を目指す(互換性は無い) Selenium4 / Chrome Dev Protocol 対応パッケージなどが登場していますので、これらを使うほうが良いかもしれません。

`selenium-r` - RSelenium のように selenium を操作できる
selenium公式 Docker image などを使うにはこちらが早いかな
既にかなり安定して使えています。僕はこちらに移行予定です。
Seleniumが対応している Firefox / Edge などにも対応します
https://ashbythorpe.github.io/selenium-r/

`chromote` Chrome DevTools Protocol で Google Chrome を直接操作可能
Posit社(旧名RStudio社) が開発しているので安心感があるかも
同一マシン上で Chrome を起動して使うにはこちらがてっとりばやいです
Chrome / Chromium 専用
https://rstudio.github.io/chromote/

`selenider` - Javaの `Selenide`, Python の `Selene` インスパイア系
より高レベルに抽象化されており、自動でページロード完了を待ってくれたりエレメント検索にCSSセレクタやxpathに加えて文字列検索が使えるなど使い心地が良さそうに見えます
詳細は<a href="https://selenide.org/documentation/selenide-vs-selenium.html">本家の Selenide</a> を見たほうが早いかも
バックエンドに上記の `chromote` `selenium-r` が選択可能
https://github.com/ashbythorpe/selenider

なおどれを使っても、データの取得などには引き続き `rvest` / `xml2` が使えます。
また、selenium公式のイメージですが、Selenium3系は下記のバージョンが最後となります。
```FROM selenium/standalone-chrome-debug:3.141.59```

こちらは既に更新終了となっていますが、このイメージをベースにChromeを最新版に入れ替えて使ったりすることも可能ですので(Chrome126で確認)、慌てて Selenium3 を放棄しなければいけない状態にはなっていませんので、ご安心ください。
(Dockerを使う大きなメリットだなぁと思いました)


# ここから以前の記事です
Webクローリングは自動化しようとすると、微妙なブラウザの挙動の違いで止まることも多く環境構築と維持に結構気を使います。

ですが今コンテナ化っちゅう最強の方法が誰でも使える幸せな時代ですので、
これを活用して安定かつ複数同時のクロールができるようにしちゃいましょう。


# docker-compose.yaml
いきなりですが設定ファイルです。
RStudioコンテナと、ここではchromeを内包したコンテナ、起動処理用のubuntuと3種のコンテナを使います：

```docker-compose.yaml
version: "3"

networks:
  scrapeapp_net:
volumes:
  tmp_machine:
  tmp_sel_dl:
  
services:
  r:
    image: scrapelab/r   # rocker/verse を元に諸々追加したコンテナ
    volumes:
      - /dev/shm:/dev/shm
      # スクリプト一式
      - tmp_machine:/home/rstudio/scrapelab_compose
      # ダウンロード用dir
      - tmp_sel_dl:/home/rstudio/scrapelab_compose/sel_dl
      # GSuiteへのアクセストークン保存用。インタラクティブ実行時は、最初に一度従来のOAuth Danceするとホスト側に保存される。自動化の際はGCPのAPIキーを保存
      - ~/.scrapelab_token:/home/rstudio/.token
    environment:
      PASSWORD: "somepasswordXXXX"
      ROOT: "TRUE"
    networks:
      - scrapeapp_net
    ports:
      - "8787:8787"
      - "8080:80"
    hostname:
      "compose_${docker_host}"
    # これをコメント解除すると、自動でスクリプトが走る。開発時はコメントアウトのままでRStudio GUIが起動
    # command: su rstudio -c '/usr/local/lib/R/bin/Rscript /home/rstudio/scrapelab_compose/rselenium_batchloader.R'
    restart: always
    
  sel:
    image: selenium/standalone-chrome-debug:3.141.59
    # 共通ボリューム内にファイルダウンロードdirをマウント
    volumes:
      - /dev/shm:/dev/shm
      - tmp_sel_dl:/home/seluser/Downloads
    networks:
      - scrapeapp_net
    ports:
      # 4444は docker network 内だけで使うのでexposeしなくて良い
      # VNC 開発用 複数起動のためにポートはレンジで指定 デプロイ時は閉じる (5900はMac標準で使ってるので1飛ばす)
      - "5901-5909:5900"
    restart: always

  # ファイルパーミッション問題のダーティハック デプロイ用はちゃんとuid/gid他を設定してください
  init_only:
    restart: always
    image: ubuntu
    networks:
      - scrapeapp_net
    volumes:
      - tmp_machine:/scrapeapp
      - tmp_sel_dl:/scrapeapp/Downloads
    # 各コンテナから見える共有フォルダを777で作っちゃう
    command: 
      sh -c 'chmod 777 /scrapeapp && chmod -R 777 /scrapeapp/Downloads && \
        mkdir -p /scrapeapp/temp && chmod -R 777 /scrapeapp/temp && \
        mkdir -p /scrapeapp/export && chmod -R 777 /scrapeapp/export 

  # wireguard使うと、どこにデプロイしても事務所など特定IPからリモートスクレープ出来る。ここでは割愛
```

上記を保存して、実行します。その際 sel イメージを好きな数だけ実行します

```bash
$ docker-compose up -d --scale sel=5
```



# RStudioから呼ぼう
よく使いそうなライブラリを読み込みます

```R
pacman::p_load(googledrive, googlesheets4, aws.s3,
  jsonlite, selectr, XML, xml2, rvest, RSelenium, 
  foreach, doParallel, parallel, tictoc,
  tidyverse, devtools)
```

chromeの設定を行います。この辺り、pythonのサンプルが多く見つかるのですが
Rでの書き方に慣れていれば、それらも参考になります。
まず pref / arg の使い分けはこんな感じです。
リスト、ベクトルを作成して最後に named list にまとめます。

```R
chrome_prefs = 
  list(  
    "profile.default_content_settings.popups" = 0L,
    "download.prompt_for_download" = FALSE,
    "download.directory_upgrade" = TRUE,
    "safebrowsing.enabled" = FALSE,
    "safebrowsing.disable_download_protection" = TRUE,
    "acceptSslCerts" = TRUE,
    "AutoSelectCertificateForUrls" = '["{\"pattern\":\"*\",\"filter\":{}}"]',
    "SuppressInitialDiagnosticInformation" = FALSE,
    "EnableVerboseLogging" = FALSE
  )


chrome_args = 
  c('--disable-features=VizDisplayCompositor', 
    '--ignore-certificate-errors', 
    '--ignore-urlfetcher-cert-requests', 
    '--no-sandbox', 
    '--disable-gpu', 
    '--disable-web-security', 
    '--log-level=3'
    )

# extented capabilities用のリストにバインド
eCaps_withhead = 
  list(chromeOptions = 
         list(
           prefs = chrome_prefs,
           args = chrome_args ))
```


あとは、composeで起動したseleniumを一つずつremoteDriver()でenumしていきます

```R
  remDr <- remoteDriver(
    remoteServerAddr = "docker_sel_1",
    port = 4444,
    browserName = "chrome",
    extraCapabilities = eCaps_headless )
```

全てのコンテナは共通のdockerネットワーク(ここでは scrapeapp_net )に接続した状態で起動します。
簡易DNS的な機能がありまして、ホスト名はdocker-compose.yamlで指定した名前 + _数字となります


数を可変にする場合は一々初期化するのは面倒かと思いますので、適当にループで処理します。
僕のはこんな感じです：

```R
# docker-compose内で動いてるインスタンスを検出してホスト名一覧をリターン
enum_rd_compose <- function(){

  # docker-compose scale= で複数インスタンスが接続されているか、順番にpingして調べる
  # とりあえず暫定的に上限は20台
  i = 1
  while (i <= 20) {
    ping_host <- paste0(docker_net, "sel_", i)
    print(paste("pinging", ping_host, "..."))
    
    # ホストに到着しなかった場合 c(NA, NA, NA) が返ってくる
    res <- ping(ping_host)
    if( is.na(res[1]) ){
      print(glue("{i-1} selenium instances found. returning their names."))
      sel_hosts <- paste0("docker_sel_", 1:(i-1) )
      return(sel_hosts)
    }
    print(paste0("found: ", ping_host))
    i = i + 1
  }
  print(glue("{i-1} selenium instances found. returning their names."))
  sel_hosts <- paste0(docker_net, "sel_", 1:(i-1) )
  return(sel_hosts)
}

init_RD <- function(sel_host, sel_port = 4444L){
  remDr <- remoteDriver(
    remoteServerAddr = sel_host,
    port = sel_port,
    browserName = "chrome",
    extraCapabilities = eCaps_withhead
  )
  remDr$closeall()
  remDr$open()
  return(remDr)
}

# ホスト名一覧を取得
compose_sel_hosts <- enum_rd_compose()
# それぞれinitしてリストに格納
remDr <- lapply(compose_sel_hosts, init_RD)
```

# これで複数同時にクロールできるよ！
以後このように、各seleniumが使えます。
mcparallel()などを利用して、複数ジョブをディスパッチして並列でどんどんデータが拾えます！

```R
remDr[1]$open()
```

# ダウンロードもできるよ！
また、ヘッドレスchromeを使うとどうやっても上手くファイルをダウンロード出来ません...
元々設定が複雑な所に、chromeのバージョン違いなどでまず無理なのでは無いかと...?
selenium のコンテナを使う方法では、あんまり何も考えずにダウンロード出来ますよ！

コンテナ内の標準ダウンロードフォルダを RStudio 内にマウントしてありますので、
単純に /home/rstudio/scrapelab_compose/sel_dl ( ~/scrapelab_compose/seldl )を見に行けばOKです。

```R
f <- list.Files("~/scrapelab_compose/seldl")
```


# VNCデバッグ快適だよ！

VNCビュワーで、上記ホストのポート5901~に繋ぐと、
RSeleniumで操作している様がリアルタイムで見られます。
こんな感じ：

![Screenshot 2021-09-03 at 16.33.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255842/4523de83-a78a-5964-c8f0-c825b80be714.png)

要素を拾うのもdeveloperツールを開いて指定していくだけで、ポンポン作業できます！
データ収集が捗りますね

以上、「RSeleniumでファイルダウンロードできないんだけど」みたいな声を
あちこちで見ましたので、僕のやり方のご紹介でした。

仕事ではコレベースのコードが、二年ほど毎日データ収集してくれています。
安定していて素晴らしいですね rocker は！