docker-machineでselenium実行時にダウンロードしたファイル名がおかしくなる
docker-machine
固有の問題なのかどうかわからないのですが、コンテナ上で実行しているselenium
でファイルをダウンロードした際に何故かダウンロードしたファイル名がおかしくなる(どうおかしいかは後述)という問題でかなりハマったのでその作業メモ
- 環境
- ruby 2.3.0
- docker-machine 0.7.0 (virtual box)
- docker 1.11.0
seleniumでテストする準備
まず問題のファイルダウンロードの前にselenium
でテストするための環境です。
seleniumの操作環境構築
selenium
の操作はruby
で行おうと思うのでgem
のインストールです。
- 環境
- ruby 2.3.0
bundle init
echo "gem 'selenium-webdriver'" >> Gemfile
bundle install --path=vendor/bundle
seleniumコンテナ起動
こちらはすごく簡単で、公式のコンテナを起動するだけです。今回は現時点での最新版の2.53.0
で試します。
docker run -d -p 4444:4444 -p 5900:5900 selenium/standalone-chrome-debug:2.53.0
ポート4444
が実際にselenium
にアクセスするポートで、ポート5900
がコンテナの中でブラウザが動いている様子を見るためのvnc
の入り口です。
vncサーバに接続
コンテナ内で動くブラウザの様子を確認するためにvnc
サーバに接続します。mac
の場合はvnc
クライアントが標準で入っているので、
Finder起動 -> command + K
でサーバへ接続
のダイアログが出てくるので、サーバアドレス
のところに
vnc://<docker-machineのip>:5900
を入力します。
するとパスワードを聞かれるのでsecret
を入力します。
下記のような画面になれば準備完了です。

selenium
操作スクリプト
単純にグーグルを開いて検索して検索結果を取得、という処理を書いてみましょう。
require 'selenium-webdriver'
# webdriver操作用のユーティリティ
def with_webdriver
driver = Selenium::WebDriver.for(
:remote,
url: "http://192.168.99.100:4444/wd/hub",
desired_capabilities: :chrome
)
# implict_waitを設定しておくとfind_elementなどで描画待ちでまだ取得できない場合などに待ってくれる
driver.manage.timeouts.implicit_wait = 10
yield driver
ensure
driver.quit
end
with_webdriver do |d|
# gooleにアクセス
d.navigate.to "https://www.google.co.jp"
# 検索ボックス取得
search_box = d.find_element(id: "lst-ib")
# 検索ボックスに検索ワードを入れて
search_box.send_keys("docker selenium-webdriver")
# enter押下
search_box.send_keys(:return)
# 検索結果の見出しはclassが"r"のdivに囲まれた"a"タグのようなのでそれを全部表示
d.find_elements(:class, "r").each do |result|
puts result.text
end
# この状態をスクリーンショットで保存
d.save_screenshot "google_search_result.png"
end
これを実行すると下記のように検索結果の見出行が表示された上でその時のスクリーンショットがpng
で保存されます。
$ bundle exec ruby run-selenium.rb
GitHub - SeleniumHQ/docker-selenium: Docker images for ...
Docker で Selenium Grid の環境構築をもっと楽に! - Cybozu ...
selenium/hub public - Docker Hub
Docker で Selenium Grid による並列実行環境を構築 - Qiita
Dockerを使って素早くSelenium Grid環境を構築しよう | hifive ...
SeleniumサーバもDockerで。「Docker Selenium」レビュー ...
Docker Selenium. Getting Started - Selenium Conf 2015 ...
Docker Hub の Selenium Hub/Node イメージ + fig で ...
Selenium Grid with Docker | TEST DETECTIVE
Running headless Selenium WebDriver tests in Docker ...
取得したスクリーンショット(google_search_result.png
)
便利ですね。
ファイルのダウンロードで問題発生
さて、次にファイルのダウンロードを行いましょう。
適当に郵便局のページから北海道の郵便番号ファイルを取得してみます。
require 'selenium-webdriver'
def with_webdriver
driver = Selenium::WebDriver.for(
:remote,
url: "http://192.168.99.100:4444/wd/hub",
desired_capabilities: :chrome
)
driver.manage.timeouts.implicit_wait = 10
yield driver
ensure
driver.quit
end
with_webdriver do |d|
d.navigate.to "http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html"
# ダウンロードリンク取得
link = d.find_element(:xpath, "//*[@href='oogaki/zip/01hokkai.zip']")
link.click
#ダウンロード待ち
sleep 10
end
実行
bundle exec ruby download-zip.rb
ダウンロードされたファイルはどこにあるんでしょうか。コンテナの中を検索してみましょう。
# 起動中のseleniumコンテナのコンテナ名確認
$ docker ps --format "table {{.Image}}\t{{.Names}}"
IMAGE NAMES
selenium/standalone-chrome-debug:2.53.0 hopeful_albattani
# そのコンテナに対してzipファイルの検索実行
$ docker exec hopeful_albattani find / -name '*.zip'
/home/seluser/Downloads/01hokkai.zip
/home/seluser/Downloads
以下に保存されるようですね。
コンテナのファイルをホスト側で取得
ダウンロードしたならそのファイルをスクリプトから取得したいですね。
というわけで保存場所も分かったので、volume
でマウントしてみます。
# コンテナを落とします。(コンテナ名は先程調べた名称)
$ docker kill hopeful_albattani
# ダウンロードファイルを保存するフォルダを作ります。
$ mkdir downloads
# このフォルダをマウントして再度コンテナを起動します。
$ docker run -d -v $(pwd)/downloads:/home/seluser/Downloads -p 4444:4444 -p 5900:5900 selenium/standalone-chrome-debug:2.53.0
$ ls downloads
# => まだ空っぽ
この状態でダウンロード処理を実行してからdownloads
フォルダを確認します。
$ bundle exec ruby download-zip.rb
$ ls downloads
01hokkai.zip
ダウンロードファイルをホスト側に取り込めました!
もう一回実行してみましょう。
$ bundle exec ruby download-zip.rb
$ ls downloads
01hokkai (1).zip 01hokkai.zip
実際に手動で操作した時のように同じファイルが既に存在したら、ファイル名 (n).拡張子
の形式で保存されてますね。
ここでハマる
いまやろうとしているのは繰り返しダウンロード処理を実行出来るようにしたかったのでプログラム中でダウンロードしたファイルを使ったら都度消して次回実行時も同じファイル名で取得できるようにしたかったので、ダウンロードが完了するまで待って、中身を使って、消す、という処理を行う必要がありました。
seleniumでのwait処理
先ほどの処理のようにダウンロード完了をsleep
で待っていると無駄にまったり、まだ終わっていないのにsleep
が終わる可能性があります。
seleniumにはwait
することも考慮されていて待つためのユーティリティがそろっています。
waitオブジェクト
waitオブジェクトを作ってuntil
メソッドをブロック付きで実行すると
最大waitオブジェクトに設定した待ち時間
待つかブロックが真を返すまで待ってくれます。
待ち時間をすぎれば例外、ブロックが真を返せばその結果がuntil
の結果として返されます。
このときブロックは複数回実行されるので注意が必要です。
w = Selenium::WebDriver::Wait.new(:timeout => 10)
w.until do
# このブロックが真(というか非nil)を返すまで複数回実行される
end
これを利用して特定の条件がそろうまでwaitするメソッドを作成します。
def wait_until(&b)
w = Selenium::WebDriver::Wait.new(:timeout => 10)
w.until(&b)
end
これを使うとdonwloads
フォルダに01hokkai.zip
が出来るまで待つ、という処理は
def wait_until_download(file)
wait_until do
File.exists?(file)
end
end
というような感じでかけます。
というわけで下記のような処理を実行してみます。
require 'selenium-webdriver'
def with_webdriver
driver = Selenium::WebDriver.for(
:remote,
url: "http://192.168.99.100:4444/wd/hub",
desired_capabilities: :chrome
)
driver.manage.timeouts.implicit_wait = 10
yield driver
ensure
driver.quit
end
def wait_until(&b)
w = Selenium::WebDriver::Wait.new(:timeout => 10)
w.until(&b)
end
def wait_until_download(file)
wait_until do
File.exists?(file)
end
end
with_webdriver do |d|
d.navigate.to "http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html"
# ダウンロードリンク取得
link = d.find_element(:xpath, "//*[@href='oogaki/zip/01hokkai.zip']")
link.click
begin
# ダウンロードを待つ
path = "./downloads/01hokkai.zip"
wait_until_download(path)
# ダウンロードしたファイルを使う
p [path, File.size(path)]
ensure
# 終わったらダウンロード削除
File.delete(path) if File.exists?(path)
end
end
実行してみましょう。した後、downloadsフォルダを確認してみましょう。
$ bundle exec ruby download-zip.rb
["./downloads/01hokkai.zip", 107252]
$ ls downloads
$ #何も無い
きちんとダウンローしたものをプログラムで使用してから削除できました。
では、続けて2回目を実行してみましょう。
$ bundle exec ruby download-zip.rb
〜略〜/vendor/bundle/ruby/2.3.0/gems/selenium-webdriver-2.53.0/lib/selenium/webdriver/common/wait.rb:76:in `until': timed out after 10 seconds (Selenium::WebDriver::Error::TimeOutError)
from download-zip.rb:17:in `wait_until'
from download-zip.rb:21:in `wait_until_download'
from download-zip.rb:36:in `block in <main>'
from download-zip.rb:10:in `with_webdriver'
from download-zip.rb:26:in `<main>'
$ ls downloads
01hokkai (1).zip
なぜかダウンロードに失敗して異常終了し、downloadsフォルダには連番付きで01hokkai (1).zip
でファイルができています。
ファイルはきちんと消していたのに何故??
解決?
いろいろ調べましたが。これ!というような原因はわかりませんでした。ですが、1つ考えられるのは、docker-machineでvirtual boxのファイル共有機能でコンテナとファイルを共有しているので、その辺かな?と思い、
ダウンロードファイルがを待つ処理(wait_until_download
)を
- ファイルが存在しており、そのファイルが前回調べた時と同じファイルサイズのまま
という条件にしてみました。
def wait_until_download(file)
wait_until do
prev_size = -1
wait_until do
if File.exists?(pati)
size = File.size(file)
ret = if size == prev_size
true
else
prev_size = size
false
end
sleep 1
ret
end
end
end
end
sleep 1
なんかが、なかなか無理矢理感がありますがこれ入れないと何故かエラーがでてしまい。。
で最終的に下記のスクリプトが完成品です。
require 'selenium-webdriver'
def with_webdriver
driver = Selenium::WebDriver.for(
:remote,
url: "http://192.168.99.100:4444/wd/hub",
desired_capabilities: :chrome
)
driver.manage.timeouts.implicit_wait = 10
yield driver
ensure
driver.quit
end
def wait_until(&b)
w = Selenium::WebDriver::Wait.new(:timeout => 10)
w.until(&b)
end
def wait_until_download(file)
prev_size = -1
wait_until do
if File.exists?(file)
size = File.size(file)
ret = if size == prev_size
true
else
prev_size = size
false
end
sleep 1
ret
end
end
end
with_webdriver do |d|
d.navigate.to "http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html"
# ダウンロードリンク取得
link = d.find_element(:xpath, "//*[@href='oogaki/zip/01hokkai.zip']")
link.click
begin
# ダウンロードを待つ
path = "./downloads/01hokkai.zip"
wait_until_download(path)
# ダウンロードしたファイルを使う
p [path, File.size(path)]
ensure
# 終わったらダウンロード削除
File.delete(path) if File.exists?(path)
end
end
これで2回連続実行してみましょう。
$ bundle exec ruby download-zip.rb
["./downloads/01hokkai.zip", 107252]
$ ls downloads
# =>何も無い
$ bundle exec ruby download-zip.rb
["./downloads/01hokkai.zip", 107252]
$ ls downloads
# =>何も無い
無事連続してダウンロード出来るようになりました。しかしどういう理由なんでしょうねぇ。
ダウンロードするコンテナ側のプロセスがファイルを掴んでる間にホスト側でファイルを消してしまってる??よくわかりません。
以上の内容を
に置いています。