当前位置: 首页 > news >正文

处理大型excel文件的技术选型

处理大型excel文件的技术选型

前言

最近在做财务系统的时候,遇到了一个头疼的问题:客户经常要导入几十万行的Excel账单文件,用传统的POI直接读取,内存直接爆了。这不,就踩坑踩了一圈,把几个主流的解决方案都试了个遍,今天就来聊聊大型Excel文件到底该怎么处理。

问题背景

我们的场景很简单:财务人员需要导入各个平台(美团、抖音、京东等)导出的Excel账单,少则几千行,多则几十万行。一开始用POI的普通读取方式,结果10万行的文件直接OOM了。

这时候你就会发现,Excel大文件处理是个坑,不是所有的库都能hold住。

方案一:POI 5.0 的 SAX 模型

优点

  • 内存占用低:真正的流式读取,理论上可以处理无限大的文件
  • 官方支持:Apache POI 是官方库,不用担心兼容性问题
  • 性能优秀:底层基于事件驱动,速度快

缺点(重点来了)

  • 代码复杂度爆表:你需要实现一堆回调接口,什么 SharedStringsTableXSSFReaderContentHandler,看着就头大
  • 维护成本高:半年后你自己都不想碰这段代码,更别说接手的同事了
  • 学习曲线陡峭:新人基本看不懂,还得花时间培训

实战感受

我试着用SAX模型写了一版,光是处理表头和数据解析就写了200多行代码,还得处理各种边界情况。代码看起来像这样:

class MyXSSFSheetXMLHandler implements XSSFSheetXMLHandler.SheetContentsHandler {@Overridepublic void startRow(int rowNum) {// 处理行开始}@Overridepublic void cell(String cellReference, String formattedValue, XSSFComment comment) {// 处理每个单元格// 还得自己解析列索引...}@Overridepublic void endRow(int rowNum) {// 处理行结束// 在这里组装数据...}
}

说实话,写完这段代码我就后悔了。虽然能用,但维护起来真的太痛苦了。不推荐,除非你有特殊需求必须用POI 5.0的SAX。

方案二:excel-streaming-reader

优点

  • 代码简洁:API设计得很友好,跟普通的POI读取差不多
  • 上手快:基本不需要学习成本,看一眼文档就会用
  • 与POI结合好:如果你用的是POI 4.x,这个库简直完美

缺点(致命伤)

  • 不兼容POI 5.x:这是最大的问题!POI 5.0改了很多底层API,excel-streaming-reader的最新版本(2.1.0)还停留在POI 4.x
  • 会报错:如果你的项目用的是POI 5.2.3(像我们一样),直接就报NoSuchMethodError

实战感受

我一开始很兴奋地引入了这个库,代码写起来确实舒服:

try (InputStream is = new FileInputStream(file);Workbook workbook = StreamingReader.builder().rowCacheSize(100).bufferSize(4096).open(is)) {for (Sheet sheet : workbook) {for (Row r : sheet) {// 直接遍历,多简单!}}
}

但是!运行的时候直接崩溃:

NoSuchMethodError: 'org.apache.poi.xssf.model.SharedStringsTable 
org.apache.poi.xssf.eventusermodel.XSSFReader.getSharedStringsTable()'

查了一圈发现,POI 5.0把这个方法签名改了,excel-streaming-reader还没适配。无奈只能放弃。

结论:如果你的项目还在用POI 4.x,强烈推荐这个库;如果已经升级到POI 5.x,就别折腾了。

方案三:阿里 EasyExcel(强烈推荐)

优点

  • 完美兼容POI 5.x:不用担心版本冲突问题
  • 性能优秀:阿里内部大规模使用,经过生产验证
  • 内存占用极低:100万行数据也能稳稳处理
  • API友好:监听器模式,代码清晰易懂
  • 功能丰富:支持读、写、导出、样式设置等

缺点

  • 基本没有,硬要说的话就是多了一个依赖

实战感受

最后我选择了EasyExcel,结果真香!代码量直接减少了一半,而且可读性提升了好几个档次。

先加个依赖:

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.4</version>
</dependency>

然后写个监听器:

public class BillDataListener implements ReadListener<Map<Integer, String>> {private static final int BATCH_SIZE = 1000;private List<BillEntity> dataList = new ArrayList<>();private BillMapper mapper;@Overridepublic void invoke(Map<Integer, String> data, AnalysisContext context) {// 处理每一行数据BillEntity bill = convertToBill(data);dataList.add(bill);// 达到批量大小就入库if (dataList.size() >= BATCH_SIZE) {saveToDB();dataList.clear();}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 处理剩余数据saveToDB();}private void saveToDB() {if (!dataList.isEmpty()) {mapper.insertBatch(dataList);}}
}

使用起来也超级简单:

EasyExcel.read(filePath, new BillDataListener(mapper)).headRowNumber(1)  // 表头行数.sheet().doRead();

就这么简单!EasyExcel会自动流式读取,内存占用一直很稳定,我测试过50万行的文件,内存占用不到200M。

实战技巧

  1. 批量处理:别一行一行地插入数据库,攒够1000条再批量插入,性能提升10倍

  2. headRowNumber(0):如果你要自己控制表头解析,就设置为0,让EasyExcel不跳过任何行

  3. 类型转换:监听器收到的是Map<Integer, Object>,不是String!需要自己转换:

private String convertToString(Object value) {if (value == null) return "";if (value instanceof String) return (String) value;if (value instanceof Double) {// 去掉.0Double d = (Double) value;if (d == d.longValue()) {return String.valueOf(d.longValue());}return String.valueOf(d);}if (value instanceof Date) {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value);}return value.toString();
}
  1. 日期格式兼容:Excel中的日期可能是2025/10/3这种斜杠格式,要多支持几种格式:
private Date parseDate(String dateStr) {String[] patterns = {"yyyy-MM-dd HH:mm:ss","yyyy/M/d HH:mm:ss","yyyy/MM/dd HH:mm:ss","yyyy-MM-dd","yyyy/M/d","yyyy/MM/dd"};for (String pattern : patterns) {try {return new SimpleDateFormat(pattern).parse(dateStr);} catch (Exception e) {// 继续尝试下一个格式}}return null;
}
  1. 封装成抽象基类:如果你的项目有多个Excel解析场景,强烈建议封装一个抽象基类,避免重复代码:
public abstract class AbstractBaseExcelParser {/*** 流式处理Excel文件(带批量处理)* @param filePath 文件路径* @param batchSize 批次大小,建议1000* @param batchProcessor 批处理回调函数*/protected void processExcelFileStreaming(Path filePath, int batchSize, BiConsumer<Map<Integer, String>, List<Map<Integer, String>>> batchProcessor) throws Exception {final Map<Integer, String> headerRowMap = new HashMap<>();final List<Map<Integer, String>> batchRowData = new ArrayList<>(batchSize);final int headerRowIndex = getTargetHeaderRowIndex();EasyExcel.read(filePath.toFile(), new AnalysisEventListener<Map<Integer, Object>>() {private int currentRow = 0;private boolean headerParsed = false;@Overridepublic void invoke(Map<Integer, Object> data, AnalysisContext context) {// 解析表头if (currentRow == headerRowIndex) {data.forEach((key, value) -> {if (value != null) {headerRowMap.put(key, convertToString(value).trim());}});headerParsed = true;currentRow++;return;}// 跳过表头前的行if (!headerParsed || currentRow <= headerRowIndex) {currentRow++;return;}// 转换数据类型并过滤空行Map<Integer, String> rowData = convertToStringMap(data);if (!isEmptyRow(rowData)) {batchRowData.add(rowData);}// 达到批次大小时触发回调if (batchRowData.size() >= batchSize) {batchProcessor.accept(headerRowMap, new ArrayList<>(batchRowData));batchRowData.clear();}currentRow++;}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 处理剩余数据if (!batchRowData.isEmpty()) {batchProcessor.accept(headerRowMap, new ArrayList<>(batchRowData));}}}).sheet(0).headRowNumber(0).doRead();}// 类型转换辅助方法private Map<Integer, String> convertToStringMap(Map<Integer, Object> objectMap) {Map<Integer, String> stringMap = new HashMap<>();objectMap.forEach((key, value) -> {if (value != null) {stringMap.put(key, convertToString(value));}});return stringMap;}// 子类需要实现的方法protected abstract int getTargetHeaderRowIndex();
}

这样子类只需要关注业务逻辑,调用起来超级简单:

public class BillExcelParser extends AbstractBaseExcelParser {@Overrideprotected int getTargetHeaderRowIndex() {return 0;  // 第一行是表头}public void parseAndSave(Path filePath) throws Exception {processExcelFileStreaming(filePath, 1000, (headerMap, rowDataList) -> {// 批量转换为实体List<BillEntity> bills = rowDataList.stream().map(this::convertToBill).collect(Collectors.toList());// 批量入库billMapper.insertBatch(bills);});}private BillEntity convertToBill(Map<Integer, String> rowData) {// 根据headerMap找到对应的列,转换为实体// ...}
}
  1. 处理不同Excel格式的坑

    • 有些Excel文件前几行是说明文字,表头在第3行,这时候getTargetHeaderRowIndex()返回2就行
    • Excel中的数字会被读取为Double类型,身份证号、订单号这种要特别处理,不然会变成科学计数法
    • 空单元格EasyExcel会跳过,所以你的Map<Integer, String>可能没有某些key
    • 表头有合并单元格时要小心,EasyExcel会把合并单元格的值放在第一个格子里
  2. 内存优化的关键设计(重点!):

很多人用流式读取时会犯一个致命错误:直接存储POI的Row对象或Cell对象。千万别这么干!

错误示例:

// ❌ 错误:直接存储Row对象
List<Row> batchRows = new ArrayList<>();EasyExcel.read(file, new AnalysisEventListener<Map<Integer, Object>>() {@Overridepublic void invoke(Map<Integer, Object> data, AnalysisContext context) {Row row = context.readRowHolder().getRow();  // 获取POI原始RowbatchRows.add(row);  // 💣 内存泄漏!if (batchRows.size() >= 1000) {processRows(batchRows);batchRows.clear();  // 清空了,但Row还持有Workbook引用!}}
}).doRead();

为什么会内存泄漏?因为Row对象内部持有对Sheet的引用,Sheet持有Workbook的引用,Workbook持有整个文件的数据结构。即使你clear()了List,这些Row对象还在你的批处理队列里,导致垃圾回收器无法释放Workbook占用的内存。

正确做法:立即转换为纯数据结构

// ✅ 正确:立即转换为Map<Integer, String>
private Map<Integer, String> convertToStringMap(Map<Integer, Object> objectMap) {Map<Integer, String> stringMap = new HashMap<>();  // 纯数据结构objectMap.forEach((key, value) -> {if (value != null) {stringMap.put(key, convertToString(value));  // 立即转String}});return stringMap;  // 返回的Map不持有任何POI对象引用
}

使用这个方法后:

List<Map<Integer, String>> batchRowData = new ArrayList<>();  // 存纯数据EasyExcel.read(file, new AnalysisEventListener<Map<Integer, Object>>() {@Overridepublic void invoke(Map<Integer, Object> data, AnalysisContext context) {// 立即转换,不持有POI对象Map<Integer, String> rowData = convertToStringMap(data);batchRowData.add(rowData);  // ✅ 安全!if (batchRowData.size() >= 1000) {processRows(batchRowData);batchRowData.clear();  // 真正释放了内存}}
}).doRead();

效果对比

  • 错误方式:88MB文件,内存占用飙到2GB+,最后OOM
  • 正确方式:88MB文件,内存稳定在20-50MB

这就是为什么我说convertToStringMap()是个巧妙的设计——它切断了与POI对象的引用链,让垃圾回收器能够及时回收已处理的数据。

  1. 内存监控:开发的时候建议加上JVM参数 -Xmx512m -XX:+PrintGCDetails,看看内存到底占用多少,心里有个数

最终建议

根据我的实战经验,给出以下建议:

  1. 新项目直接用EasyExcel

    • 不用纠结,闭着眼睛选就行
    • 性能好、代码简洁、维护方便
  2. 老项目用POI 4.x的,可以试试excel-streaming-reader

    • 改动成本低,代码侵入性小
    • 但要评估一下后续是否会升级POI版本
  3. POI 5.x的项目,要么用EasyExcel,要么别折腾了

    • excel-streaming-reader不兼容
    • SAX模型代码复杂度太高,不值得
  4. 实在想用POI 5.x的SAX,请三思

    • 除非你有特殊需求(比如必须用POI的某些高级特性)
    • 做好写复杂代码和长期维护的准备

总结

处理大型Excel文件,技术选型真的很重要。不要盲目追求性能,代码的可维护性同样重要。

我的最终选择:EasyExcel

理由很简单:

  • 性能够用
  • 代码简洁
  • 维护方便
  • 社区活跃
  • 坑少

如果你也在纠结用哪个库,希望这篇文章能帮到你。少踩坑,早下班!


写于2025年10月,基于实际项目经验总结

http://www.dtcms.com/a/550046.html

相关文章:

  • [Comake][D1][AI_AO][bf_ssl_demo]
  • 网站建设方案报价一个用vue做的网站
  • 网上书城网站开发的目的与意阜宁城乡建设局网站
  • 数据结构——三十四、Floyd算法(王道408)
  • Linux服务器配置ssh免密登陆
  • JUC(二)-- 并发编程
  • 湖北省住房和城乡建设部网站大连网站建设收费
  • 石家庄网站建设今天改网名做淘宝客没网站
  • DoIP(Diagnostic over IP)路由激活与诊断请求
  • 协议选型框架
  • Double DQN(DDQN)详解与实现
  • 针对Java集合框架的面试题有哪些
  • HashMap 的哈希算法与冲突解决:深入 Rust 的高性能键值存储
  • 甘肃省建设厅官方网站质监局淮安网站优化
  • 网站开发设计电子书wordpress相关文章插件
  • 【Python OOP Diary 3】高级特性与设计模式练习题(七至十三)
  • 音响网站模板镇江网站制作优化
  • 01_Kotlin语言基础学习
  • 运维:部署Jenkins
  • 01_FastMCP 2.x 中文文档之FastMCP的介绍
  • @FeignClient调用超时
  • Ubuntu(②共享剪贴板)
  • 桂林北站有核酸检测点吗app开发的公司
  • apache启动失败Failed to start The Apache HTTP Server.
  • 汕头建设南京网站seo服务
  • 饰品网站模板建立网站的作用
  • 解析视频融合平台EasyCVR的分析平台技术如何成为“全域视频管理中台”
  • ubuntu启动项问题
  • 网站开发工资咋样品牌建设的作用和意义
  • 展馆门户网站建设深圳网站外包公司