在自动化测试和网页抓取中,完整捕获整个页面内容是常见需求。传统截图只能捕获当前视窗内容,无法获取超出可视区域的页面部分。长截图技术通过截取整个滚动页面解决了这个问题,特别适用于:
- 保存完整网页存档
- 生成页面可视化报告
- 验证响应式设计
- 捕获动态加载内容
本文将深入探讨三种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() {@Overridepublic BufferedImage getScreenshot(WebDriver driver) {// 自定义截图逻辑}@Overridepublic 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");