ヘッドレスChrome+Javaでファイルをダウンロードする方法(Selenium)

ヘッドレス Chrome ではセキュリティ上の理由からファイルのダウンロードが禁止されていますが、Selenium + Java + ヘッドレス Chrome の組み合わせで、これをなんとかダウンロードできるようにする方法を書いておきます。

基本的には既知の方法なんですが、初見では使用パッケージが分からなかったので自分用メモ。

あと、多分あまり情報がまとまって無い気がする「この設定でもヘッドレスではダウンロードできないサイト」の情報についても触れておきます。

ヘッドレス Chrome でファイルダウンロードを可能にする設定(Java)

import部分はこんな感じ。(漏れがあったらごめんなさい)

import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;Code language: Java (java)

Javaソース部分はこんな感じ。

// Chrome 起動オプションを構成
ChromeOptions options = new ChromeOptions();
// headlessモードで起動
options.addArguments("--headless");

// Chrome WebDriver生成処理
ChromeDriverService driverService = ChromeDriverService.createDefaultService();
driver = new ChromeDriver(driverService, options);

// Headless Chrome でもファイルをダウンロードできるようにするHack
Map<String, Object> commandParams = new HashMap<>();
commandParams.put("cmd", "Page.setDownloadBehavior");
Map<String, String> params = new HashMap<>();
params.put("behavior", "allow");
params.put("downloadPath", "c:\\PATH\\TO\\DOWNLOAD_DIR_NAME");
commandParams.put("params", params);
ObjectMapper objectMapper = new ObjectMapper();
HttpClient httpClient = HttpClientBuilder.create().build();
try {
    String command = objectMapper.writeValueAsString(commandParams);
    String u = driverService.getUrl().toString() + "/session/" + driver.getSessionId() + "/chromium/send_command";
    HttpPost request = new HttpPost(u);
    request.addHeader("content-type", "application/json");
    request.setEntity(new StringEntity(command));
    httpClient.execute(request);
} catch (Exception e) {
    // Headless Chrome への切り替えHack失敗
    e.printStackTrace();
}Code language: Java (java)

もう完全に力技ですな。勘の良い方ならお気付きのとおり、ダウンロード先ディレクトリのパスに日本語が含まれると動作しない仕様です。(なんかやり方ありますかね…)

本来的には ChromeDriver クラスを継承したサブクラスを作って、Protected な execute メソッドからこんな感じでコマンドを投げるコードが書けると楽なんですけどね。

Map<String,String> paramList = new HashMap<>();
paramList.put("behavior", "allow");
paramList.put("downloadPath", "c:\\PATH\\TO\\DOWNLOAD_DIR_NAME"));
driver.execute("Page.setDownloadBehavior", paramList);Code language: Java (java)

残念ながらこの書き方だと動作せず。けっこう探しましたが、やはり、先の方法以外に穴は無いように思います。

どうやってもヘッドレスでダウンロードできないサイトがある

少なくとも Chrome 74 のヘッドレスモードでは、先の設定でもファイルをダウンロードができないパターンをいくつか確認しています。

この内、僕が把握しているのは、少なくとも以下の2パターンです。

  1. 動的に生成された JavaScript 内から location.href 関数などでファイルをダウンロードする場合
  2. (こちらはちゃんと検証していませんが)target="_blank" で新しいウィンドウを開いてファイルをダウンロードする場合

いずれの場合も、飛び先の URL を入手して Selenium から get() すればダウンロードできそうな気がしますが…。

で、Chromium のソースをざっと斜め読みした感じだと、先のトリッキーな設定は PageHandler に効いているようで、その前提において他に使えそうなコマンドはなさそうに感じました。したがって、本来的にはこの設定がどのシチュエーションでも効いてくれるべき、という気はするので、なんとなーく Chrome のバグの予感が。でも、「これはセキュリティポリシーです」と言われれば「ハイ、そうですか」と言うしかないような気もします。

憶測だけでテキトーな事を書いてしまうと、歴史的経緯からして、もともとこの権限処理は Headless 化以前には不必要だった気がしなくはないので、今はまだ実装漏れがあるのかな、という気もしなくはないけれども。

ちなみに将来の Chrome でこの部分が改善されたことに気がつかれた方は、コメント欄などで教えて頂けると助かります。

参考資料:

Hatena Pocket Line

コメントを記入