通过Selenium实现网页截图来生成应用封面

一. 什么是Selenium

Selenium 是一个开源的 Web 应用程序自动化测试工具集,主要用于模拟用户在浏览器中的各种操作,实现对 Web 应用的自动化测试、数据爬取、定期任务执行等功能。其核心是 WebDriver,它提供了一套跨编程语言、跨浏览器的 API,让开发者可以通过代码控制浏览器行为(如点击、输入、跳转、截图等)。

Selenium的官方文档:Selenium 浏览器自动化项目 | Selenium

在实际使用建议搭配WebDriverManager使用,可以自动化管理浏览器驱动。解决了开发中的许多痛点:

  1. 无需手动下载浏览器驱动,告别找版本、下文件的繁琐;

  2. 自动匹配浏览器版本,避免因版本不兼容导致的运行失败;

  3. 省去环境变量配置,代码无需硬编码驱动路径,跨机器/系统运行更顺畅;

  4. 简化跨环境和团队协作,不用手动同步各环境的驱动版本,降低维护成本。

对比维度 不使用 WebDriverManager 使用 WebDriverManager
驱动与版本 手动下载,需人工匹配浏览器版本,易因更新报错 自动检测浏览器版本,下载匹配驱动,适配更新
环境配置 需手动配置环境变量或硬编码路径,换环境需重配 无需配置,自动管理驱动路径,跨环境通用
跨环境 / 团队协作 需手动同步所有环境驱动,易出现不一致问题 自动适配环境,代码一次编写多环境可用
维护与错误风险 需人工维护驱动,版本 / 路径问题易导致运行失败 全自动化管理,大幅降低维护成本和错误风险

二. 开发实现

1. 本地生成截图

1.1 引入Selenium网页截图依赖

XML 复制代码
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.33.0</version>
</dependency>
<dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>6.1.0</version>
</dependency>

1.2 提供根据URL生成截图文件并返回路径的方法,用于网页截图

1.初始化驱动

java 复制代码
@Slf4j
public class WebScreenshotUtils {

    private static final WebDriver webDriver;

    static {
        final int DEFAULT_WIDTH = 1600;
        final int DEFAULT_HEIGHT = 900;
        webDriver = initChromeDriver(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }

    @PreDestroy
    public void destroy() {
        webDriver.quit();
    }

    /**
     * 初始化 Chrome 浏览器驱动
     */
    private static WebDriver initChromeDriver(int width, int height) {
        try {
            // 自动管理 ChromeDriver
            WebDriverManager.chromedriver().setup();
            // 配置 Chrome 选项
            ChromeOptions options = new ChromeOptions();
            // 无头模式(Chrome浏览器在后台运行,不会弹出窗口)
            options.addArguments("--headless");
            // 禁用GPU(在某些环境下避免问题)
            options.addArguments("--disable-gpu");
            // 禁用沙盒模式(Docker环境需要)
            options.addArguments("--no-sandbox");
            // 禁用开发者shm使用
            options.addArguments("--disable-dev-shm-usage");
            // 设置窗口大小
            options.addArguments(String.format("--window-size=%d,%d", width, height));
            // 禁用扩展
            options.addArguments("--disable-extensions");
            // 设置用户代理
            options.addArguments("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
            // 创建驱动
            WebDriver driver = new ChromeDriver(options);
            // 设置页面加载超时
            driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
            // 设置隐式等待
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
            return driver;
        } catch (Exception e) {
            log.error("初始化 Chrome 浏览器失败", e);
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "初始化 Chrome 浏览器失败");
        }
    }
}

这是样板代码。

重点:

(1)在静态代码块里初始化驱动,确保整个应用生命周期内只初始化一次

(2)默认使用已经初始化好的驱动实例

(3)在项目停止前正确销毁驱动,释放资源

  1. 编写子方法
java 复制代码
/**
 * 保存图片到文件
 */
private static void saveImage(byte[] imageBytes, String imagePath) {
    try {
        FileUtil.writeBytes(imageBytes, imagePath);
    } catch (Exception e) {
        log.error("保存图片失败: {}", imagePath, e);
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "保存图片失败");
    }
}

/**
 * 压缩图片
 */
private static void compressImage(String originalImagePath, String compressedImagePath) {
    // 压缩图片质量(0.1 = 10% 质量)清晰度还行,且减少文件大小
    final float COMPRESSION_QUALITY = 0.3f;
    try {
        ImgUtil.compress(
                FileUtil.file(originalImagePath),
                FileUtil.file(compressedImagePath),
                COMPRESSION_QUALITY
        );
    } catch (Exception e) {
        log.error("压缩图片失败: {} -> {}", originalImagePath, compressedImagePath, e);
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "压缩图片失败");
    }
}

/**
 * 等待页面加载完成
 */
private static void waitForPageLoad(WebDriver driver) {
    try {
        // 创建等待页面加载对象
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        // 等待 document.readyState 为complete
        wait.until(webDriver ->
                ((JavascriptExecutor) webDriver).executeScript("return document.readyState")
                        .equals("complete")
        );
        // 额外等待一段时间,确保动态内容加载完成
        Thread.sleep(2000);
        log.info("页面加载完成");
    } catch (Exception e) {
        log.error("等待页面加载时出现异常,继续执行截图", e);
    }
}
  1. 编写完整的截图方法,思路是访问页面->等待页面加载完成->截图->保存截图文件并压缩->返回压缩后的路径。
java 复制代码
/**
 * 生成网页截图
 *
 * @param webUrl 网页URL
 * @return 压缩后的截图文件路径,失败返回null
 */
public static String saveWebPageScreenshot(String webUrl) {
    if (StrUtil.isBlank(webUrl)) {
        log.error("网页URL不能为空");
        return null;
    }
    try {
        // 创建临时目录
        String rootPath = System.getProperty("user.dir") + File.separator + "tmp" + File.separator + "screenshots"
                + File.separator + UUID.randomUUID().toString().substring(0, 8);
        FileUtil.mkdir(rootPath);
        // 图片后缀
        final String IMAGE_SUFFIX = ".png";
        // 原始截图文件路径
        String imageSavePath = rootPath + File.separator + RandomUtil.randomNumbers(5) + IMAGE_SUFFIX;
        // 访问网页
        webDriver.get(webUrl);
        // 等待页面加载完成
        waitForPageLoad(webDriver);
        // 截图
        byte[] screenshotBytes = ((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES);
        // 保存原始图片
        saveImage(screenshotBytes, imageSavePath);
        log.info("原始截图保存成功: {}", imageSavePath);
        // 压缩图片
        final String COMPRESSION_SUFFIX = "_compressed.jpg";
        String compressedImagePath = rootPath + File.separator + RandomUtil.randomNumbers(5) + COMPRESSION_SUFFIX;
        compressImage(imageSavePath, compressedImagePath);
        log.info("压缩图片保存成功: {}", compressedImagePath);
        // 删除原始图片,只保留压缩图片
        FileUtil.del(imageSavePath);
        return compressedImagePath;
    } catch (Exception e) {
        log.error("网页截图失败: {}", webUrl, e);
        return null;
    }
}

2. 保存截图到对象存储

将生成的封面图上传到腾讯云COS对象存储,使得其能够持久化存储并快速访问。

1.1 在腾讯云控制台中创建一个存储桶:存储桶列表 - 对象存储 - 控制台

1.2 在配置文件中添加COS相关配置

java 复制代码
cos:
  client:
    host: your-custom-domain.com
    secretId: your-secret-id
    secretKey: your-secret-key
    region: ap-shanghai
    bucket: your-bucket-name

1.3 引入依赖

XML 复制代码
<dependency>
     <groupId>com.qcloud</groupId>
     <artifactId>cos_api</artifactId>
     <version>5.6.227</version>
</dependency>

1.4 创建COS客户端配置类

java 复制代码
/**
 * 腾讯云COS配置类
 */
@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {

    /**
     * 域名
     */
    private String host;

    /**
     * secretId
     */
    private String secretId;

    /**
     * 密钥(注意不要泄露)
     */
    private String secretKey;

    /**
     * 区域
     */
    private String region;

    /**
     * 桶名
     */
    private String bucket;

    @Bean
    public COSClient cosClient() {
        // 初始化用户身份信息(secretId, secretKey)
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
        // 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
        ClientConfig clientConfig = new ClientConfig(new Region(region));
        // 生成cos客户端
        return new COSClient(cred, clientConfig);
    }
}

1.5 创建可复用的CosManager类,专门负责和COS对象存储进行交互,提供文件上传功能。

java 复制代码
/**
 * COS对象存储管理器
 */
@Component
@Slf4j
public class CosManager {

    @Resource
    private CosClientConfig cosClientConfig;

    @Resource
    private COSClient cosClient;

    /**
     * 上传对象
     *
     * @param key  唯一键
     * @param file 文件
     * @return 上传结果
     */
    public PutObjectResult putObject(String key, File file) {
        PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file);
        return cosClient.putObject(putObjectRequest);
    }

    /**
     * 上传文件到 COS 并返回访问 URL
     *
     * @param key  COS对象键(完整路径)
     * @param file 要上传的文件
     * @return 文件的访问URL,失败返回null
     */
    public String uploadFile(String key, File file) {
        // 上传文件
        PutObjectResult result = putObject(key, file);
        if (result != null) {
            // 构建访问URL
            String url = String.format("%s%s", cosClientConfig.getHost(), key);
            log.info("文件上传COS成功: {} -> {}", file.getName(), url);
            return url;
        } else {
            log.error("文件上传COS失败,返回结果为空");
            return null;
        }
    }
}

3. 服务优化

在生成截图时,提供静态方法初始化了一个全局公用的WebDriver来避免重复加载。

优点是性能高,但是在并发截图的场景下,如果共用一个WebDriver,可能会导致截图错误的页面。

java 复制代码
// 危险:多线程共享同一个driver
private static final WebDriver webDriver = new ChromeDriver();

// 线程A: driver.get("page1.html") -> 截图
// 线程B: driver.get("page2.html") -> 截图  
// 结果:线程 A 可能截到 page2 的内容

优化思路:

ThreadLocal 模式,每个线程使用同一个WebDriver:

java 复制代码
private static final ThreadLocal<WebDriver> driverThreadLocal = new ThreadLocal<>();

public static WebDriver getDriver() {
    WebDriver driver = driverThreadLocal.get();
    if (driver == null) {
        driver = initChromeDriver();
        driverThreadLocal.set(driver);
    }
    return driver;
}

优点是实现简单,能够解决并发问题,缺点是当线程较多时可能会导致内存溢出。

4. 功能优化

定期清理本地临时生成的封面文件,以及在应用删除时关联删除对应的封面图,避免资源浪费。

用Spring Scheduler写一个定时任务:

java 复制代码
@Configuration
@EnableScheduling
@Slf4j
public class ScreenshotConfig {

    /**
     * 每天凌晨2点清理过期的临时截图文件
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupTempScreenshots() {
        log.info("开始定时清理过期的临时截图文件");
        try {
            WebScreenshotUtils.cleanupTempFiles();
            log.info("定时清理临时截图文件完成");
        } catch (Exception e) {
            log.error("定时清理临时截图文件失败", e);
        }
    }
}
相关推荐
渣哥2 小时前
事务嵌套场景必问:Spring 传播机制如何真正发挥作用?
java
聪明的笨猪猪2 小时前
Java SE “概念与优势”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
Java水解2 小时前
MySQL常用客户端工具详解
后端·mysql
Olaf_n2 小时前
SpringBoot自动装配
spring boot·后端·程序员
Yunfeng Peng2 小时前
2- 十大排序算法(希尔排序、计数排序、桶排序)
java·算法·排序算法
RainbowSea3 小时前
8. Spring AI tools/function-call
java·spring·ai编程
Jabes.yang3 小时前
Java面试揭秘:从Spring Boot到微服务的技术问答
spring boot·微服务·java面试·互联网大厂·技术问答
RainbowSea3 小时前
7. Spring AI 对话记忆 + 结构化输出
java·spring·ai编程