如何使用Apache XSSF POI流式API读取真实数值而非格式化值?
解决POI流式API读取单元格真实数值的问题
我完全懂你的困扰——用流式API本来是为了解决内存占用问题,结果却只能拿到格式化后的截断值,根本取不到单元格的真实数值。别着急,咱们换个思路,不依赖SheetContentsHandler里的formattedValue,直接解析Excel底层的XML事件,就能拿到原始值,同时还能保持流式处理的内存优势。
问题根源
你之前用的SheetContentsHandler.cell()方法里的formattedValue,是POI通过DataFormatter按照单元格的显示格式处理后的结果——比如单元格设置了只显示2位小数,那这个值就会自动截断成2位,自然拿不到真实的完整数值。要获取原始值,得直接读取XML里存储的单元格原生内容。
解决方案:直接解析XML事件流
通过XSSFReader读取每个sheet的XML流,用SAX解析器逐个处理XML元素,提取单元格的类型和原始值。这种方式完全是流式处理,不会把整个Excel文档加载到内存,同时能拿到未被格式化的真实数据。
示例代码
import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.xssf.eventusermodel.XSSFReader; import org.apache.poi.xssf.model.SharedStringsTable; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; import java.io.File; import java.io.InputStream; import java.math.BigDecimal; public class StreamingExcelRealValueReader { public static void main(String[] args) throws Exception { OPCPackage pkg = OPCPackage.open(new File("your-excel-file.xlsx")); XSSFReader reader = new XSSFReader(pkg); SharedStringsTable sharedStringsTable = reader.getSharedStringsTable(); // 创建SAX解析器并设置自定义内容处理器 XMLReader saxParser = XMLReaderFactory.createXMLReader(); saxParser.setContentHandler(new RealValueContentHandler(sharedStringsTable)); // 遍历所有sheet并解析 XSSFReader.SheetIterator sheetIterator = (XSSFReader.SheetIterator) reader.getSheetsData(); while (sheetIterator.hasNext()) { try (InputStream sheetStream = sheetIterator.next()) { System.out.println("正在处理Sheet: " + sheetIterator.getSheetName()); saxParser.parse(new InputSource(sheetStream)); } } pkg.close(); } private static class RealValueContentHandler extends DefaultHandler { private final SharedStringsTable sharedStringsTable; private String currentCellRef; private String currentCellType; private StringBuilder currentCellValue; public RealValueContentHandler(SharedStringsTable sharedStringsTable) { this.sharedStringsTable = sharedStringsTable; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { switch (qName) { case "c": // 单元格元素开始 currentCellRef = attributes.getValue("r"); currentCellType = attributes.getValue("t"); // 获取单元格类型标识 currentCellValue = new StringBuilder(); break; case "v": // 单元格值元素开始,准备收集内容 currentCellValue = new StringBuilder(); break; } } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (currentCellValue != null) { currentCellValue.append(ch, start, length); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if ("v".equals(qName)) { // 单元格值读取完成,处理原始值 Object realValue = parseRawValue(currentCellValue.toString(), currentCellType); // 替换成你的业务逻辑方法 useTheCellValue(currentCellRef, realValue); } } private Object parseRawValue(String rawValue, String cellType) { if (rawValue == null || rawValue.isEmpty()) { return null; } switch (cellType) { case "s": // 共享字符串类型,从共享表中获取真实字符串 int stringIndex = Integer.parseInt(rawValue); return sharedStringsTable.getItemAt(stringIndex).getString(); case "n": // 数字类型,用BigDecimal避免精度丢失 return new BigDecimal(rawValue); case "b": // 布尔类型 return "1".equals(rawValue); // 按需扩展:公式、日期等类型的处理 default: return rawValue; } } } // 替换为你的业务处理方法 private static void useTheCellValue(String cellRef, Object value) { System.out.printf("单元格 %s: 真实值 = %s (类型: %s)%n", cellRef, value, value != null ? value.getClass().getSimpleName() : "空值"); } }
关键细节说明
- 单元格类型识别:通过XML元素
c的t属性判断类型,比如"n"代表数字、"s"代表共享字符串、"b"代表布尔值。 - 原始值解析:
- 数字类型用
BigDecimal解析原始字符串,避免double类型的精度丢失问题 - 字符串类型从
SharedStringsTable中读取真实内容(Excel会复用重复字符串来节省空间)
- 数字类型用
- 流式内存优势:整个过程只处理当前XML片段,不会加载整个文档到内存,完美解决大文件的内存占用问题。
额外提示
如果需要处理日期类型,Excel会把日期存储为数字(从1900年1月1日开始的天数),这时候需要额外解析单元格的样式表,判断是否为日期格式后再转换为日期对象。不过如果你的需求只是获取数值的真实值,上面的代码就足够满足了。
内容的提问来源于stack exchange,提问作者Vincent Couturier
相关产品推荐
相关产品推荐

