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

相关推荐
绝无仅有23 分钟前
企微审批对接错误与解决方案
后端·算法·架构
hweiyu0037 分钟前
Maven 私库
java·maven
bigFish啦啦啦44 分钟前
docker proxy
docker
Super Rookie1 小时前
Spring Boot 企业项目技术选型
java·spring boot·后端
来自宇宙的曹先生1 小时前
用 Spring Boot + Redis 实现哔哩哔哩弹幕系统(上篇博客改进版)
spring boot·redis·后端
写不出来就跑路1 小时前
Spring Security架构与实战全解析
java·spring·架构
expect7g1 小时前
Flink-Checkpoint-1.源码流程
后端·flink
00后程序员1 小时前
Fiddler中文版如何提升API调试效率:本地化优势与开发者实战体验汇总
后端
ZeroNews内网穿透1 小时前
服装零售企业跨区域运营难题破解方案
java·大数据·运维·服务器·数据库·tcp/ip·零售
果子⌂2 小时前
容器技术入门之Docker环境部署
linux·运维·docker