2017-07-19 57 views
7

wiem, że to nie było możliwe before, ale teraz z następującym aktualizacji:Take pełnoekranowego strona strzał w Chrome z selenem

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

to wydaje się być możliwe przy użyciu narzędzi chrom Dev.

Czy to jest możliwe teraz przy użyciu selenu w java?

+0

AFAIK jest to możliwe w 'selenium' +' PhantomJS' – Andersson

+2

Tak, ale potrzebuję sterownika Chrome –

+0

Jeśli używasz Javy, wygląda na to, że ktoś stworzył ładną bibliotekę, aby zrobić coś dla ciebie. Może spróbuj tego https://github.com/yandex-qatools/ashot/ – stewartm

Odpowiedz

2

Aby to zrobić z Selenium Webdriver w Javie zajmuje trochę pracy. Jak sugeruje Florent B. musimy zmienić niektóre zastosowania klas przez domyślny ChromeDriver, aby to działało. Najpierw musimy zrobić nowy DriverCommandExecutor który dodaje komendy nowy Chrome:

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) 
     ); 
    } 
} 

Następnie musimy stworzyć nową klasę ChromeDriver które następnie użyć tej rzeczy. Musimy stworzyć klasę, ponieważ oryginalne nie ma konstruktora, który pozwala nam zastąpić wykonawcę poleceń ... Tak więc nowa klasa staje:

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)); 
    } 
} 

Jest to głównie kopią oryginalnej klasy, ale niektórzy konstruktorzy niepełnosprawnych (ponieważ niektóre z potrzebnych kodu są pakietami prywatnymi). Jeśli potrzebujesz tych konstruktorów, musisz umieścić klasy w pakiecie org.openqa.selenium.chrome.

Z tymi zmianami są w stanie wywołać żądany kod, jak pokazano przez Florent B., ale teraz w Javie z API Selen:

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; 
    } 
} 

ta umożliwia korzystanie z poleceń, jak określone i tworzy plik z obrazem w formacie png w środku. Oczywiście można również bezpośrednio utworzyć obraz BufferedImage za pomocą ImageIO.read() na bajtach.

+0

Świetna robota !!! Czy istnieje możliwość dodania importu do klasy ChromeExtender? Zgadywałem na kilku z nich. – SiKing

+0

Witam @SiKing, dodałem import. Ale uważaj, Chrome 61, który dostałem dzisiaj, usuwa wywołanie forceViewport użyte w moim oryginalnym przykładzie. Dodałem alternatywę, która wydaje się działać dla mnie w kodzie do takeScreenshot(). – fjalvingh

+0

Emulation.resetViewport "wydaje się nie działać na chrome 61 (było OK na chrome 60) .Ale istnieje alternatywa? –

10

Tak, możliwe jest wykonanie pełnego ekranu strony z Selenium od wersji Chrome w59. Kierowca Chrome ma dwa nowe punkty końcowe, aby wywołać bezpośrednio z DevTools API:

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

selenu API nie realizuje tych poleceń, więc musisz wysłać je bezpośrednio z podstawową wykonawca. To nie jest proste, ale przynajmniej można uzyskać dokładnie taki sam wynik, jak DevTools.

Oto przykład z pyton działa na lokalnym lub zdalnym przykład:

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) 

, a kod wziąć pełną stronę ekranu:

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']) 

Z 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"); 
    } 
} 
+1

wszelkie wskazówki, jak to zrobić w java? –

+2

Spróbuj przedłużyć 'HttpCommandExecutor' i wywołaj' defineCommand', aby użyć nowego punktu końcowego. Następnie stwórz nowy zdalny dysk sieciowy z rozszerzonym 'HttpCommandExecutor'. –

+1

https://github.com/SeleniumHQ/selenium/blob/master/java/client/src/org/openqa/selenium/chrome/ChromeDriverCommandExecutor.java –