在自動化測試和網頁抓取中,完整捕獲整個頁面內容是常見需求。傳統截圖只能捕獲當前視窗內容,無法獲取超出可視區域的頁面部分。長截圖技術通過截取整個滾動頁面解決了這個問題,特別適用于:
- 保存完整網頁存檔
- 生成頁面可視化報告
- 驗證響應式設計
- 捕獲動態加載內容
本文將深入探討三種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");