🌈 场景概述
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 POI
库XSS*
方法实现:
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 视为数据库的理由:
- 数据存储:Excel 文件(.xlsx 或 .xls)可以存储大量的数据,类似于数据库中的表。
- 表格结构:Excel 中的工作表类似于数据库中的表,它们都有行和列的结构。
- 数据操作:Excel 提供了排序、筛选和查找等基本的数据操作功能,这些也是数据库管理系统(DBMS)中常见的操作。
- 数据查询:Excel 允许使用公式和函数(如 VLOOKUP、HLOOKUP、INDEX 和 MATCH)来查询和分析数据。
- 数据验证:Excel 提供数据验证功能,可以限制输入的数据类型,类似于数据库中的数据完整性约束。
- 宏和自动化:Excel 的宏功能可以用来自动化重复性的数据操作任务,类似于数据库中的存储过程和触发器。
然而,尽管 Excel 具有这些数据库的特性,它也有一些限制,使其不适合作为大型或复杂的数据库解决方案:
- 性能问题:对于大型数据集,Excel 的性能可能会下降,因为它不是为处理大规模数据而设计的。
- 数据安全和权限管理:Excel 在数据安全和权限管理方面不如专业的数据库管理系统强大。
- 数据一致性和完整性:Excel 缺乏数据库管理系统中的数据一致性和完整性约束。
- 多用户访问:Excel 文件通常不适合多用户同时访问和编辑,而数据库管理系统支持多用户并发访问。
- 扩展性和可伸缩性:随着数据量的增长,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实现
- 告诉他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演示