Java读取WPS Excel嵌入式图片:POI增强方案与实战避坑指南
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 手动解析兜底已经是一个在开发成本与可靠性之间取得不错平衡的实践。

相关新闻

《中餐厅10》再迎挑战 黄晓明“全自动帮厨”技能点满 细节控店长拉满服务力

《中餐厅10》再迎挑战 黄晓明“全自动帮厨”技能点满 细节控店长拉满服务力

《中餐厅南洋拾光季》第七期正式上线,本期节目中,中餐厅迎来第二次主厨餐桌挑战,前厅后厨双线并进,上演了一场忙碌又温馨的美味保卫战。店长黄晓明用细致入微的关怀与高效的行动力,在前厅后厨之间稳健穿梭,…

2026/8/2 4:10:15阅读更多 →
富士XT-5相机全面上手指南:从核心设定到实战技巧

富士XT-5相机全面上手指南:从核心设定到实战技巧

1. 从开箱到开机:XT-5的初次上手与核心设定拿到富士XT-5,第一感觉是它比想象中更紧凑、更扎实。作为富士X系列的新一代旗舰,它延续了经典的复古旁轴造型,但内在却是一次全面的进化。对于从XT-4升级或者初次接触富士的用户来说&…

2026/8/2 4:10:15阅读更多 →
Android端车牌识别实战:YOLOv5与PlateNet的移动端部署与优化

Android端车牌识别实战:YOLOv5与PlateNet的移动端部署与优化

1. 从桌面到掌上:为什么要在Android上做车牌识别?几年前,当我第一次把训练好的车牌检测模型从服务器部署到一台老旧的Android测试机上时,那体验堪称灾难。画面卡顿、识别延迟高、手机发烫,一个简单的Demo几乎耗尽了所有…

2026/8/2 4:10:15阅读更多 →
计算机组成原理指令系统习题精解:从指令格式到程序执行全剖析

计算机组成原理指令系统习题精解:从指令格式到程序执行全剖析

1. 为什么课后习题的答案与解析如此重要?如果你正在学习《计算机组成原理》,尤其是使用微课版教材,那么第五章“指令系统”绝对是一个分水岭。很多同学学到这里,感觉概念都懂了,但一做题就懵,特别是遇到那些…

2026/8/2 5:26:38阅读更多 →
解决Word中MathPage.WLL文件未找到错误的完整指南

解决Word中MathPage.WLL文件未找到错误的完整指南

1. 问题现象与根源剖析如果你在安装完Mathtype后,满怀期待地打开Word准备大展拳脚,结果迎面弹出一个“文件未找到:MathPage.WLL”的错误对话框,那种感觉就像开车时发现钥匙插不进去一样,既困惑又恼火。这个报错的核心&…

2026/8/2 5:26:38阅读更多 →
AI智能体调试新范式:AgentRx框架实现可观测性与实时干预

AI智能体调试新范式:AgentRx框架实现可观测性与实时干预

1. 项目概述:当AI智能体“生病”时,我们如何诊断?在AI智能体(AI Agent)的开发与应用浪潮中,一个日益凸显的痛点正困扰着每一位从业者:调试。想象一下,你精心设计的智能体&#xff0c…

2026/8/2 5:26:38阅读更多 →
生物医药投资评估:从团队、管线到AI驱动的多维尽调逻辑

生物医药投资评估:从团队、管线到AI驱动的多维尽调逻辑

1. 项目概述:解码生物医药投资的“好标的”密码在生物医药这个被誉为“皇冠上的明珠”的赛道里,每天都有无数的创业故事和科学突破上演。作为一名长期关注并参与早期投资的从业者,我经常被问到一个问题:“你们到底看中一家生物医药…

2026/8/2 5:26:38阅读更多 →
计算机组成原理课程设计:复杂模型机核心部件与微程序设计实战

计算机组成原理课程设计:复杂模型机核心部件与微程序设计实战

1. 从零到一:理解“复杂模型机”课程设计的核心目标如果你正在为计算机组成原理的课程设计发愁,尤其是面对“复杂模型机”这个听起来就有点唬人的题目,别慌,这其实是这门课最精华、最能让你把书本知识“串”起来的实践环节。我当年…

2026/8/2 5:26:38阅读更多 →
Ascend C 算子实战(二)|SigmoidCustom 逐元素激活算子完整开发指南

Ascend C 算子实战(二)|SigmoidCustom 逐元素激活算子完整开发指南

前言前面我们完成了 AddCustom、SubCustom 二元逐元素算子开发,吃透了「Host 调度 AICore 核内流水线」标准化开发框架。本次进阶实现单输入激活算子 SigmoidCustom,完整落地 sigmoid (x) 1/(1exp (-x)) 算法。 相比加减二元算子,Sigmoid 仅…

2026/8/2 5:24:38阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/2 0:00:10阅读更多 →
限时公开!某头部SaaS公司内部AI模板工厂架构文档(含5类行业模板源码+性能压测报告)

限时公开!某头部SaaS公司内部AI模板工厂架构文档(含5类行业模板源码+性能压测报告)

更多请点击: https://intelliparadigm.com 第一章:AI模板批量生成的核心价值与落地全景 AI模板批量生成正从实验性工具演进为现代软件工程的关键基础设施。它通过语义理解、上下文感知与结构化约束,将重复性高、模式明确的代码/文档/配置生成…

2026/8/2 0:00:12阅读更多 →
如何快速找回消失的网页:Web Archives浏览器扩展终极指南

如何快速找回消失的网页:Web Archives浏览器扩展终极指南

如何快速找回消失的网页:Web Archives浏览器扩展终极指南 【免费下载链接】web-archives Browser extension for viewing archived and cached versions of web pages, available for Chrome, Edge and Safari 项目地址: https://gitcode.com/gh_mirrors/we/web-a…

2026/8/2 0:00:13阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/2 0:00:10阅读更多 →
限时公开!某头部SaaS公司内部AI模板工厂架构文档(含5类行业模板源码+性能压测报告)

限时公开!某头部SaaS公司内部AI模板工厂架构文档(含5类行业模板源码+性能压测报告)

更多请点击: https://intelliparadigm.com 第一章:AI模板批量生成的核心价值与落地全景 AI模板批量生成正从实验性工具演进为现代软件工程的关键基础设施。它通过语义理解、上下文感知与结构化约束,将重复性高、模式明确的代码/文档/配置生成…

2026/8/2 0:00:12阅读更多 →
如何快速找回消失的网页:Web Archives浏览器扩展终极指南

如何快速找回消失的网页:Web Archives浏览器扩展终极指南

如何快速找回消失的网页:Web Archives浏览器扩展终极指南 【免费下载链接】web-archives Browser extension for viewing archived and cached versions of web pages, available for Chrome, Edge and Safari 项目地址: https://gitcode.com/gh_mirrors/we/web-a…

2026/8/2 0:00:13阅读更多 →
无损视频剪辑终极指南:如何实现快速高效的多媒体处理

无损视频剪辑终极指南:如何实现快速高效的多媒体处理

无损视频剪辑终极指南:如何实现快速高效的多媒体处理 【免费下载链接】lossless-cut The swiss army knife of lossless video/audio editing 项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut 在数字媒体创作领域,视频编辑处理的质量损…

2026/8/2 1:29:34阅读更多 →
AI辅助本科论文写作:8大工具评测与高效使用指南

AI辅助本科论文写作:8大工具评测与高效使用指南

1. 本科生论文写作的AI辅助现状本科毕业论文是每个大学生必须跨越的一道坎。记得我当年写论文时,光是文献检索就花了整整两周时间,打印的参考文献堆满了半个书桌。如今AI技术的发展为学术写作带来了革命性变化,合理使用这些工具可以节省80%以…

2026/8/2 2:32:55阅读更多 →
如何快速配置大麦自动抢票系统:从零开始搭建Python抢票助手

如何快速配置大麦自动抢票系统:从零开始搭建Python抢票助手

如何快速配置大麦自动抢票系统:从零开始搭建Python抢票助手 【免费下载链接】ticket-purchase 大麦自动抢票,支持人员、城市、日期场次、价格选择 项目地址: https://gitcode.com/GitHub_Trending/ti/ticket-purchase 还在为抢不到热门演唱会门票…

2026/8/2 2:09:20阅读更多 →