让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演示

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

相关推荐
揭老师高效办公2 天前
在Word和WPS文字的表格中快速输入连续的星期、月、日
word·wps
小雪人8285 天前
wps的excel如何转为谷歌在线表格
excel·wps
揭老师高效办公6 天前
在PowerPoint和WPS演示让蝴蝶一直跳8字舞
powerpoint·wps
LAM LAB6 天前
【WPS】WPSPPT 快速抠背景
wps
揭老师高效办公6 天前
PowerPoint和WPS演示如何在放映PPT时用鼠标划重点
powerpoint·wps
揭老师高效办公6 天前
PowerPoint和WPS演示如何循环放映PPT
powerpoint·wps
l1t18 天前
DeepSeek辅助编写的将xlsx格式文件中sheet1.xml按需分别保留或去掉标签的程序
xml·python·excel·wps·xlsx
l1t21 天前
利用DeepSeek辅助WPS电子表格ET格式分析
人工智能·python·wps·插件·duckdb
skywalk81631 个月前
Ubuntu24.04桌面版安装wps
linux·ubuntu·wps
阿幸软件杂货间1 个月前
小黑课堂计算机一级WPSOffice题库安装包1.44_Win中文_计算机一级考试_安装教程
wps