在自动化测试和网页抓取中,完整捕获整个页面内容是常见需求。传统截图只能捕获当前视窗内容,无法获取超出可视区域的页面部分。长截图技术通过截取整个滚动页面解决了这个问题,特别适用于:
- 保存完整网页存档
- 生成页面可视化报告
- 验证响应式设计
- 捕获动态加载内容
本文将深入探讨三种Java/Kotlin Selenium实现长截图的专业方案,使用无头Chrome浏览器作为运行环境。
一、CDP协议截图(推荐方案)
原理与技术优势
Chrome DevTools Protocol(CDP)是Chrome提供的底层调试协议,通过Page.captureScreenshot
命令可直接获取整个页面渲染结果,包括:
- 超出视口的滚动区域
- 固定定位元素
- CSS动画状态
核心优势:
- 原生浏览器支持,无需调整窗口大小
- 性能最佳(约比传统方法快3-5倍)
- 支持视网膜屏高分辨率截图
完整实现代码
public class CdpScreenshotter {
public static String captureFullPageScreenshot(WebDriver driver) {
// 1. 匹配CDP版本
Optional<CdpVersion> version = new CdpVersionFinder()
.match(driver.getCapabilities().getBrowserVersion());
if (!version.isPresent()) {
throw new RuntimeException("未找到匹配的CDP版本,请检查浏览器版本");
}
// 2. 配置截图参数
Map<String, Object> params = new HashMap<>();
params.put("format", "png");
params.put("quality", 90); // 图片质量 (0-100)
params.put("captureBeyondViewport", true); // 关键参数:捕获超出视口的内容
params.put("fromSurface", true); // 捕获合成后的表面
// 3. 执行CDP命令
@SuppressWarnings("unchecked")
Map<String, String> response = (Map<String, String>)
((HasCdp) driver).executeCdpCommand("Page.captureScreenshot", params);
// 4. 提取并处理base64数据
return response.get("data");
}
public static void saveScreenshot(String base64Data, String filePath) {
byte[] imageBytes = Base64.getDecoder().decode(
base64Data.replaceFirst("^data:image/\\w+;base64,", "")
);
try (FileOutputStream stream = new FileOutputStream(filePath)) {
stream.write(imageBytes);
} catch (IOException e) {
throw new RuntimeException("截图保存失败", e);
}
}
}
Kotlin实现版本
object CdpScreenshotter {
fun captureFullPageScreenshot(driver: WebDriver): String {
val version = CdpVersionFinder()
.match(driver.capabilities.getBrowserVersion())
?: throw RuntimeException("未找到匹配的CDP版本")
val params = mutableMapOf<String, Any>(
"format" to "png",
"quality" to 90,
"captureBeyondViewport" to true,
"fromSurface" to true
)
val response = (driver as HasCdp).executeCdpCommand(
"Page.captureScreenshot", params
) as Map<String, String>
return response["data"]!!
}
fun saveScreenshot(base64Data: String, filePath: String) {
val cleanData = base64Data.replace(Regex("^data:image/\\w+;base64,"), "")
val imageBytes = Base64.getDecoder().decode(cleanData)
File(filePath).writeBytes(imageBytes)
}
}
最佳实践建议
- 版本兼容性处理:定期更新
cdpVersionFinder
库,确保支持新版Chrome - 内存优化:处理大页面时使用流式写入避免OOM
- 错误处理:添加重试机制应对网络波动
- 性能监控:记录命令执行时间优化测试套件
二、浏览器窗口调整方案
实现原理与适用场景
通过JavaScript获取页面完整尺寸,然后调整浏览器窗口大小至整个页面尺寸,最后执行传统截图。
适用场景:
- 不支持CDP的老版本浏览器
- 需要兼容多浏览器引擎(Firefox, Safari等)
- 简单页面快速实现
增强版实现(解决常见问题)
public class WindowResizeScreenshotter {
public static <T> T captureFullPage(TakesScreenshot instance, OutputType<T> outputType) {
WebDriver driver = extractDriver(instance);
// 保存原始窗口状态
Dimension originalSize = driver.manage().window().getSize();
Point originalPosition = driver.manage().window().getPosition();
try {
// 计算页面完整尺寸
Dimension pageSize = calculateFullPageSize(driver);
// 特殊处理:应对最小窗口限制
Dimension adjustedSize = ensureMinimumSize(pageSize);
// 调整窗口
driver.manage().window().setSize(adjustedSize);
// 等待页面重排完成
waitForPageSettled(driver);
// 执行截图
return instance.getScreenshotAs(outputType);
} finally {
// 恢复原始状态
driver.manage().window().setPosition(originalPosition);
driver.manage().window().setSize(originalSize);
}
}
private static Dimension calculateFullPageSize(WebDriver driver) {
JavascriptExecutor js = (JavascriptExecutor) driver;
// 获取包含视口和滚动区域的完整尺寸
long fullHeight = (Long) js.executeScript(
"return Math.max(" +
"document.documentElement.scrollHeight, " +
"document.body.scrollHeight, " +
"document.documentElement.clientHeight" +
");"
);
long fullWidth = (Long) js.executeScript(
"return Math.max(" +
"document.documentElement.scrollWidth, " +
"document.body.scrollWidth, " +
"document.documentElement.clientWidth" +
");"
);
return new Dimension((int) fullWidth, (int) fullHeight);
}
private static Dimension ensureMinimumSize(Dimension size) {
// 确保尺寸不小于浏览器允许的最小值
int minWidth = Math.max(size.width, 100);
int minHeight = Math.max(size.height, 100);
return new Dimension(minWidth, minHeight);
}
private static void waitForPageSettled(WebDriver driver) {
new WebDriverWait(driver, Duration.ofSeconds(5))
.ignoring(StaleElementReferenceException.class)
.until(d -> {
Object result = ((JavascriptExecutor) d)
.executeScript("return document.readyState");
return "complete".equals(result);
});
}
}
注意事项
- 无头模式必须:确保使用Headless Chrome避免可见窗口限制
ChromeOptions options = new ChromeOptions(); options.addArguments("--headless=new"); // Chrome 109+推荐语法 options.addArguments("--window-size=1920,1080");
- 页面重排问题:调整大小后等待页面稳定
- 内存限制:超大页面可能导致浏览器崩溃
- 固定定位元素:可能被错误截断
三、AShot高级截图库方案
框架优势与专业功能
AShot是专为Selenium设计的高级截图库,提供:
- 智能视口拼接算法
- 设备像素比(DPR)支持
- 元素级截图能力
- 阴影DOM处理
专业级实现(含DPR处理)
public class AShotScreenshotter {
public static BufferedImage captureFullPage(WebDriver driver) {
// 获取设备像素比
float dpr = getDevicePixelRatio(driver);
// 配置专业级截图策略
ShootingStrategy strategy = ShootingStrategies.viewportRetina(
new WebDriverCoordsProvider(),
new HorizontalScrollDecorator(),
new VerticalScrollDecorator(),
dpr
).setScrollTimeout(1000);
return new AShot()
.shootingStrategy(strategy)
.addIgnoredAreas(calculateIgnoredAreas(driver)) // 忽略动态广告区域
.takeScreenshot(driver)
.getImage();
}
private static float getDevicePixelRatio(WebDriver driver) {
try {
Object result = ((JavascriptExecutor) driver)
.executeScript("return window.devicePixelRatio || 1;");
return Float.parseFloat(result.toString());
} catch (Exception e) {
return 1.0f;
}
}
private static Collection<Coords> calculateIgnoredAreas(WebDriver driver) {
// 示例:忽略已知广告区域
List<WebElement> ads = driver.findElements(By.cssSelector(".ad-container"));
return ads.stream()
.map(e -> {
Point location = e.getLocation();
Dimension size = e.getSize();
return new Coords(
location.x,
location.y,
size.width,
size.height
);
})
.collect(Collectors.toList());
}
public static void saveImage(BufferedImage image, String path) {
try {
ImageIO.write(image, "PNG", new File(path));
} catch (IOException e) {
throw new RuntimeException("图片保存失败", e);
}
}
}
高级功能配置
// 创建自定义截图策略
ShootingStrategy advancedStrategy = new ShootingStrategy() {
@Override
public BufferedImage getScreenshot(WebDriver driver) {
// 自定义截图逻辑
}
@Override
public BufferedImage getScreenshot(WebDriver driver, WebElement element) {
// 元素级截图
}
};
// 配置复杂截图参数
AShot aShot = new AShot()
.withDpr(2.0f) // 明确设置设备像素比
.imageCropper(new IndentCropper(10)) // 添加10像素边框
.coordsProvider(new SmartCoordsProvider()) // 智能坐标检测
.screenshotDecorator(new BlurDecorator(5)); // 添加模糊效果
疑难问题解决方案
1. 截图出现空白区域
原因:页面包含懒加载内容
解决方案:
// 滚动页面触发加载
js.executeScript("window.scrollTo(0, document.body.scrollHeight)");
Thread.sleep(1000); // 等待内容加载
2. CDP版本不匹配
解决方案:自动版本探测
public String findCompatibleCdpVersion(String browserVersion) {
List<String> versions = Arrays.asList("115", "114", "113");
for (String v : versions) {
if (browserVersion.startsWith(v)) return v;
}
return "latest";
}
3. 超大页面内存溢出
优化策略:
// 分块截图并合并
List<BufferedImage> segments = new ArrayList<>();
int segmentHeight = 5000; // 5,000像素分段
for (int y = 0; y < totalHeight; y += segmentHeight) {
js.executeScript("window.scrollTo(0, " + y + ")");
BufferedImage segment = // 截取当前视口
segments.add(segment);
}
// 使用ImageIO合并图像
结论
- 现代浏览器优先选择CDP方案:性能最佳,实现简单
- 兼容性要求选择窗口调整:适合跨浏览器测试
- 复杂页面使用AShot:处理特殊布局和元素
- 无头模式需要的配置:
ChromeOptions options = new ChromeOptions(); options.addArguments("--headless=new"); options.addArguments("--disable-gpu"); options.addArguments("--no-sandbox");