Java/Kotlin selenium 无头浏览器 [Headless Chrome] 实现长截图 三种方式

在自动化测试和网页抓取中,完整捕获整个页面内容 是常见需求。传统截图只能捕获当前视窗内容,无法获取超出可视区域的页面部分。长截图技术通过截取整个滚动页面解决了这个问题,特别适用于:

  1. 保存完整网页存档
  2. 生成页面可视化报告
  3. 验证响应式设计
  4. 捕获动态加载内容

本文将深入探讨三种Java/Kotlin Selenium实现长截图的专业方案,使用无头Chrome浏览器作为运行环境。


一、CDP协议截图(推荐方案)

原理与技术优势

Chrome DevTools Protocol(CDP)是Chrome提供的底层调试协议 ,通过Page.captureScreenshot命令可直接获取整个页面渲染结果,包括:

  • 超出视口的滚动区域
  • 固定定位元素
  • CSS动画状态

核心优势

  • 原生浏览器支持,无需调整窗口大小
  • 性能最佳(约比传统方法快3-5倍)
  • 支持视网膜屏高分辨率截图

完整实现代码

java 复制代码
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实现版本

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

最佳实践建议

  1. 版本兼容性处理 :定期更新cdpVersionFinder库,确保支持新版Chrome
  2. 内存优化:处理大页面时使用流式写入避免OOM
  3. 错误处理:添加重试机制应对网络波动
  4. 性能监控:记录命令执行时间优化测试套件

二、浏览器窗口调整方案

实现原理与适用场景

通过JavaScript获取页面完整尺寸,然后调整浏览器窗口大小至整个页面尺寸,最后执行传统截图。

适用场景

  • 不支持CDP的老版本浏览器
  • 需要兼容多浏览器引擎(Firefox, Safari等)
  • 简单页面快速实现

增强版实现(解决常见问题)

java 复制代码
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);
            });
    }
}

注意事项

  1. 无头模式必须 :确保使用Headless Chrome避免可见窗口限制

    java 复制代码
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--headless=new"); // Chrome 109+推荐语法
    options.addArguments("--window-size=1920,1080");
  2. 页面重排问题:调整大小后等待页面稳定

  3. 内存限制:超大页面可能导致浏览器崩溃

  4. 固定定位元素:可能被错误截断


三、AShot高级截图库方案

框架优势与专业功能

AShot是专为Selenium设计的高级截图库,提供:

  • 智能视口拼接算法
  • 设备像素比(DPR)支持
  • 元素级截图能力
  • 阴影DOM处理

专业级实现(含DPR处理)

java 复制代码
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);
        }
    }
}

高级功能配置

java 复制代码
// 创建自定义截图策略
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. 截图出现空白区域

原因 :页面包含懒加载内容
解决方案

java 复制代码
// 滚动页面触发加载
js.executeScript("window.scrollTo(0, document.body.scrollHeight)");
Thread.sleep(1000); // 等待内容加载

2. CDP版本不匹配

解决方案:自动版本探测

java 复制代码
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. 超大页面内存溢出

优化策略

java 复制代码
// 分块截图并合并
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合并图像

结论

  1. 现代浏览器优先选择CDP方案:性能最佳,实现简单

  2. 兼容性要求选择窗口调整:适合跨浏览器测试

  3. 复杂页面使用AShot:处理特殊布局和元素

  4. 无头模式需要的配置

    java 复制代码
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--headless=new");
    options.addArguments("--disable-gpu");
    options.addArguments("--no-sandbox");

简单页面 兼容性需求 复杂动态页面 开始截图 页面类型 使用CDP方案 使用窗口调整方案 使用AShot方案

相关推荐
陈旭金-小金子3 分钟前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
LUCIAZZZ17 分钟前
钉钉机器人-自定义卡片推送快速入门
java·jvm·spring boot·机器人·钉钉·springboot
优秀13535 分钟前
java33
java
fajianchen1 小时前
Spring中观察者模式的应用
java·开发语言
库库林_沙琪马1 小时前
深入理解 @JsonGetter:精准掌控前端返回数据格式!
java·前端
手握风云-2 小时前
JavaEE初阶第一期:计算机是如何 “思考” 的(上)
java·java-ee
普通的冒险者2 小时前
微博项目(总体搭建)
java·开发语言
BAGAE2 小时前
Flutter 与原生技术(Objective-C/Swift,java)的关系
java·开发语言·macos·objective-c·cocoa·智慧城市·hbase
江湖有缘2 小时前
使用obsutil工具在OBS上完成基本的数据存取【玩转华为云】
android·java·华为云
bxlj_jcj2 小时前
Kafka环境搭建全攻略:从Docker到Java实战
java·docker·kafka