让AI帮我用java实现EasyExel读取图片—支持WPS嵌入图片

🌈 场景概述

java 小伙伴相信都使用 EasyExcel 以及 POI 库实现过 Excel 批量导入、导出功能,但只有部分人实现过 excel 导入带图片数据的场景。这个技术实现手段网上也有很多案例和demo,最常见的就是通过 XSSFPictureData 来实现。但是在 WPS 单元格嵌入图片场景下,本法无效。

本文讲解:如何利用 AI 在5分钟内实现用 Java EasyExcel 针对 WPS Excel 单元格嵌入图片 的读取。

🎯 本节你将学到:

  • 什么?凡是用过电脑的人99%都会数据库?
  • Excel 真的是你平时见到的那个样子么?
  • 用 AI 5分钟搞定Java EasyExcel 针对 WPS Excel 单元格嵌入图片 的读取。

👇 视频版教程

(喜欢看视频教程就看视频,喜欢看图文教程就继续往下滑)

让AI帮我用java实现EasyExel读取图片(支持WPS嵌入图片),AI 5分钟搞定普通程序员5天工作量

👉🏻 笔记原文👉🏻 让AI帮我用java实现EasyExel读取图片---支持WPS嵌入图片 · 语雀

1、常规Excel读取图片的问题

最常见的通过 java + easyexcel 实现excel图片读取的方法,基本都是通过Apache POIXSS*方法实现:

import org.apache.poi.xssf.usermodel.XSSFPictureData;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

// 假设你已经有了一个MultipartFile类型的Excel文件
XSSFWorkbook workbook = new XSSFWorkbook(file.getInputStream());
Sheet sheet = workbook.getSheetAt(0);
Map<String, XSSFPictureData> pictures = new HashMap<>();

for (XSSFPictureData picture : workbook.getAllPictures()) {
    pictures.put(picture.getPackagePart().getPartName().getName(), picture);
}

// 遍历sheet中的所有形状,找到图片并处理
for (POIXMLDocumentPart dr : sheet.getRelations()) {
    if (dr instanceof XSSFDrawing) {
        XSSFDrawing drawing = (XSSFDrawing) dr;
        for (XSSFShape shape : drawing.getShapes()) {
            if (shape instanceof XSSFPicture) {
                XSSFPicture picture = (XSSFPicture) shape;
                XSSFPictureData picData = pictures.get(picture.getPackagePart().getPartName().getName());
                // 处理图片数据,例如保存到服务器或数据库
            }
        }
    }
}

但是,本法对于 WPS 内嵌图片无效,因为 WPS 内嵌图片使用了 DISPIMG函数,我们用上面的方法解析后,得到的只是函数信息,并非图片信息。如下图的 =DISPIMG("ID_79A9B2935BEA4B1B8836ECE25C09D573",1)

2、揭开 Excel 的神秘面纱

🧑‍🎓 你以为的Excel 🆚 真实的Excel

你以为的Excel

真实的Excel

📚 这里要讲一个知识,就是 Excel 可以被理解为一种简单的数据库

因为它具有存储和组织数据的能力,并且可以通过公式、查询和宏等功能来处理数据。以下是一些将 Excel 视为数据库的理由:

  1. 数据存储:Excel 文件(.xlsx 或 .xls)可以存储大量的数据,类似于数据库中的表。
  2. 表格结构:Excel 中的工作表类似于数据库中的表,它们都有行和列的结构。
  3. 数据操作:Excel 提供了排序、筛选和查找等基本的数据操作功能,这些也是数据库管理系统(DBMS)中常见的操作。
  4. 数据查询:Excel 允许使用公式和函数(如 VLOOKUP、HLOOKUP、INDEX 和 MATCH)来查询和分析数据。
  5. 数据验证:Excel 提供数据验证功能,可以限制输入的数据类型,类似于数据库中的数据完整性约束。
  6. 宏和自动化:Excel 的宏功能可以用来自动化重复性的数据操作任务,类似于数据库中的存储过程和触发器。

然而,尽管 Excel 具有这些数据库的特性,它也有一些限制,使其不适合作为大型或复杂的数据库解决方案:

  1. 性能问题:对于大型数据集,Excel 的性能可能会下降,因为它不是为处理大规模数据而设计的。
  2. 数据安全和权限管理:Excel 在数据安全和权限管理方面不如专业的数据库管理系统强大。
  3. 数据一致性和完整性:Excel 缺乏数据库管理系统中的数据一致性和完整性约束。
  4. 多用户访问:Excel 文件通常不适合多用户同时访问和编辑,而数据库管理系统支持多用户并发访问。
  5. 扩展性和可伸缩性:随着数据量的增长,Excel 的扩展性和可伸缩性不如专业的数据库系统。

因此,虽然 Excel 可以被视为一种数据库,但它更适合于小型、简单的数据管理和分析任务。对于需要高性能、高安全性、复杂查询和大规模数据处理的场景,专业的数据库管理系统(如 MySQL、PostgreSQL、Oracle 等)会是更合适的选择。

前面我们截图中的"真实的Excel"是怎么回事?

其实很简单:

我们将 WPS 创建的.xlsx文件后缀名改为.zip后解压缩文件,得到的就是这个效果。

3、EasyExcel 读取WPS内嵌图片如何解决

3.1 先来找到图片位置以及绑定方式

根据前面的分析,我们可以看到 wps 的内嵌图片是通过DISPIMG函数加载的,同时我们通过 Excel 神秘面纱中看到了其"Excel 数据的底层文件构造",接下来我们找到这个函数与图片之间的逻辑关系,然后通过代码找到这个图片就行了。

  • 文件:/xl/cellimages.xml
  • 文件:/xl/_rels/cellimages.xml.rels
  • 最后找到图片

3.2 让AI帮助我们用java实现

  • 首先打开领航AGI聚合平台领航AGI
  • 点击 playground 进入 AI 工具
  • 输入提示要求(第一次没有提到wps问题,所以生成的代码仍存在问题,这里不做演示,直接下一步)
  • 告诉他wps内嵌图片问题,并把文件xml给AI

AI完整对话内容(点击查看全部)

3.3 看最终代码

这里面我后来把文件名称调整了下,方便后期大家检索

WPSExcelImportImgListener.java

package com.sinosoft.hanlin.jyy.handler;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.sinosoft.hanlin.jyy.core.vo.common.WPSExcelImportImgDemoVo;
import lombok.Getter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 微信 LHYYH0001
 * @description: Excel带图片导入
 * 处理图片与数据行的关联。由于图片的对应顺序已在 ExcelImageExtractor 中提供,这里可以通过行号进行匹配。
 * @create 2024-11-05 09:07
 **/
@Getter
public class WPSExcelImportImgListener extends AnalysisEventListener<WPSExcelImportImgDemoVo> {

    private List<WPSExcelImportImgDemoVo> dataList = new ArrayList<>();

    @Override
    public void invoke(WPSExcelImportImgDemoVo data, AnalysisContext context) {
        // 处理每一行的数据
        dataList.add(data);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 所有数据解析完成后执行
    }
}

WPSExcelImportImgDemoVo.java

package com.sinosoft.hanlin.jyy.core.vo.common;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class WPSExcelImportImgDemoVo {

    @ExcelProperty("标题")
    private String title;

//    // 将图片字段设置为 List<ImageData> 类型,用于接收多张图片
//    @ExcelProperty(value = "图片")
//    private List<ImageData> picture;

    @ExcelProperty(value = "图片")
    private String picture;
}

WPSExcelImportImgExtractor.java

package com.sinosoft.hanlin.jyy.handler;

import org.apache.commons.io.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class WPSExcelImportImgExtractor {

    /**
     * 提取 Excel 中的图片,并返回单元格与图片路径的映射
     * <p>
     * WPS 的内置函数无法提取图片,需要使用第三方库解析XML文件。
     * WPS 内嵌图片会把图片做DISPIMG函数处理,如=DISPIMG("ID_9D6E8C240C8945178DFF238232B217BF",1)
     * 我们可以将.xlsx 文件后缀改成.zip后解压,即可看到
     * 在xl路径下的cellimages.xml文件中,可以看到函数中的id值
     * 并且在cellimages.xml.rels中可以看到函数与图片之间的关系,而图片就位于xl/media路径下
     *
     * @param inputStream Excel 文件的输入流
     * @param outputDir   图片保存的目标目录
     * @return 单元格位置(如 A1)与图片路径的映射
     * @throws Exception 异常
     */
    public Map<String, String> extractImages(InputStream inputStream, String outputDir) throws Exception {
        Map<String, String> cellImageMap = new HashMap<>();
        Map<String, String> relsMap = new HashMap<>();
        Map<String, byte[]> imagesData = new HashMap<>();

        // 创建目标目录
        File dir = new File(outputDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        ZipInputStream zis = new ZipInputStream(inputStream);
        ZipEntry zipEntry;
        ByteArrayOutputStream baos = null;
        String sheetXml = null;
        String cellImagesXml = null;
        String cellImagesRelsXml = null;

        // 首先遍历所有的Zip条目,找到需要的XML和图片文件
        while ((zipEntry = zis.getNextEntry()) != null) {
            String entryName = zipEntry.getName();
            if ("xl/cellimages.xml".equals(entryName)) {
                baos = new ByteArrayOutputStream();
                IOUtils.copy(zis, baos);
                cellImagesXml = baos.toString("UTF-8");
                baos.close();
            } else if ("xl/_rels/cellimages.xml.rels".equals(entryName)) {
                baos = new ByteArrayOutputStream();
                IOUtils.copy(zis, baos);
                cellImagesRelsXml = baos.toString("UTF-8");
                baos.close();
            } else if (entryName.startsWith("xl/media/")) {
                byte[] imageBytes = IOUtils.toByteArray(zis);
                String imageName = entryName.substring("xl/media/".length());
                imagesData.put(imageName, imageBytes);
            }
            zis.closeEntry();
        }
        zis.close();

        // 解析cellimages.xml.rels,建立rId到图片文件名的映射
        if (cellImagesRelsXml != null) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document relsDoc = builder.parse(new ByteArrayInputStream(cellImagesRelsXml.getBytes("UTF-8")));
            NodeList relNodes = relsDoc.getElementsByTagName("Relationship");
            for (int i = 0; i < relNodes.getLength(); i++) {
                Element relElement = (Element) relNodes.item(i);
                String rId = relElement.getAttribute("Id");
                // e.g., "media/image1.png"
                String target = relElement.getAttribute("Target");
                relsMap.put(rId, target.substring("media/".length()));
            }
        }

        // 解析cellimages.xml,提取图片与单元格的关系
        if (cellImagesXml != null) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document cellImagesDoc = builder.parse(new ByteArrayInputStream(cellImagesXml.getBytes("UTF-8")));
            NodeList cellImageNodes = cellImagesDoc.getElementsByTagName("etc:cellImage");
            for (int i = 0; i < cellImageNodes.getLength(); i++) {
                Element cellImageElement = (Element) cellImageNodes.item(i);
                // 获取图片 name 属性,如 "ID_6C483737A6AC427DAA4E4974252FB8A8"
                Element picElement = (Element) cellImageElement.getElementsByTagName("xdr:pic").item(0);
                Element cNvPr = (Element) picElement.getElementsByTagName("xdr:cNvPr").item(0);
                // e.g., "ID_6C483737A6AC427DAA4E4974252FB8A8"
                String imageName = cNvPr.getAttribute("name");
                // 获取 r:embed 属性,如 "rId1"
                Element blipFill = (Element) picElement.getElementsByTagName("xdr:blipFill").item(0);
                Element blip = (Element) blipFill.getElementsByTagName("a:blip").item(0);
                // e.g., "rId1"
                String rId = blip.getAttribute("r:embed");
                // e.g., "image1.png"
                String imageFileName = relsMap.get(rId);

                // TODO: 根据需要确定图片对应的单元格位置
                // 由于cellimages.xml中没有直接包含单元格位置的信息,这里需要通过其他途径获取
                // 例如,可以通过图片的位置信息(如x, y坐标)与单元格的位置对应
                // 但是这需要解析更多的XML信息,这里假设图片对应的顺序与数据行对应

                // 保存图片到本地
                byte[] imageBytes = imagesData.get(imageFileName);
                if (imageBytes != null) {
                    String savedImagePath = outputDir + File.separator + imageFileName;
                    FileOutputStream fos = new FileOutputStream(savedImagePath);
                    fos.write(imageBytes);
                    fos.close();

                    // 由于缺少单元格位置信息,这里需要自定义逻辑进行映射
                    // 例如,可以将图片顺序与数据行顺序对应
                    // 这里将图片名存储到一个列表中,后续与数据行进行关联
                    cellImageMap.put(imageName, savedImagePath);
                }
            }
        }

        return cellImageMap;
    }
}

ImportImgDemoApi.java

// ImportImgDemoApi.java
package com.sinosoft.hanlin.jyy.core.api;

import com.alibaba.excel.EasyExcel;
import com.sinosoft.hanlin.jyy.annotation.NoAuth;
import com.sinosoft.hanlin.jyy.core.vo.common.WPSExcelImportImgDemoVo;
import com.sinosoft.hanlin.jyy.handler.WPSExcelImportImgExtractor;
import com.sinosoft.hanlin.jyy.handler.WPSExcelImportImgListener;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
 * 数据+图片导入
 */
@Slf4j
@RestController
@RequestMapping("/easy")
public class ImportImgDemoApi {

    /**
     * 从输入流中读取 Excel 文件并解析数据,包括图片
     * @author 微信 LHYYH0001
     *
     * @param filePath Excel 文件
     * @return 包含解析后数据的列表
     */
    @ApiOperation(value = "Excel导入带图片demo", notes = "Excel导入带图片demo", produces = MediaType.APPLICATION_JSON_VALUE)
    @PostMapping(value = "/import", produces = MediaType.APPLICATION_JSON_VALUE)
    @NoAuth
    public List<WPSExcelImportImgDemoVo> importExcel(@RequestParam("filePath") MultipartFile filePath, HttpServletResponse httpServletResponse) throws Exception {
        // 创建监听器
        WPSExcelImportImgListener listener = new WPSExcelImportImgListener();

        // 获取文件输入流
        InputStream inputStreamForData = filePath.getInputStream();

        // 使用 EasyExcel 读取数据
        EasyExcel.read(inputStreamForData, WPSExcelImportImgDemoVo.class, listener).sheet().doRead();
        List<WPSExcelImportImgDemoVo> dataList = listener.getDataList();

        // 重置输入流以供图片提取
        InputStream inputStreamForImages = filePath.getInputStream();

        // 提取图片并保存
        WPSExcelImportImgExtractor extractor = new WPSExcelImportImgExtractor();
        // 修改为您希望保存图片的目录
        String imageOutputDir = "/Users/javastarboy/Desktop/京东自营/";
        Map<String, String> cellImageMap = extractor.extractImages(inputStreamForImages, imageOutputDir);

        // 遍历数据并关联图片
        // 假设图片的顺序与数据行的顺序一致
        int imageIndex = 0;
        for (int i = 0; i < dataList.size(); i++) {
            WPSExcelImportImgDemoVo vo = dataList.get(i);
            // 获取对应的图片
            String imagePath = null;
            if (i < cellImageMap.size()) {
                imagePath = (String) cellImageMap.values().toArray()[i];
                // 将图片路径设置到实体类
                vo.setPicture(imagePath);
                log.info("图片 {} 已关联到标题 {}", imagePath, vo.getTitle());
            }
        }

        // 关闭输入流
        inputStreamForImages.close();

        // 返回数据列表
        return dataList;
    }
}

验证一下

请求后,excel的图片会存储在 imageOutputDir****路径下。

存储逻辑可以根据你的业务逻辑做对应调整,上面 imageOutputDir****只是demo演示

分享不易,点赞、关注,支持下哦~

相关推荐
spjhandsomeman3 小时前
EXCEL 或 WPS 列下划线转驼峰
excel·wps
哑巴湖小水怪3 小时前
WPS宏编辑器开发,单元格内容变更自动触发事件
java·编辑器·wps
LKID体2 天前
win32com库基于wps对Word文档的基础操作
c#·word·wps
有过~3 天前
WPS Office手机去广高级版
pdf·wps
Morris_4 天前
WPS文档中的“等线”如何删除
wps·等线
默默提升实验室4 天前
WPS 默认模板修改
wps
瓦风5 天前
【Excel&WPS如何对工作表和文档进行加密保护】
excel·wps
102112345678905 天前
怎么对 PDF 添加权限密码或者修改密码-免费软件分享
人工智能·网络安全·adobe·pdf·密码学·wps·福昕阅读器
鹏大师运维6 天前
【功能介绍】信创终端系统上各WPS版本的授权差异
linux·wps·授权·麒麟·国产操作系统·1024程序员节·统信uos