Mac+Linux服务器搭建Selenium-Chrome WebDriver服务JAVA实现

需求和方案

接到个研发需求任务,需要为网页版的类似文档的内容,能够提供后台服务自动下载成PDF的功能。原先前端是有让用户自行点击下载成为PDF的功能。当前这个功能如果后端服务也需要拥有的化,我的第一个反应是,后端是有数据的,找设计要下文档的排版,然后用个PDF的三方库,直接按照排版手搓一个PDF文件。我网上找了JAVA的PDF库,Apache PDFBOX,是个开源的JAVA库,可以用于创建、修改和解析PDF文档。

pom.xml 复制代码
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.27</version>
</dependency>

但是当我上手开始写PDF的时候,发现要实现网页版的复杂样式还是很复杂的。

我想了第二种思路,就通过数据自动化生成HTML,然后通过工具直接转化为PDF文件,我哼哧哼哧在网上找了一些三方库,尝试使用,比如下面的openhtmltopdf-pdfbox是一个开源且免费的库,用于将 HTML 转换为 PDF。它使用 Apache PDFBox 库来生成 PDF 文档。支持多种 HTML 和 CSS 特性。

pom.xml 复制代码
<dependency>
    <groupId>com.openhtmltopdf</groupId>
    <artifactId>openhtmltopdf-pdfbox</artifactId>
    <version>1.0.10</version>
</dependency>

用起来发现,他对于比较复杂的html或者CSS语法还是识别不够准确,HTML能够在浏览器上很好的展示样式,但是转成PDF就导致样式错乱,直接没法下手,抓耳挠腮。

我又在思考还有什么比较好的方法呢?我突然想到为啥前端同学可以做到样式不变的下载成为PDF文件?我就和前端同学讨论了原理和方法。原来他们是使用Canvas通过页面截图的方式来生成对应的PDF文件。

这个方案倒是启发了我,后端有没有一个工具,能够访问对应HTML的URL,然后通过在某个区域进行截图的方式,存储为图片,然后再将图片转成PDF文件呢。这样我就不需要感知HTML复杂的样式设计,直接粗暴的将浏览器上的展示样式直接生搬硬套成PDF文件,这样的好处是用户在页面上手动下载的PDF和我后端服务生成的PDF结构样式都是统一的,都是基于原有的HTML页面。

这个类似于爬虫的快照方案,把目标网页的指定区域截图抓下来。我就找到了Selenium。Selenium 是一种用于自动化 Web 浏览器的工具。它允许您控制浏览器,就像您自己在使用浏览器一样。您可以使用 Selenium 来模拟用户在浏览器中的行为,例如点击按钮、填写表单、验证文本等。Selenium 最常用于自动化 Web 测试。您可以使用 Selenium 来测试 Web 应用程序的功能、可用性和性能。Selenium 还可以用于其他目的,例如数据抓取、屏幕截图和浏览器自动化。

Selenium 的主要优点包括:

  • 跨平台支持: Selenium 可以在 Windows、macOS 和 Linux 等多种平台上运行。
  • 支持多种浏览器: Selenium 支持 Chrome、Firefox、Edge、Safari 等多种浏览器。
  • 易于使用: Selenium 提供了简单易用的 API,使您可以轻松地编写自动化脚本。
  • 开源且免费: Selenium 是一个开源且免费的工具,您可以自由地下载和使用它。

所以最后选定的方案就是:

「访问URL ==》 指定区域截图为PNG ==》将PNG转成PDF」

后面我就介绍了在我本机MAC上和Linux服务器上搭建Selenium+Chrome的方案

MAC本地解决方案

1、添加Maven依赖

pom.xml 复制代码
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>3.141.59</version>
</dependency>

虽然你能够在 mvnrepository.com/artifact/or...

上看到很多4.XX的版本,但是实际运行下来会有Class Not Found的报错

bash 复制代码
java.lang.NoClassDefFoundError: org/openqa/selenium/internal/Require

所以找了这个最稳定,下载量最大的版本进行安装。

2、需要本地安装ChromeDriver

由于我们本地都有Chrome浏览器,所以我就打算让程序访问本地的Chrome程序渲染对应的URL链接。那么这个时候就需要ChromeDriver了,他是谷歌浏览器的驱动程序,可以用于自动化测试和网页爬虫等任务。

因为ChromeDriver的版本要和Chrome浏览器的版本适配匹配,所以得先打开你本地的Chrome查看对应的版本。

我的版本是120.X , 所以需要到最新如下的地址下载最新的版本 googlechromelabs.github.io/chrome-for-...

由于我的电脑是MAC M1 Pro,需要选择对应的ARM架构

如果是比较低的版本,可以选择国内的镜像下载 :registry.npmmirror.com/binary.html... , 这个地址最高版本能够下载到114.X

然后解压下载的zip文件,将里面的chromedriver可执行文件,拖到/usr/local/bin路径下,如下

然后双击该文件,会出现报错信息:

根据提示分析,该应用违背了Mac系统的叫做GateKeeper的安全机制, 就是当打开没有签名的Mac应用时,就会出现'App can't be opened because it is from an unidentified developer'的错误。 当删除这个属性,就可以去除app的隔离性,正常运行。知道了问题根源所在,那么就在命令行下执行:

bash 复制代码
xattr -d com.apple.quarantine chromedriver

执行后,再双击chromedriver文件,如果出现如下successfully字样,则说明已经安装成功。

最后在给软件授权

bash 复制代码
sudo chmod 777   /usr/local/bin/chromedriver

这样可以使用代码来访问了

3、JAVA代码触发

java 复制代码
package com.pai.server.common.utils;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;

/**
 * WebDriverUtil
 * 网页元素快照工具类
 *
 * @version : V1.0
 * @date : 2023/12/21 2:05 PM
 */
public class WebDriverUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebDriverUtil.class);

    /**
     * 网页元素快照成PNG文件
     */
    public static WebElementStruct snapshotWebElement2Png(String url, String pngFilePath, String webElementClassName, int waitLoadSeconds) {

        // 可以切换到本地的WebDriver
        WebDriver driver = initLocalWebDriver();

        try {
            LOGGER.info("网页元素快照开始!url = " + url + ",pngFilePath = " + pngFilePath + ",webElementClassName = " + webElementClassName + ",waitLoadSeconds = " + waitLoadSeconds);

            // ChromeDrive开始加载页面
            driver.get(url);

            // 本地的WebDriver可以使用Sleep来等待页面加载完毕
            Thread.sleep(10000);

            // 获取需要截图的元素,以及对应的长宽和坐标
            WebElement element = driver.findElement(By.className(webElementClassName));

            int elementWidth = element.getSize().getWidth();
            int elementHeight = element.getSize().getHeight();
            int elementX = element.getLocation().getX();
            int elementY = element.getLocation().getY();
            WebElementStruct webElementStruct = new WebDriverUtil().new WebElementStruct();
            webElementStruct.x = elementX;
            webElementStruct.y = elementY;
            webElementStruct.width = elementWidth;
            webElementStruct.height = elementHeight;

            System.out.println("查询到的WebElementStruct = " + webElementStruct);

            // 重新设置对应的浏览器窗口大小,将元素正好在浏览器中展示出来
            int windowWidth = elementWidth;
            int windowHeight = elementHeight + elementY;

            driver.manage().window().setSize(new Dimension(windowWidth, windowHeight));

            // 快照存储
            File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
            FileUtils.copyFile(srcFile, new File(FilenameUtils.normalize(pngFilePath)));

            LOGGER.info("网页元素快照成功!url = " + url + ",pngFilePath = " + pngFilePath + ",webElementClassName = " + webElementClassName + ",waitLoadSeconds = " + waitLoadSeconds);

            return webElementStruct;
        } catch (Throwable e) {

            LOGGER.error("网页元素快照失败!url = " + url + ",pngFilePath = " + pngFilePath + ",webElementClassName = " + webElementClassName + ",waitLoadSeconds = " + waitLoadSeconds, e);
            throw new RuntimeException(e);

        } finally {
            driver.quit();
        }
    }

    /**
     * 初始化本地测试的WebDriver
     *
     * @return
     */
    private static WebDriver initLocalWebDriver() {

        LOGGER.info(">>>>>>>> 开始设置本地WebDriver <<<<<<<<");
        ChromeOptions options = new ChromeOptions();

        options.addArguments("disable-infobars");
        options.addArguments("--headless");
        options.addArguments("--dns-prefetch-disable");
        options.addArguments("--no-referrers");
        options.addArguments("--disable-gpu");
        options.addArguments("--disable-audio");
        options.addArguments("--no-sandbox");
        options.addArguments("--ignore-certificate-errors");
        options.addArguments("--allow-insecure-localhost");

        // 设置chrome的页面Load机制
        options.setPageLoadStrategy(PageLoadStrategy.EAGER);

        // 设置驱动
        // 通过系统类型,获取chrome驱动位置
        String chromeDriver = "/usr/local/bin/chromedriver";
        System.setProperty("webdriver.chrome.driver", chromeDriver);

        // 无头模式
        System.setProperty("java.awt.headless", "true");

        WebDriver driver = new ChromeDriver(options);
        LOGGER.info(">>>>>>>> 结束设置本地WebDriver <<<<<<<<");

        return driver;
    }

    /**
     * 网页元素区域
     */
    public class WebElementStruct {

        private int x;

        private int y;

        private int width;

        private int height;

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        public int getWidth() {
            return width;
        }

        public void setWidth(int width) {
            this.width = width;
        }

        public int getHeight() {
            return height;
        }

        public void setHeight(int height) {
            this.height = height;
        }
    }

    public static void main(String[] args) {
        String url = "https://app.pinvo.io/preview/invoice?t_id=IITSS98A7JpNePgNJyFDVzNdl286a3YljplC3P+2S84=&nw_k=33431d08-8347-4bb8-9111-e3a89b455ad9";
        String imagePath = "/Users/shaoshuai.shao/Desktop/html.png";

        snapshotWebElement2Png(url, imagePath, "pinvo-invoice-pdf", 10);

    }
}

Linux服务器解决方案

1、Chrome + Chromedriver部署

本地我们还有Chrome浏览器可以访问对应的URL,服务器上我们就需要安装和部署类似于浏览器的能力,最简单最直接的思考路径就是有没有现成的Docker可以部署?

顺着线索找到了selenium/standalone-chrome项目

hub.docker.com/r/selenium/...

你可以在对应的tags目录下,找到对应的你所需要的版本

hub.docker.com/r/selenium/...

Docker部署脚本命令:

bash 复制代码
docker run --name selenium-chrome -p 4444:4444 --shm-size=2g selenium/standalone-chrome:120.0-chromedriver-120.0

如上,我选择了Chrome版本为120.0,ChromeDriver也是版本为120.0的docker版本

由于我在AWS上进行部署,所以将服务器的4444端口打开即可;如果是阿里云的服务器上进行部署,貌似端口4444不可用,可以映射到别的端口。

这样服务端就轻松部署完毕。

2、调试遇到些问题

其中在调试过程中,还是遇到了一些问题,在请求服务端的时候,会报如下的错误:

bash 复制代码
[1556179366.141][SEVERE]: bind() failed: Cannot assign requested address (99)

后来查询问题的原因是需要在JAVA代码中,Options里面新增如下参数。

JAVA 复制代码
options.addArguments('--disable-dev-shm-usage')

--disable-dev-shm-usage 参数用于禁用 Chrome 中的 DevTools 共享内存。这可以减少 Chrome 使用的内存量,从而提高性能。

DevTools 共享内存是一个特殊的内存区域,用于在 Chrome DevTools 和 Chrome 浏览器之间共享数据。它通常用于存储快照、日志和其他调试数据。

在某些情况下,DevTools 共享内存可能会导致 Chrome 使用过多的内存。例如,如果您在 Chrome 中打开了很多标签页,或者您正在使用内存密集型的 DevTools 功能,那么 DevTools 共享内存可能会增长到非常大。

禁用 DevTools 共享内存可以防止 Chrome 使用过多的内存。这对于内存有限的计算机来说非常有用。

3、JAVA代码

对于代码编写,可以查看官网的解释

www.selenium.dev/documentati...

java 复制代码
package com.pai.server.common.utils;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.openqa.selenium.*;

import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
 * WebDriverUtil
 * 网页元素快照工具类
 *
 * @version : V1.0
 * @date : 2023/12/21 2:05 PM
 */
public class WebDriverUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebDriverUtil.class);

    /**
     * 网页元素快照成PNG文件
     */
    public static WebElementStruct snapshotWebElement2Png(String url, String pngFilePath, String webElementClassName, int waitLoadSeconds) {

        // 使用远程的WebDriver
        WebDriver driver = initRemoteWebDriver();

        try {
            LOGGER.info("网页元素快照开始!url = " + url + ",pngFilePath = " + pngFilePath + ",webElementClassName = " + webElementClassName + ",waitLoadSeconds = " + waitLoadSeconds);

            // ChromeDrive开始加载页面
            driver.get(url);

            // 等待页面加载完毕
            WebDriverWait wait = new WebDriverWait(driver, waitLoadSeconds);
            wait.until(ExpectedConditions.presenceOfElementLocated(By.className(webElementClassName)));

            // 获取需要截图的元素,以及对应的长宽和坐标
            WebElement element = driver.findElement(By.className(webElementClassName));

            int elementWidth = element.getSize().getWidth();
            int elementHeight = element.getSize().getHeight();
            int elementX = element.getLocation().getX();
            int elementY = element.getLocation().getY();
            WebElementStruct webElementStruct = new WebDriverUtil().new WebElementStruct();
            webElementStruct.x = elementX;
            webElementStruct.y = elementY;
            webElementStruct.width = elementWidth;
            webElementStruct.height = elementHeight;

            System.out.println("查询到的WebElementStruct = " + webElementStruct);

            // 重新设置对应的浏览器窗口大小,将元素正好在浏览器中展示出来
            int windowWidth = elementWidth;
            int windowHeight = elementHeight + elementY;

            driver.manage().window().setSize(new Dimension(windowWidth, windowHeight));

            // 快照存储
            File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
            FileUtils.copyFile(srcFile, new File(FilenameUtils.normalize(pngFilePath)));

            LOGGER.info("网页元素快照成功!url = " + url + ",pngFilePath = " + pngFilePath + ",webElementClassName = " + webElementClassName + ",waitLoadSeconds = " + waitLoadSeconds);

            return webElementStruct;
        } catch (Throwable e) {

            LOGGER.error("网页元素快照失败!url = " + url + ",pngFilePath = " + pngFilePath + ",webElementClassName = " + webElementClassName + ",waitLoadSeconds = " + waitLoadSeconds, e);
            throw new RuntimeException(e);

        } finally {
            driver.quit();
        }
    }


    private static WebDriver initRemoteWebDriver() {

        LOGGER.info(">>>>>>>> 开始设置RemoteWebDriver <<<<<<<<");

        try {
            ChromeOptions options = new ChromeOptions();

            options.addArguments("disable-infobars");
            options.addArguments("--headless");
            options.addArguments("--dns-prefetch-disable");
            options.addArguments("--no-referrers");
            options.addArguments("--disable-gpu");
            options.addArguments("--disable-audio");
            options.addArguments("--no-sandbox");
            options.addArguments("--ignore-certificate-errors");
            options.addArguments("--allow-insecure-localhost");
            options.addArguments("--disable-dev-shm-usage");

            Map<String, Integer> timeouts = new HashMap<>();
            timeouts.put("implicit", 10000);
            options.setCapability("timeouts", timeouts);

            // 设置chrome的页面Load机制
            options.setPageLoadStrategy(PageLoadStrategy.EAGER);

            WebDriver driver = new RemoteWebDriver(new URL("IP:4444"), options);

            LOGGER.info(">>>>>>>> 完成设置RemoteWebDriver <<<<<<<<");

            return driver;
        } catch (Throwable e) {
            LOGGER.error("初始化远程WebDriver失败", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 网页元素区域
     */
    public class WebElementStruct {

        private int x;

        private int y;

        private int width;

        private int height;

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        public int getWidth() {
            return width;
        }

        public void setWidth(int width) {
            this.width = width;
        }

        public int getHeight() {
            return height;
        }

        public void setHeight(int height) {
            this.height = height;
        }
    }

    public static void main(String[] args) {
        String url = "https://app.pinvo.io/preview/invoice?t_id=IITSS98A7JpNePgNJyFDVzNdl286a3YljplC3P+2S84=&nw_k=33431d08-8347-4bb8-9111-e3a89b455ad9";
        String imagePath = "/Users/shaoshuai.shao/Desktop/html.png";

        snapshotWebElement2Png(url, imagePath, "pinvo-invoice-pdf", 10);
    }
}

引用

Mac系统安装chromedriver遇到的问题和解决办法_the version of chrome cannot be detected. trying w-CSDN博客

ChromeDriver谷歌驱动最新版安装118/119/120-CSDN博客

基于Java+selenium+Chrome,实现截取html页面内容并保存为图片 - _天青色烟雨 - 博客园

Java+Selenium根据元素创建指定区域截图------Element快照 - 明月, - 博客园

GitHub - SeleniumHQ/docker-selenium: Provides a simple way to run Selenium Grid with Chrome, Firefox, and Edge using Docker, making it easier to perform browser automation

How to fix "[SEVERE]: bind() failed: Cannot assign requested address (99)" while starting chromedriver

相关推荐
2401_857610035 分钟前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
少说多做3438 分钟前
Android 不同情况下使用 runOnUiThread
android·java
知兀9 分钟前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑202031 分钟前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深32 分钟前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
凌冰_34 分钟前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞42 分钟前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货44 分钟前
Rust 的简介
开发语言·后端·rust
shuangrenlong44 分钟前
slice介绍slice查看器
java·ubuntu