恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java读取WPS Excel嵌入式图片:POI增强方案与实战避坑指南
首页
资讯中心
/
Java读取WPS Excel嵌入式图片:POI增强方案与实战避坑指南
Java读取WPS Excel嵌入式图片:POI增强方案与实战避坑指南
发布时间:2026/8/2 4:10:04
1. 项目概述当Java遇上WPS Excel的“图文并茂”处理Excel文件是Java后端开发中再常见不过的需求从简单的数据导入导出到复杂的报表生成Apache POI库几乎是每个Java开发者的标配工具。然而当需求从“读取数据”升级到“读取数据并提取其中的图片”时尤其是面对国内办公软件WPS Office生成的、包含嵌入式图片的.xlsx文件时很多开发者会发现事情并没有想象中那么简单。你可能会遇到图片读取为null、图片位置错乱、甚至直接抛出异常的情况。这背后是WPS与Microsoft Office在实现OOMLOffice Open XML标准时的细微差异以及POI库默认处理逻辑的局限性。这个项目的核心就是要解决这个痛点用Java稳定、准确地读取WPS Excel文件中嵌入的图片并建立图片与单元格的映射关系。无论是用于内容审核、数据归档、还是生成带图的HTML预览这个能力都至关重要。如果你正在为“POI读WPS的图读不出来”而头疼或者你的系统需要处理来自不同办公软件WPS/Office的混合文件源那么接下来的内容将为你提供一套经过实战检验的完整解决方案。2. 核心挑战与方案选型为什么不是简单的workbook.getAllPictures()在深入代码之前我们必须先理解问题的根源。Apache POI的XSSFWorkbook类提供了getAllPictures()方法听起来像是万能钥匙但在处理WPS文件时它常常失灵。2.1 WPS与MS Office的OOML差异解析Excel的.xlsx文件本质上是一个ZIP压缩包里面包含了XML文件和各种资源如图片。图片信息主要存储在两部分/xl/drawings/drawingX.xml定义了图片在表格中的位置、锚点锚定到哪个单元格、尺寸和变形信息。/xl/media/目录存储图片的二进制实体文件如image1.png。关键差异在于绘图关系文件的引用路径。MS Office生成的Excel其drawingX.xml中引用图片的关系r:embed通常指向../media/image1.png。而WPS为了兼容性或自身实现有时会使用不同的关系ID映射或者其生成的XML结构在命名空间、元素属性上与标准存在细微差别。POI默认的解析器是按照MS Office的“习惯”来写的当遇到WPS的这些“方言”时就可能无法正确建立图片资源路径的关联导致getAllPictures()返回的列表里找不到对应的图片数据或者找到的图片对象其Data部分为空。2.2 方案对比直接解析ZIP vs 增强POI面对这个问题通常有两种思路“硬核”底层解析完全绕过POI使用java.util.zip包直接解压.xlsx文件然后自己解析XML找到drawingX.xml和/xl/media/下的文件。这种方法控制力最强绝对可控但实现复杂需要精细处理XML解析、关系映射和坐标计算将twoCellAnchor等坐标转换为具体的行号列号开发成本高且容易因WPS版本更新而需要调整。“增强”POI解析依然以POI作为主要框架因为它对单元格数据、公式、样式的解析已经非常成熟。我们只针对其图片提取的薄弱环节进行“补丁式”增强。核心思路是在POI解析工作表的同时我们直接深入到ZIP包中对应的drawings文件夹手动解析drawingX.xml文件建立我们自己的图片位置与二进制数据的映射关系。显然第二种方案更具性价比。我们不必重复造轮子去解析整个Excel而是聚焦于POI未能完美处理的那一小部分。本项目将采用这种“增强型POI”方案。注意这里讨论的是WPS保存的标准.xlsx格式文件而非WPS特有的.et格式。.et格式是WPS的私有格式POI无法直接读取通常需要先通过WPS软件或特定转换工具将其转为.xlsx。3. 实战构建支持WPS嵌入式图片的Java读取器接下来我们将一步步构建这个读取器。我们将创建一个名为WpsAwareExcelReader的类它不仅能读取单元格数据还能返回一个ListCellImageInfo其中包含图片数据、格式以及其所属的单元格位置。3.1 环境准备与依赖首先确保你的项目引入了Apache POI的依赖。如果你使用Maven在pom.xml中添加dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.3/version !-- 建议使用较新版本 -- /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-compress/artifactId version1.23.0/version !-- 用于处理ZIP -- /dependency高版本的POI如5.x通常包含了对commons-compress的传递依赖但显式声明可以避免版本冲突。3.2 核心模型定义CellImageInfo我们需要一个数据结构来承载图片的所有信息。import lombok.Data; // 推荐使用Lombok简化代码 import java.awt.Dimension; import java.io.InputStream; Data public class CellImageInfo { /** 图片二进制数据 */ private byte[] imageData; /** 图片格式如 png, jpeg, gif */ private String format; /** 图片建议的文件后缀如 .png */ private String suggestedFileSuffix; /** 图片所锚定的起始单元格行号 (0-based) */ private int anchorFromRow; /** 图片所锚定的起始单元格列号 (0-based) */ private int anchorFromCol; /** 图片所锚定的结束单元格行号 (0-based)可能相同 */ private int anchorToRow; /** 图片所锚定的结束单元格列号 (0-based)可能相同 */ private int anchorToCol; /** 图片在Excel中的内部关系ID如 rId1 */ private String relationshipId; /** 图片在/media/中的文件名如 image1.png */ private String imageFileName; }3.3 主流程实现WpsAwareExcelReader这个类是核心它封装了读取逻辑。import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFDrawing; import org.apache.poi.xssf.usermodel.XSSFShape; import org.apache.poi.xssf.usermodel.XSSFPicture; import org.apache.poi.xssf.usermodel.XSSFClientAnchor; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; 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.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class WpsAwareExcelReader { /** * 读取Excel文件返回工作表数据及图片信息 * param excelFile Excel文件 * return Map: key为工作表索引value为该表的数据和图片信息 */ public MapInteger, SheetDataAndImages readExcel(File excelFile) throws Exception { MapInteger, SheetDataAndImages result new HashMap(); // 1. 使用POI正常打开工作簿读取单元格数据 try (OPCPackage pkg OPCPackage.open(excelFile); Workbook workbook new XSSFWorkbook(pkg)) { // 2. 同时将Excel文件作为ZIP包打开准备手动解析绘图文件 MapString, byte[] drawingXmlMap extractDrawingXmls(excelFile); MapString, byte[] mediaFileMap extractMediaFiles(excelFile); int numberOfSheets workbook.getNumberOfSheets(); for (int sheetIndex 0; sheetIndex numberOfSheets; sheetIndex) { Sheet sheet workbook.getSheetAt(sheetIndex); SheetDataAndImages sheetResult new SheetDataAndImages(); sheetResult.setSheetName(sheet.getSheetName()); // 3. 读取单元格数据标准POI操作 readSheetData(sheet, sheetResult); // 4. 关键步骤增强读取图片信息 // 先尝试POI标准方法对MS Office文件有效 ListCellImageInfo poiImages extractImagesByPoiStandard((XSSFWorkbook) workbook, sheetIndex); sheetResult.getImages().addAll(poiImages); // 再尝试手动解析Drawing XML针对WPS的补充 String drawingXmlKey findDrawingXmlKeyForSheet(sheetIndex, drawingXmlMap.keySet()); if (drawingXmlKey ! null mediaFileMap ! null) { ListCellImageInfo manualImages parseDrawingXmlManually( drawingXmlMap.get(drawingXmlKey), mediaFileMap, sheetIndex ); // 去重避免POI已读取的和手动解析的重复 manualImages.removeIf(manualImg - poiImages.stream().anyMatch(poiImg - Objects.equals(poiImg.getRelationshipId(), manualImg.getRelationshipId()) ) ); sheetResult.getImages().addAll(manualImages); } result.put(sheetIndex, sheetResult); } } return result; } // --- 以下为关键子方法实现 --- /** * 从Excel ZIP包中提取所有 /xl/drawings/drawing*.xml 文件 */ private MapString, byte[] extractDrawingXmls(File excelFile) throws IOException { MapString, byte[] drawingMap new HashMap(); Pattern drawingPattern Pattern.compile(xl/drawings/drawing\\d\\.xml); try (ZipArchiveInputStream zis new ZipArchiveInputStream(new FileInputStream(excelFile))) { ZipArchiveEntry entry; while ((entry zis.getNextZipEntry()) ! null) { String entryName entry.getName(); Matcher matcher drawingPattern.matcher(entryName); if (matcher.matches()) { ByteArrayOutputStream baos new ByteArrayOutputStream(); byte[] buffer new byte[1024]; int len; while ((len zis.read(buffer)) -1) { baos.write(buffer, 0, len); } drawingMap.put(entryName, baos.toByteArray()); } } } return drawingMap; } /** * 从Excel ZIP包中提取所有 /xl/media/ 下的文件 */ private MapString, byte[] extractMediaFiles(File excelFile) throws IOException { MapString, byte[] mediaMap new HashMap(); Pattern mediaPattern Pattern.compile(xl/media/.); try (ZipArchiveInputStream zis new ZipArchiveInputStream(new FileInputStream(excelFile))) { ZipArchiveEntry entry; while ((entry zis.getNextZipEntry()) ! null) { String entryName entry.getName(); Matcher matcher mediaPattern.matcher(entryName); if (matcher.matches()) { ByteArrayOutputStream baos new ByteArrayOutputStream(); byte[] buffer new byte[1024]; int len; while ((len zis.read(buffer)) -1) { baos.write(buffer, 0, len); } // 以文件名如image1.png为key存储 String fileName entryName.substring(entryName.lastIndexOf(/) 1); mediaMap.put(fileName, baos.toByteArray()); } } } return mediaMap; } /** * 尝试使用POI标准API提取图片对MS Office文件效果好 */ private ListCellImageInfo extractImagesByPoiStandard(XSSFWorkbook workbook, int sheetIndex) { ListCellImageInfo images new ArrayList(); XSSFDrawing drawing workbook.getSheetAt(sheetIndex).getDrawingPatriarch(); if (drawing null) { return images; } for (XSSFShape shape : drawing.getShapes()) { if (shape instanceof XSSFPicture) { XSSFPicture pic (XSSFPicture) shape; XSSFClientAnchor anchor (XSSFClientAnchor) pic.getAnchor(); // 注意有些图片可能没有锚点信息需要判断 if (anchor ! null) { CellImageInfo info new CellImageInfo(); info.setImageData(pic.getPictureData().getData()); info.setFormat(pic.getPictureData().getMimeType()); info.setSuggestedFileSuffix(pic.getPictureData().suggestFileExtension()); info.setAnchorFromRow(anchor.getRow1()); info.setAnchorFromCol(anchor.getCol1()); info.setAnchorToRow(anchor.getRow2()); info.setAnchorToCol(anchor.getCol2()); // POI内部关系ID不易直接获取此处可留空或尝试反射 images.add(info); } } } return images; } /** * 手动解析Drawing XML文件核心增强逻辑 * param drawingXmlBytes drawingX.xml的文件字节 * param mediaFileMap 媒体文件映射表 * param sheetIndex 工作表索引用于日志 */ private ListCellImageInfo parseDrawingXmlManually(byte[] drawingXmlBytes, MapString, byte[] mediaFileMap, int sheetIndex) throws Exception { ListCellImageInfo images new ArrayList(); DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); // 必须开启命名空间支持 DocumentBuilder db dbf.newDocumentBuilder(); Document doc db.parse(new ByteArrayInputStream(drawingXmlBytes)); // 定义命名空间WPS和MS Office可能使用相同的 MapString, String nsMap new HashMap(); nsMap.put(a, http://schemas.openxmlformats.org/drawingml/2006/main); nsMap.put(xdr, http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing); nsMap.put(r, http://schemas.openxmlformats.org/officeDocument/2006/relationships); // 查找所有的 twoCellAnchor 或 oneCellAnchor 元素图片锚点 NodeList anchorList doc.getElementsByTagNameNS(nsMap.get(xdr), twoCellAnchor); // 也可以考虑 oneCellAnchor这里以 twoCellAnchor 为例 if (anchorList.getLength() 0) { // 尝试其他可能的锚点类型或本地名称 anchorList doc.getElementsByTagName(twoCellAnchor); } for (int i 0; i anchorList.getLength(); i) { Element anchor (Element) anchorList.item(i); // 在锚点元素下查找 pic 元素 NodeList picList anchor.getElementsByTagNameNS(nsMap.get(xdr), pic); if (picList.getLength() 0) { picList anchor.getElementsByTagName(pic); } for (int j 0; j picList.getLength(); j) { Element pic (Element) picList.item(j); CellImageInfo imageInfo parsePicElement(pic, anchor, nsMap, mediaFileMap); if (imageInfo ! null) { images.add(imageInfo); } } } return images; } private CellImageInfo parsePicElement(Element pic, Element anchor, MapString, String nsMap, MapString, byte[] mediaFileMap) { try { CellImageInfo info new CellImageInfo(); // 1. 解析关系ID和图片引用 // 路径通常为: pic nvPicPr cNvPr blipFill blip r:embed Element blipFill (Element) pic.getElementsByTagNameNS(nsMap.get(xdr), blipFill).item(0); if (blipFill null) blipFill (Element) pic.getElementsByTagName(blipFill).item(0); if (blipFill ! null) { Element blip (Element) blipFill.getElementsByTagNameNS(nsMap.get(a), blip).item(0); if (blip null) blip (Element) blipFill.getElementsByTagName(blip).item(0); if (blip ! null) { String embedId blip.getAttributeNS(nsMap.get(r), embed); info.setRelationshipId(embedId); // 2. 根据关系ID从同一drawing.xml的关系文件/_rels/drawingX.xml.rels中解析出目标媒体文件名 // 为简化这里假设关系ID直接对应media下的文件名如 rId1 - image1.png。 // 更严谨的做法是解析对应的 .rels 文件。这里提供一个启发式匹配 String possibleFileName findMediaFileByRelId(embedId, mediaFileMap.keySet()); if (possibleFileName ! null mediaFileMap.containsKey(possibleFileName)) { info.setImageData(mediaFileMap.get(possibleFileName)); info.setImageFileName(possibleFileName); // 根据文件后缀推断格式 if (possibleFileName.endsWith(.png)) { info.setFormat(image/png); info.setSuggestedFileSuffix(.png); } else if (possibleFileName.endsWith(.jpg) || possibleFileName.endsWith(.jpeg)) { info.setFormat(image/jpeg); info.setSuggestedFileSuffix(.jpg); } // ... 其他格式 } } } // 3. 解析锚点坐标 Element from (Element) anchor.getElementsByTagNameNS(nsMap.get(xdr), from).item(0); Element to (Element) anchor.getElementsByTagNameNS(nsMap.get(xdr), to).item(0); if (from null || to null) { from (Element) anchor.getElementsByTagName(from).item(0); to (Element) anchor.getElementsByTagName(to).item(0); } if (from ! null) { Element colElem (Element) from.getElementsByTagNameNS(nsMap.get(xdr), col).item(0); Element rowElem (Element) from.getElementsByTagNameNS(nsMap.get(xdr), row).item(0); if (colElem null || rowElem null) { colElem (Element) from.getElementsByTagName(col).item(0); rowElem (Element) from.getElementsByTagName(row).item(0); } if (colElem ! null) info.setAnchorFromCol(Integer.parseInt(colElem.getTextContent())); if (rowElem ! null) info.setAnchorFromRow(Integer.parseInt(rowElem.getTextContent())); } if (to ! null) { // 解析逻辑同 from Element colElem (Element) to.getElementsByTagNameNS(nsMap.get(xdr), col).item(0); Element rowElem (Element) to.getElementsByTagNameNS(nsMap.get(xdr), row).item(0); if (colElem null || rowElem null) { colElem (Element) to.getElementsByTagName(col).item(0); rowElem (Element) to.getElementsByTagName(row).item(0); } if (colElem ! null) info.setAnchorToCol(Integer.parseInt(colElem.getTextContent())); if (rowElem ! null) info.setAnchorToRow(Integer.parseInt(rowElem.getTextContent())); } return (info.getImageData() ! null) ? info : null; // 确保有图片数据才返回 } catch (Exception e) { // 记录日志跳过此图片 System.err.println(解析图片元素时出错: e.getMessage()); return null; } } /** * 简化版的关系ID到文件名的映射查找。 * 实际情况更复杂需要解析 drawingX.xml.rels 文件。 * 这里假设一种常见模式rId1 对应 image1.png, rId2 对应 image2.png... */ private String findMediaFileByRelId(String relId, SetString mediaFileNames) { if (relId null) return null; // 尝试从 rId1 中提取数字 1 String numStr relId.replaceAll(\\D, ); // 移除非数字字符 if (!numStr.isEmpty()) { int index Integer.parseInt(numStr); for (String fileName : mediaFileNames) { // 查找类似 image1.png, image2.jpg 的文件 if (fileName.matches((?i).* index \\..)) { return fileName; } } } // 如果上述方法失败回退到遍历所有媒体文件寻找可能匹配根据实际观察的WPS命名规律调整 // 例如WPS有时可能命名为 picture1.png for (String fileName : mediaFileNames) { if (fileName.contains(relId) || fileName.contains(numStr)) { // 弱匹配 return fileName; } } return null; } // 辅助方法根据工作表索引猜测对应的drawing文件名 private String findDrawingXmlKeyForSheet(int sheetIndex, SetString drawingKeys) { // 通常第一个工作表的drawing是 drawing1.xml但并非绝对。 // 更可靠的方法是通过解析 /xl/_rels/workbook.xml.rels 来建立sheet与drawing的关系。 // 此处为简化使用常见模式。 String expectedName xl/drawings/drawing (sheetIndex 1) .xml; if (drawingKeys.contains(expectedName)) { return expectedName; } // 如果找不到返回第一个drawing文件这是一种妥协对于单图sheet可能有效 return drawingKeys.isEmpty() ? null : drawingKeys.iterator().next(); } // 读取单元格数据的标准方法略 private void readSheetData(Sheet sheet, SheetDataAndImages result) { // ... 使用POI的Row和Cell API遍历读取数据存储到result中 for (Row row : sheet) { // ... 读取每一行每一列 } } } // 用于承载一个工作表所有数据的容器类 Data class SheetDataAndImages { private String sheetName; private ListListObject cellData; // 二维列表存储单元格值 private ListCellImageInfo images new ArrayList(); }3.4 使用示例public class Main { public static void main(String[] args) { WpsAwareExcelReader reader new WpsAwareExcelReader(); try { File wpsExcelFile new File(path/to/your/wps_file.xlsx); MapInteger, SheetDataAndImages allData reader.readExcel(wpsExcelFile); for (Map.EntryInteger, SheetDataAndImages entry : allData.entrySet()) { int sheetIndex entry.getKey(); SheetDataAndImages sheetInfo entry.getValue(); System.out.println(工作表: sheetInfo.getSheetName()); System.out.println(找到 sheetInfo.getImages().size() 张图片); for (CellImageInfo img : sheetInfo.getImages()) { System.out.println( - 图片: img.getImageFileName() , 格式: img.getFormat() , 锚定于单元格: ( img.getAnchorFromRow() , img.getAnchorFromCol() )); // 可以将图片数据保存到文件 if (img.getImageData() ! null) { String outputPath output/images/sheet sheetIndex _ img.getImageFileName(); Files.write(Paths.get(outputPath), img.getImageData()); } } // 处理单元格数据... // ListListObject data sheetInfo.getCellData(); } } catch (Exception e) { e.printStackTrace(); } } }4. 避坑指南与性能优化实战心得在实际开发和压测中我遇到了不少坑也总结了一些优化点。4.1 常见问题排查表问题现象可能原因解决方案getAllPictures()返回空列表但文件里确实有图。1. WPS文件格式差异导致POI关系映射失败。2. 图片可能以“浮动图形”而非“嵌入式对象”存在。启用本文的“手动解析Drawing XML”方案。检查drawingX.xml中是否存在xdr:twoCellAnchor或xdr:oneCellAnchor元素。手动解析找到了图片二进制但无法关联到正确单元格。1. 锚点坐标解析错误命名空间问题。2. WPS使用了非标准的from/to子元素名。在parsePicElement方法中增加调试日志打印出解析出的XML片段。对比WPS和MS Office生成的drawingX.xml文件差异。读取大文件50MB时内存溢出OOM。1. POI的XSSFWorkbook一次性将整个工作簿加载到内存。2. 手动解压时ByteArrayOutputStream缓存了所有媒体文件。对于大数据量文件强烈推荐使用POI的“事件模型”XSSF and SAX (Event API)。对于图片可以在SAX解析过程中遇到drawing部分时仅记录图片关系ID和坐标最后再按需从ZIP包中提取对应的媒体文件字节避免一次性加载所有图片到内存。图片保存后损坏或无法打开。1. 从ZIP流中读取字节时出错未正确关闭流或缓冲区处理不当。2. 图片格式推断错误。使用ByteArrayOutputStream确保完整读取。通过文件魔数Magic Number或ImageIO.read()来验证图片数据的有效性而不仅仅依赖文件扩展名。处理速度非常慢。1. 多次遍历ZIP文件分别读drawing和media。2. DOM解析XML对于大文件开销大。优化为单次遍历ZIP同时收集drawing和media文件。对于非常大的drawingX.xml考虑使用SAX解析器替代DOM。4.2 性能与内存优化实战技巧流式处理与按需加载这是处理大型Excel文件的黄金法则。不要使用XSSFWorkbook而是用XSSFReader配合SAXParser。你可以重写org.xml.sax.helpers.DefaultHandler在遇到drawing或pic相关标签时只记录其r:id和坐标信息存入一个轻量级的列表。在所有Sheet的XML解析完成后再根据这个列表用ZipArchiveInputStream精准定位到/xl/media/目录下对应的图片文件进行读取。这样内存中始终只保持当前正在处理的Sheet的单元格数据和少数几张图片的引用。缓存Drawing XML解析结果一个Excel文件中多个Sheet可能共用同一个drawingX.xml虽然不常见。可以在第一次解析后将其Document对象或解析出的结构缓存起来避免重复的XML解析和ZIP条目查找。关系文件的精准映射上述示例中的findMediaFileByRelId方法是启发式的不健壮。生产环境必须解析xl/drawings/_rels/drawingX.xml.rels文件。这个文件明确记录了drawingX.xml内部rId1这样的关系ID到底对应着../media/image1.png这样的哪个具体目标。解析这个.rels文件是建立准确映射的唯一可靠方法。处理单元格注释中的图片除了通过Drawing嵌入的图片单元格注释Comment里也可能有图片。POI提供了XSSFSheet.getCellComments()来获取所有注释但注释中的图片存储路径和方式又与Drawing不同通常存储在/xl/media/下但关系链是另一套。如果你的需求包含注释图片需要额外处理vmlDrawing部分对应xl/drawings/vmlDrawingX.vml文件。关于WPS JS宏的图片如果Excel文件包含了WPS JS宏并且宏动态生成了图片这些图片可能不会以标准嵌入式图片的形式存在而是作为OLE对象或其他形式。这种情况极为复杂通常的解决方案是要求用户在执行宏并生成静态图片后再保存文件进行导入。4.3 一个更健壮的关系解析方法示例补充一个解析.rels文件来精准定位图片的方法private MapString, String parseDrawingRels(byte[] relsBytes) throws Exception { MapString, String relIdToTarget new HashMap(); DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); Document doc dbf.newDocumentBuilder().parse(new ByteArrayInputStream(relsBytes)); NodeList relationshipList doc.getElementsByTagName(Relationship); for (int i 0; i relationshipList.getLength(); i) { Element rel (Element) relationshipList.item(i); String id rel.getAttribute(Id); String target rel.getAttribute(Target); String type rel.getAttribute(Type); // 类型为 http://schemas.openxmlformats.org/officeDocument/2006/relationships/image if (type ! null type.contains(image)) { // target 可能是 ../media/image1.png需要规范化 if (target.startsWith(../)) { target target.substring(3); // 去掉 ../ - media/image1.png } // 只取文件名部分方便与mediaFileMap匹配 String fileName target.substring(target.lastIndexOf(/) 1); relIdToTarget.put(id, fileName); } } return relIdToTarget; }然后在parseDrawingXmlManually方法中不仅传入mediaFileMap再传入这个relIdToTarget映射就可以通过relId直接拿到准确的fileName从而万无一失地获取图片数据。最后这个方案的真正价值在于它的鲁棒性。它不依赖于POI内部可能变化的实现细节而是基于OOML标准文件结构进行解析因此对WPS不同版本、甚至未来其他兼容Excel的办公软件都可能有更好的适应性。当然它也增加了代码的复杂度所以你需要根据项目的实际需求是只需要处理WPS文件还是需要一套通吃所有xlsx的方案来权衡是否引入这套增强逻辑。对于大多数“读取WPS带图Excel”的需求本文提供的混合方案POI标准API 手动解析兜底已经是一个在开发成本与可靠性之间取得不错平衡的实践。