web-dev-qa-db-ja.com

SeleniumでChromeで全画面のスクリーンショットを撮る

私はこれが不可能だったことを知っています before ですが、次のアップデートで:

https://developers.google.com/web/updates/2017/04/devtools-release-notes#screenshots

これはChrome Dev Toolsを使用して可能と思われます。

JavaでSeleniumを使用することは可能ですか?

15
Majid Laissi

Selenium Webdriverを使用してこれを行うには、Javaに少し手間がかかります。新しいChromeコマンドを追加する新しいDriverCommandExecutorを作成します。

import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.remote.CommandInfo;
import org.openqa.Selenium.remote.http.HttpMethod;
import org.openqa.Selenium.remote.service.DriverCommandExecutor;
import org.openqa.Selenium.remote.service.DriverService;

public class MyChromeDriverCommandExecutor extends DriverCommandExecutor {
    private static final ImmutableMap<String, CommandInfo> CHROME_COMMAND_NAME_TO_URL;

    public MyChromeDriverCommandExecutor(DriverService service) {
        super(service, CHROME_COMMAND_NAME_TO_URL);
    }

    static {
        CHROME_COMMAND_NAME_TO_URL = ImmutableMap.of("launchApp", new CommandInfo("/session/:sessionId/chromium/launch_app", HttpMethod.POST)
        , "sendCommandWithResult", new CommandInfo("/session/:sessionId/chromium/send_command_and_get_result", HttpMethod.POST)
        );
    }
}

その後、新しいChromeDriverクラスを作成して、このクラスを使用する必要があります。オリジナルにはコマンドエグゼキュータを置き換えるコンストラクタがないため、クラスを作成する必要があります...したがって、新しいクラスは次のようになります。

import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.Capabilities;
import org.openqa.Selenium.WebDriverException;
import org.openqa.Selenium.chrome.ChromeDriverService;
import org.openqa.Selenium.html5.LocalStorage;
import org.openqa.Selenium.html5.Location;
import org.openqa.Selenium.html5.LocationContext;
import org.openqa.Selenium.html5.SessionStorage;
import org.openqa.Selenium.html5.WebStorage;
import org.openqa.Selenium.interactions.HasTouchScreen;
import org.openqa.Selenium.interactions.TouchScreen;
import org.openqa.Selenium.mobile.NetworkConnection;
import org.openqa.Selenium.remote.FileDetector;
import org.openqa.Selenium.remote.RemoteTouchScreen;
import org.openqa.Selenium.remote.RemoteWebDriver;
import org.openqa.Selenium.remote.html5.RemoteLocationContext;
import org.openqa.Selenium.remote.html5.RemoteWebStorage;
import org.openqa.Selenium.remote.mobile.RemoteNetworkConnection;

public class MyChromeDriver  extends RemoteWebDriver implements LocationContext, WebStorage, HasTouchScreen, NetworkConnection {
    private RemoteLocationContext locationContext;
    private RemoteWebStorage webStorage;
    private TouchScreen touchScreen;
    private RemoteNetworkConnection networkConnection;

    //public MyChromeDriver() {
    //  this(ChromeDriverService.createDefaultService(), new ChromeOptions());
    //}
    //
    //public MyChromeDriver(ChromeDriverService service) {
    //  this(service, new ChromeOptions());
    //}

    public MyChromeDriver(Capabilities capabilities) {
        this(ChromeDriverService.createDefaultService(), capabilities);
    }

    //public MyChromeDriver(ChromeOptions options) {
    //  this(ChromeDriverService.createDefaultService(), options);
    //}

    public MyChromeDriver(ChromeDriverService service, Capabilities capabilities) {
        super(new MyChromeDriverCommandExecutor(service), capabilities);
        this.locationContext = new RemoteLocationContext(this.getExecuteMethod());
        this.webStorage = new RemoteWebStorage(this.getExecuteMethod());
        this.touchScreen = new RemoteTouchScreen(this.getExecuteMethod());
        this.networkConnection = new RemoteNetworkConnection(this.getExecuteMethod());
    }

    @Override
    public void setFileDetector(FileDetector detector) {
        throw new WebDriverException("Setting the file detector only works on remote webdriver instances obtained via RemoteWebDriver");
    }

    @Override
    public LocalStorage getLocalStorage() {
        return this.webStorage.getLocalStorage();
    }

    @Override
    public SessionStorage getSessionStorage() {
        return this.webStorage.getSessionStorage();
    }

    @Override
    public Location location() {
        return this.locationContext.location();
    }

    @Override
    public void setLocation(Location location) {
        this.locationContext.setLocation(location);
    }

    @Override
    public TouchScreen getTouch() {
        return this.touchScreen;
    }

    @Override
    public ConnectionType getNetworkConnection() {
        return this.networkConnection.getNetworkConnection();
    }

    @Override
    public ConnectionType setNetworkConnection(ConnectionType type) {
        return this.networkConnection.setNetworkConnection(type);
    }

    public void launchApp(String id) {
        this.execute("launchApp", ImmutableMap.of("id", id));
    }
}

これはほとんどが元のクラスのコピーですが、一部のコンストラクターは無効になっています(必要なコードの一部はパッケージプライベートであるため)。これらのコンストラクタが必要な場合は、クラスをパッケージorg.openqa.Selenium.chromeに配置する必要があります。

これらの変更により、Florent Bが示すように必要なコードを呼び出すことができますが、Selenium APIを使用してJavaになりました:

import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.remote.Command;
import org.openqa.Selenium.remote.Response;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import Java.awt.image.BufferedImage;
import Java.io.ByteArrayInputStream;
import Java.io.File;
import Java.io.FileOutputStream;
import Java.io.IOException;
import Java.util.Base64;
import Java.util.HashMap;
import Java.util.Map;

public class ChromeExtender {
    @Nonnull
    private MyChromeDriver m_wd;

    public ChromeExtender(@Nonnull MyChromeDriver wd) {
        m_wd = wd;
    }

    public void takeScreenshot(@Nonnull File output) throws Exception {
        Object visibleSize = evaluate("({x:0,y:0,width:window.innerWidth,height:window.innerHeight})");
        Long visibleW = jsonValue(visibleSize, "result.value.width", Long.class);
        Long visibleH = jsonValue(visibleSize, "result.value.height", Long.class);

        Object contentSize = send("Page.getLayoutMetrics", new HashMap<>());
        Long cw = jsonValue(contentSize, "contentSize.width", Long.class);
        Long ch = jsonValue(contentSize, "contentSize.height", Long.class);

        /*
         * In chrome 61, delivered one day after I wrote this comment, the method forceViewport was removed.
         * I commented it out here with the if(false), and hopefully wrote a working alternative in the else 8-/
         */
        if(false) {
            send("Emulation.setVisibleSize", ImmutableMap.of("width", cw, "height", ch));
            send("Emulation.forceViewport", ImmutableMap.of("x", Long.valueOf(0), "y", Long.valueOf(0), "scale", Long.valueOf(1)));
        } else {
            send("Emulation.setDeviceMetricsOverride",
                ImmutableMap.of("width", cw, "height", ch, "deviceScaleFactor", Long.valueOf(1), "mobile", Boolean.FALSE, "fitWindow", Boolean.FALSE)
            );
            send("Emulation.setVisibleSize", ImmutableMap.of("width", cw, "height", ch));
        }

        Object value = send("Page.captureScreenshot", ImmutableMap.of("format", "png", "fromSurface", Boolean.TRUE));

        // Since chrome 61 this call has disappeared too; it does not seem to be necessary anymore with the new code.
        // send("Emulation.resetViewport", ImmutableMap.of()); 
        send("Emulation.setVisibleSize", ImmutableMap.of("x", Long.valueOf(0), "y", Long.valueOf(0), "width", visibleW, "height", visibleH));

        String image = jsonValue(value, "data", String.class);
        byte[] bytes = Base64.getDecoder().decode(image);

        try(FileOutputStream fos = new FileOutputStream(output)) {
            fos.write(bytes);
        }
    }

    @Nonnull
    private Object evaluate(@Nonnull String script) throws IOException {
        Map<String, Object> param = new HashMap<>();
        param.put("returnByValue", Boolean.TRUE);
        param.put("expression", script);

        return send("Runtime.evaluate", param);
    }

    @Nonnull
    private Object send(@Nonnull String cmd, @Nonnull Map<String, Object> params) throws IOException {
        Map<String, Object> exe = ImmutableMap.of("cmd", cmd, "params", params);
        Command xc = new Command(m_wd.getSessionId(), "sendCommandWithResult", exe);
        Response response = m_wd.getCommandExecutor().execute(xc);

        Object value = response.getValue();
        if(response.getStatus() == null || response.getStatus().intValue() != 0) {
            //System.out.println("resp: " + response);
            throw new MyChromeDriverException("Command '" + cmd + "' failed: " + value);
        }
        if(null == value)
            throw new MyChromeDriverException("Null response value to command '" + cmd + "'");
        //System.out.println("resp: " + value);
        return value;
    }

    @Nullable
    static private <T> T jsonValue(@Nonnull Object map, @Nonnull String path, @Nonnull Class<T> type) {
        String[] segs = path.split("\\.");
        Object current = map;
        for(String name: segs) {
            Map<String, Object> cm = (Map<String, Object>) current;
            Object o = cm.get(name);
            if(null == o)
                return null;
            current = o;
        }
        return (T) current;
    }
}

これにより、指定したコマンドを使用して、png形式の画像を含むファイルを作成できます。もちろん、バイトに対してImageIO.read()を使用してBufferedImageを直接作成することもできます。

3
fjalvingh

はい、Chrome v59。以降、Seleniumでフルスクリーンのスクリーンショットを撮ることができます。Chrome=ドライバには、DevTools APIを直接呼び出す2つの新しいエンドポイントがあります。

/session/:sessionId/chromium/send_command_and_get_result
/session/:sessionId/chromium/send_command

Selenium APIはこれらのコマンドを実装していないため、基礎となるexecutorで直接送信する必要があります。簡単ではありませんが、少なくともDevToolsとまったく同じ結果を生成することは可能です。

pythonローカルインスタンスまたはリモートインスタンスで作業する例を示します。

from Selenium import webdriver
import json, base64

capabilities = {
  'browserName': 'chrome',
  'chromeOptions':  {
    'useAutomationExtension': False,
    'args': ['--disable-infobars']
  }
}

driver = webdriver.Chrome(desired_capabilities=capabilities)
driver.get("https://stackoverflow.com/questions")

png = chrome_takeFullScreenshot(driver)

with open(r"C:\downloads\screenshot.png", 'wb') as f:
  f.write(png)

、および全ページのスクリーンショットを撮るコード:

def chrome_takeFullScreenshot(driver) :

  def send(cmd, params):
    resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
    url = driver.command_executor._url + resource
    body = json.dumps({'cmd':cmd, 'params': params})
    response = driver.command_executor._request('POST', url, body)
    return response.get('value')

  def evaluate(script):
    response = send('Runtime.evaluate', {'returnByValue': True, 'expression': script})
    return response['result']['value']

  metrics = evaluate( \
    "({" + \
      "width: Math.max(window.innerWidth, document.body.scrollWidth, document.documentElement.scrollWidth)|0," + \
      "height: Math.max(innerHeight, document.body.scrollHeight, document.documentElement.scrollHeight)|0," + \
      "deviceScaleFactor: window.devicePixelRatio || 1," + \
      "mobile: typeof window.orientation !== 'undefined'" + \
    "})")
  send('Emulation.setDeviceMetricsOverride', metrics)
  screenshot = send('Page.captureScreenshot', {'format': 'png', 'fromSurface': True})
  send('Emulation.clearDeviceMetricsOverride', {})

  return base64.b64decode(screenshot['data'])

Javaの場合:

public static void main(String[] args) throws Exception {

    ChromeOptions options = new ChromeOptions();
    options.setExperimentalOption("useAutomationExtension", false);
    options.addArguments("disable-infobars");

    ChromeDriverEx driver = new ChromeDriverEx(options);

    driver.get("https://stackoverflow.com/questions");
    File file = driver.getFullScreenshotAs(OutputType.FILE);
}
import Java.lang.reflect.Method;
import Java.util.Map;
import com.google.common.collect.ImmutableMap;
import org.openqa.Selenium.OutputType;
import org.openqa.Selenium.chrome.ChromeDriver;
import org.openqa.Selenium.chrome.ChromeDriverService;
import org.openqa.Selenium.chrome.ChromeOptions;
import org.openqa.Selenium.remote.CommandInfo;
import org.openqa.Selenium.remote.HttpCommandExecutor;
import org.openqa.Selenium.remote.http.HttpMethod;


public class ChromeDriverEx extends ChromeDriver {

    public ChromeDriverEx() throws Exception {
        this(new ChromeOptions());
    }

    public ChromeDriverEx(ChromeOptions options) throws Exception {
        this(ChromeDriverService.createDefaultService(), options);
    }

    public ChromeDriverEx(ChromeDriverService service, ChromeOptions options) throws Exception {
        super(service, options);
        CommandInfo cmd = new CommandInfo("/session/:sessionId/chromium/send_command_and_get_result", HttpMethod.POST);
        Method defineCommand = HttpCommandExecutor.class.getDeclaredMethod("defineCommand", String.class, CommandInfo.class);
        defineCommand.setAccessible(true);
        defineCommand.invoke(super.getCommandExecutor(), "sendCommand", cmd);
    }

    public <X> X getFullScreenshotAs(OutputType<X> outputType) throws Exception {
        Object metrics = sendEvaluate(
            "({" +
            "width: Math.max(window.innerWidth,document.body.scrollWidth,document.documentElement.scrollWidth)|0," +
            "height: Math.max(window.innerHeight,document.body.scrollHeight,document.documentElement.scrollHeight)|0," +
            "deviceScaleFactor: window.devicePixelRatio || 1," +
            "mobile: typeof window.orientation !== 'undefined'" +
            "})");
        sendCommand("Emulation.setDeviceMetricsOverride", metrics);
        Object result = sendCommand("Page.captureScreenshot", ImmutableMap.of("format", "png", "fromSurface", true));
        sendCommand("Emulation.clearDeviceMetricsOverride", ImmutableMap.of());
        String base64EncodedPng = (String)((Map<String, ?>)result).get("data");
        return outputType.convertFromBase64Png(base64EncodedPng);
    }

    protected Object sendCommand(String cmd, Object params) {
        return execute("sendCommand", ImmutableMap.of("cmd", cmd, "params", params)).getValue();
    }

    protected Object sendEvaluate(String script) {
        Object response = sendCommand("Runtime.evaluate", ImmutableMap.of("returnByValue", true, "expression", script));
        Object result = ((Map<String, ?>)response).get("result");
        return ((Map<String, ?>)result).get("value");
    }
}
23
Florent B.