0%

Selenium专题:截图

普通截图

如果只是生成当前浏览器可视范围内的截图,则 Selenium 提供了统一的 API 实现——TakeScreenshot.getScreenshotAs()

1
File image = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);

Driver 类型与强转

虽然所有 WebDriver 实现类也都实现了 TakeScreenshot 接口,但是如果是严格面向接口编程,则所有 Driver 都是通过 WebDriver 接口变量持有的。因此,通常需要强转。

值得注意的是,生成的截图只包括浏览器可视范围,换句话说,如果存在滚动条,则截取的并非完整页面。

完整页面截图

怎样截取完整页面截图呢?我们先分浏览器种类讨论。

Firefox 最简单,FirefoxDriver 自带截图的方法:

1
File image = ((FirefoxDriver)driver).getFullPageScreenshotAs(OutputType.FILE);

ChromeDrvierChromiumDriver 都没有实现该方法。

但是,熟悉 DevTools 的小伙伴应该知道,我们可以通过执行“Capture full size screenshot”命令截取完整页面截图。所以,只要通过 CDP 调用该命令即可。

【扩展说明】

DevTools 中使用快捷键 Ctrl + Shift + P 执行命令。

查看 Chrome DevTools Protocol 文档(详见这里),可以看到只有一个 Page.captureScreenshot 命令——默认行为就是“普通截图”。

不过,我们可以利用这个命令进行完整页面截图。下面说说找到的两种方案。

Chrome 完整页面截图方案一

具体思路是:先将设备屏幕大小设置为页面相同大小,就可以直接截取完整页面截图了。

【扩展说明】

据查看了 Chrome 源码的小伙伴说,“Capture full size screenshot”命令底层就是使用的这种方案。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 执行 JS 代码获取完整页面高度
var height = ((JavascriptExecutor) chromeDriver)
.executeScript("return document.body.scrollHeight");
// 通过 CDP 命令设置设备屏幕大小
var deviceParams = new HashMap<String, Object>();
deviceParams.put("mobile", false);
// 假设只有垂直滚动条,未修改屏幕宽度
deviceParams.put("width", 0);
deviceParams.put("height", height);
deviceParams.put("deviceScaleFactor", 1);
chromeDriver.executeCdpCommand("Emulation.setDeviceMetricsOverride",
deviceParams);
// 截图并写入文件
var resource = chromeDriver.executeCdpCommand("Page.captureScreenshot",
new HashMap<>());
String data = (String) resource.get("data");
byte[] bytes = Base64.getDecoder().decode(data);
Files.write(Paths.get("E:/full.png"), bytes);
// 注意重置屏幕
chromeDriver.executeCdpCommand("Emulation.clearDeviceMetricsOverride",
new HashMap<>());

Chrome 完整页面截图方案二

思路:配置截图命令的实验属性,改变其默认行为,使其可以超出视窗截图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 这里使用了 CDP 命令获取页面大小
Map<String, Object> rect = (Map<String, Object>)
(driver.executeCdpCommand("Page.getLayoutMetrics",
new HashMap<>()))
.get("contentSize");
var width = rect.get("width");
var height = rect.get("height");
var clipParams = new HashMap<String, Object>();
clipParams.put("width", width);
clipParams.put("height", height);
clipParams.put("x", 0);
clipParams.put("y", 0);
clipParams.put("scale", 1);
var captureParams = new HashMap<String, Object>();
// 核心配置属性是:clip 和 captureBeyondViewport
captureParams.put("clip", clipParams);
captureParams.put("captureBeyondViewport", true);
captureParams.put("fromSurface", true);
var resource = driver.executeCdpCommand("Page.captureScreenshot",
captureParams);

String data = (String) resource.get("data");
byte[] bytes = Base64.getDecoder().decode(data);
Files.write(Paths.get("E:/full.png"), bytes);

【对比】

方案一有更好的兼容性(毕竟是浏览器底层实现思路),新旧版本 Chrome 上没有发现什么差异。

方案二兼容性更差。因为使用了实验属性 captureBeyondViewport,老版本 Chrome(如笔者用的 80 版)可能不生效(100 版下是有效的)。

其他通用思路

有没有通用方案?答案是:可能算有。

可以使用 Ashot 库进行完整页面截图,它的原理是滚动截图并拼接。

从原理上说,任意浏览器都是通用的,但是默认情况下,它只适合处理比较简单的可以滚动截屏的页面。

从效果来看,对一些比较复杂的场景处理起来并没有想像中那样完美,比如:

  • 最后页非整页滚动,拼接可能存在问题
  • 对于页眉页脚拼接可能会重复显示
  • 固定悬浮工具栏或菜单一类元素可能会重复显示

可能通过一些自定义截取方式可以消除这些问题,但可能不会太简单,有兴趣可以自行尝试。

参考

Chrome DevTools Protocol

GitHub - ashot

ashot API doc