RubyとSeleniumでHeadless chromeを動かす on Ubuntu/Linux

[2017-06-22 追記] chromeバージョン59の正式リリースに伴い、記事の内容を全面刷新しました。
[2017-07-27 追記] selenium使用時に`send_keys`が使用できない問題を記載しておりましたが、Chromedriver2.31のリリースに伴い問題が解消しました！

## はじめに注意

chromeはバージョン59より正式にヘッドレスモードを搭載しました。

それを試そうと「chrome headless」などで検索すると、古いバージョンのchromeをXvfbなどの仮想ディスプレイを通してLinux上で動かす記事が沢山でてきますが、これらはheadless chromeに関しての記事ではなく、普通のGUI chromeをどうにかしてcui上で動かそうという取り組みの記事です。

chromeのヘッドレスモードでは、PhantomJSと同じようにXvfbはありません。googleの中の人も公式に名言しています。もし参考にしている記事でXvfbという単語が出てきたら、少し古い情報かもしれませんのでご注意下さい。

## 環境

環境構築方法は後述しますが、先に本記事の実行環境を書いておきます。

```bash
# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"

# google-chrome-beta --version
Google Chrome 59.0.3071.36 beta

# chromedriver -v
ChromeDriver 2.29.461571 (8a88bbe0775e2a23afda0ceaf2ef7ee74e822cc5)

# ruby -v
ruby 2.2.5p319 (2016-04-26 revision 54774) [x86_64-linux]

# cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    childprocess (0.7.0)
      ffi (~> 1.0, >= 1.0.11)
    ffi (1.9.18)
    mini_portile2 (2.1.0)
    nokogiri (1.7.2)
      mini_portile2 (~> 2.1.0)
    rubyzip (1.2.1)
    selenium-webdriver (3.4.0)
      childprocess (~> 0.5)
      rubyzip (~> 1.0)
      websocket (~> 1.0)
    websocket (1.2.4)

PLATFORMS
  ruby

DEPENDENCIES
  nokogiri
  selenium-webdriver

BUNDLED WITH
   1.12.5
```

## 事前準備

必要なライブラリをインストール

```bash
$ sudo apt-get update
$ sudo apt-get install -y libappindicator1 fonts-liberation
```

## ヘッドレスモードを使用できるchromeをインストール

```bash
$ curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
$ sudo dpkg -i google-chrome-stable_current_amd64.deb
```

## コマンドラインからChromeをヘッドレスで実行する

### 遷移先ページのHTMLボディを標準出力する

```bash
$ google-chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/ > test.html
```

### 遷移先ページのスクリーンショットを撮影

```bash
$ google-chrome --headless --disable-gpu --screenshot https://www.chromestatus.com/
```


## Seleniumを通してRubyからheadless chromeを動かす

### chromedriverのインストール

~~chromedriverはChromeバージョン59をサポートしている2.30以降をインストールして下さい。~~

[2017-07-27追記]
**Chromedriverは2.31以降をインストールしてください。2.30以前ですと、後述のseleniumで`send_keys`が使えない問題が発生していたのですが、2.31でこの問題が解消しています！**

```bash
＃ curl -O https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip
＃ unzip chromedriver_linux64.zip
＃ chmod +x chromedriver
＃ mv -f chromedriver /usr/local/share/chromedriver
＃ ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver
＃ ln -s /usr/local/share/chromedriver /usr/bin/chromedriver
＃ chromedriver
Starting ChromeDriver 2.29.461571 (8a88bbe0775e2a23afda0ceaf2ef7ee74e822cc5) on port 9515
Only local connections are allowed.
```


### RubyでChromeをヘッドレスモードで実行

#### サンプルソース

以下のサンプルソースでUbuntu上で正常にfacebookのトップページにアクセスできていることは確認できています。ただ、フォームの入力を行う際に後述のエラーが発生するという状態です。

```ruby:Gemfile
source "https://rubygems.org"

# gem "rails"
gem "selenium-webdriver"
gem 'nokogiri'
```

```ruby:test_headless_chrome.rb
require 'selenium-webdriver'
require 'nokogiri'
require 'pp'

ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36"

caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {binary: '/usr/bin/google-chrome-beta', args: ["--headless", "--disable-gpu", "--user-agent=#{ua}", "window-size=1280x800"]})
session = Selenium::WebDriver.for :chrome, desired_capabilities: caps
session.manage.timeouts.implicit_wait = 30
session.navigate.to "https://www.facebook.com/"
session.save_screenshot('page.png')

id = session.find_element(:name, 'email')
id.send_keys('test@mail.com') # <= 現在はsend_keysでエラーが発生

session.quit
```

#### フォームの入力(send_keys)でエラーが出る

Macで試したような他の記事を読むと正常に実行できているようなのですが、LinuxなどCUI上で実行した場合は、フォームの入力の際にselenium-webdriver Gemが以下のようなエラーを吐きます。

同じ問題で困っている人もたくさんいるようです。原因がChromeDriver側にあるのかSelenium側にあるのか不明ですが、いずれにしてもまだ議論段階であるようです。

https://bugs.chromium.org/p/chromedriver/issues/detail?id=1772


```bash
# bundle exec ruby test_headless_chrome.rb
/root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/response.rb:69:in `assert_ok': unknown error: an X display is required for keycode conversions, consider using Xvfb (Selenium::WebDriver::Error::UnknownError)
  (Session info: headless chrome=59.0.3071.36)
  (Driver info: chromedriver=2.29.461571 (8a88bbe0775e2a23afda0ceaf2ef7ee74e822cc5),platform=Linux 4.4.0-21-generic x86_64)
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/response.rb:32:in `initialize'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/http/common.rb:83:in `new'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/http/common.rb:83:in `create_response'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/http/default.rb:107:in `request'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/http/common.rb:61:in `call'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/bridge.rb:678:in `raw_execute'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/bridge.rb:656:in `execute'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/remote/bridge.rb:448:in `send_keys_to_element'
        from /root/tmp/headless_chrome_with_selenium/.bundle/ruby/2.2.0/gems/selenium-webdriver-3.4.0/lib/selenium/webdriver/common/element.rb:156:in `send_keys'
        from test_headless_chrome.rb:42:in `<main>'
```


#### フォーム入力のsend_keysメソッドの代替手段

send_keysを使用せずにフォームを入力するため、回避策としてselenium上で任意のjavascriptを実行する`execute_script`メソッドを使用します。javascriptが実行できれば、テキストボックスをcssセレクタで選択して`setAttribute`でフォーム入力を行うことができます。

海外でも現状この集団でフォーム入力の不具合を回避している模様です。
https://bugs.chromium.org/p/chromedriver/issues/detail?id=1772#hc15

```ruby
require 'selenium-webdriver'
require 'nokogiri'
require 'pp'

ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36"

caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {binary: '/usr/bin/google-chrome', args: ["--headless", "--disable-gpu", "--user-agent=#{ua}", "window-size=1280x800"]})

session = Selenium::WebDriver.for :chrome, desired_capabilities: caps
session.manage.timeouts.implicit_wait = 30

### facebookトップページに遷移
session.navigate.to "https://www.facebook.com/"

### フォーム入力、クリックによるページ遷移
#session.find_element(:name, "email").send_keys('testtest@gmail.com') # <- send_keysメソッドが現状使用不可

# フォーム入力の代替手段
session.execute_script('document.getElementById("email").setAttribute("value", "test@mail.com");')
session.execute_script('document.getElementById("pass").setAttribute("value", "testpass");')

session.find_element(css: '#u_0_q').click

session.save_screenshot('page.png')
session.quit
```