Selemiumで認証が必要な画像を保存する(Ruby版)


最近、認証情報を必要とする画像ファイルを保存する必要が出てきました。

「Selenium 画像保存」でググると情報は出てくるんですが、その殆どが取得した画像URLに対してopen-uriを使って保存するものでした。

画像ファイル自体に認証情報が必須になっているサービスの場合、そのままopen-uri等で開こうとしても、open-uriに認証情報がないため、当然開けません。

これを開けるようにするには、大まかに2つの手段が思いつきます。

  1. open-uriにSeleniumで取得した認証用Cookieを渡す
  2. Seleniumで画像バイナリを取得して、それを保存する。

今回は2.の方法を使いました。

作成したコード

require 'uri'

  def save_binary(driver, url, filepath)
    now_window = driver.window_handle
    driver.execute_script("window.open()")
    driver.switch_to.window(driver.window_handles.last)
    driver.get(url)

    js = <<EOS
    var getBinaryResourceText = function(url) {
        var req = new XMLHttpRequest();
        req.open('GET', url, false);
        req.overrideMimeType('text/plain; charset=x-user-defined');
        req.send(null);
        if (req.status != 200) return '';

        var filestream = req.responseText;
        var bytes = [];
        for (var i = 0; i < filestream.length; i++){
            bytes[i] = filestream.charCodeAt(i) & 0xff;
        }

        return bytes;
    }
EOS
    js += "return getBinaryResourceText(\"#{url}\");"

    uri = URI.parse(url)
    file_name = File.basename(uri.path)

    data_bytes = driver.execute_script(js)

    File.open(filepath + file_name, "w") do |f|
      f.write(data_bytes.pack('C*'))
    end

    driver.close
    driver.switch_to.window(now_window)
  end

使い方

第1引数にseleniumのdriver、第2引数に保存対象のURL、題3引数に保存場所のPathを指定します。

ファイル名はURLから自動推測されます。

require 'selenium-webdriver'

driver = Selenium::WebDriver.for :chrome

# 対象とする画像ファイルに必要な認証情報を取得する
## e.g. 予め対象のサイトにログインしておく

save_binary(driver, "保存したい画像のURL", "/tmp/save_img/")

解説

大まかな流れは以下の通りです。
1. 取得したい画像を新しいタブで開く
2. 画像ファイルをJavaScriptで読み込み(バイナリデータ)
3. JavaScriptで読み込んたバイナリデータをruby側に渡し、ファイルに保存する
4. 開いたタブを閉じて、selenium_driverのコントロール先を元のタブに戻す

それぞれのフローについて詳しく解説していきます。

1. 取得したい画像を新しいタブで開く

    now_window = @driver.window_handle
    driver.execute_script("window.open()")
    driver.switch_to.window(@driver.window_handles.last)
    driver.get(url)

ここではSeleniumで新しいタブを開いた上で、そこで画像URLを開こうとしています。

対象の画像ファイルをあらかじめ開いておく理由として、対象の画像ファイルのドメインが違っている場合のCORS対策や、別ドメインからのリクエストをNGにしているサイトへの対応などの考えることを極力減らすためです。

逆にリファラー等で特定のページレベルで制限しているサイトを対象にする場合、移動したらNGになる可能性もあるのでこの節の処理と4.の処理は削除してください。

別タブとして開いている理由は、この一連の処理を別タブ内で完結させることで、driverの持っている状態をメソッド呼び出し前と呼び出し後で変えないようにするためです。

2. 画像ファイル読み込むJavaScriptを用意する

対象の画像ファイルをバイナリデータとしてJavaScriptで読み込みます。

JavaScriptで画像のバイナリデータを取得する処理部分は。こちらの「Seleniumで画像などのバイナリデータを保存する 」で使われているコードをほぼそのまま使用させていただきました。

3. JavaScriptでバイナリデータを読み込み、ファイルに保存する

2,で定義したJavaScriptを実行して、実行結果(=画像ファイルのバイナリデータ)を取得します。

Selenium Driverの execute_scriptメソッドは引数のJavaScriptの実行、実行結果を返り値として取得できるので、以下のコードで実行結果である画像ファイルのバイナリデータがdata_bytes に保存されます。

    data_bytes = driver.execute_script(js)

data_bytesに保存されたデータをファイルに書きこみますが、その前に保存名をURLから生成します。

    uri = URI.parse(url)
    file_name = File.basename(uri.path)

このコードでは保存対象のURLをURI.parseで分解、uriのpath部分をFile.basenameを使い、URL path部分の末尾の文字列を取得しています。

これにより、元のURLにクエリパラメータが含まれても、path部分の末尾のみ取得できます。

もしも、対象ファイルのURLがファイル名として良くない等、ファイル名の命名規則を変えたい場合があれば、file_name の生成ルールを変更してください。

最後に取得したデータを保存します。
引数の filepath と 生成したfile_name で保存先を決定し、書き込みます。

JavaScriptのcharCodeAt「0 から 65535 の整数を返す」、すなわち16bitで表現できる値を返しますが、
元コードではそれと 0xffの論理積をとって data_bytesには入れています。
この論理積の意味はこちらのサイトが詳しく説明してくださっているのでここでは省きます。

ここで重要なのは、実質的にdata_bytesに入っている値が0~0xff(255)までしかないことです。
data_bytesの値をRubyのpackメソッドを使い、バイナリ化した上で、対象ファイルに書き込みます。

このとき、値は8bitなので、テンプレートにはC*を指定します。
(Cはunsigned char (8bit 符号なし整数)の指定)

    File.open(filepath + file_name, "w") do |f|
      f.write(data_bytes.pack('C*'))
    end

4. 開いたタブを閉じて、selenium_driverのコントロール先を元のタブに戻す

最後に、1.で開いたタブをcloseして、selenium_driverがコントロールしている先を元のタブに戻します。

    driver.close
    driver.switch_to.window(now_window)

もし、1.で新規タブを開いていない場合はこの処理は不要です。

この一連の処理をメソッド内で行うことで、現在のdriverの状態をほぼ維持したまま、実行できるメソッドとなりました。

余談

↑と同様に、認証が必要なページをいわゆるCtrl + sで保存するような形(画像等をパス変換した上で保存)もしたいため、出来る方法を探していました。
ただどうにも調べた限りSelenium単体では難しそうで、今はWindows(WSL)上で動かしているrubyスクリプトから無理やりUWSCを呼び出して、Ctrl + s→保存ボタンのクリックを自動処理して動かしてます。
かなり強引なやり方 & Windowsでしか動かないので汎用的な方法がありましたが、教えていただきたいです。