EasyExcel 流式处理中实现末尾行过滤的技术方案

背景

在处理大规模 Excel 数据导入时,常遇到文件末尾包含汇总行的情况。这类汇总数据不应被导入业务表,但在流式处理模式下,传统的"读取总行数再减N"的方式无法应用。本文介绍一种基于延迟缓冲区的解决方案。


问题分析

传统方案的局限性

方案一:全量加载后过滤

java 复制代码
List<Row> allRows = readAllRows(file);
for (int i = 0; i < allRows.size() - skipEndRows; i++) {
    process(allRows.get(i));
}

缺陷:违背流式处理的设计初衷,大文件场景下易导致 OOM。


方案二:直接流式处理

java 复制代码
void invoke(RowData row) {
    // 边读边处理,但无法判断当前行是否为末尾行
}

缺陷:在逐行回调模式下,当前行的位置信息不足以判断其是否属于需要跳过的末尾区间。


技术方案

核心思想

采用固定窗口延迟缓冲机制 :维护一个大小为 skipEndRows 的滑动窗口,仅处理窗口溢出的数据,确保末尾 N 行始终保留在缓冲区内,直到文件读取完毕后被自动丢弃。

实现原理

  1. 使用 LinkedList 作为延迟缓冲区
  2. 每读取一行数据,追加至缓冲区尾部
  3. 当缓冲区大小超过 skipEndRows 时,从队列头部移除一行并提交处理
  4. 文件读取完成后,缓冲区内剩余的 skipEndRows 行数据自动随对象销毁而丢弃

流程示意图

skipEndRows = 1 为例,处理包含 5 行数据 + 1 行汇总的文件:

阶段 触发事件 delayBuffer 状态 batchRowData 操作说明
1 invoke(row1) [row1] [] size=1 ≤ skipEndRows,暂不处理
2 invoke(row2) [row2] [row1] size=2 > 1,移除 row1 加入批次
3 invoke(row3) [row3] [row1, row2] 移除 row2
4 invoke(row4) [row4] [row1, row2, row3] 移除 row3
5 invoke(row5) [row5] [row1, ..., row4] 移除 row4
6 invoke(汇总行) [汇总] [..., row5] 移除 row5,汇总行滞留
7 doAfterAllAnalysed() [汇总] [] 处理剩余批次,不处理缓冲区
8 GC 回收 null - 缓冲区对象销毁,汇总行丢弃

代码实现

抽象基类核心逻辑

java 复制代码
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 skipEndRows = skipEndRowCount();
    final LinkedList<Map<Integer, String>> delayBuffer = new LinkedList<>();
    
    EasyExcel.read(filePath.toFile(), new AnalysisEventListener<Map<Integer, Object>>() {
        
        @Override
        public void invoke(Map<Integer, Object> data, AnalysisContext context) {
            // 省略:表头解析、空行过滤等预处理逻辑
            
            Map<Integer, String> rowData = convertToStringMap(data);
            
            // 延迟缓冲机制:入队
            delayBuffer.add(rowData);
            
            // 当缓冲区溢出时,出队并提交处理
            if (delayBuffer.size() > skipEndRows) {
                Map<Integer, String> eligibleRow = delayBuffer.removeFirst();
                batchRowData.add(eligibleRow);
                
                // 批次满时触发回调
                if (batchRowData.size() >= batchSize) {
                    batchProcessor.accept(headerRowMap, new ArrayList<>(batchRowData));
                    batchRowData.clear();
                }
            }
        }
        
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            // 处理批次尾部数据
            if (!batchRowData.isEmpty()) {
                batchProcessor.accept(headerRowMap, new ArrayList<>(batchRowData));
                batchRowData.clear();
            }
            // 关键:delayBuffer 内的 skipEndRows 行数据在此被隐式丢弃
        }
    }).sheet(getTargetSheetIndex()).headRowNumber(0).doRead();
}

模板方法设计

java 复制代码
public abstract class AbstractBaseExcelParser {
    
    /**
     * 钩子方法:返回需跳过的末尾行数
     * 子类可覆写以适配不同业务场景
     */
    protected int skipEndRowCount() {
        return 0;  // 默认不跳过
    }
}

@Component
public class FinancialStatementParser extends AbstractBaseExcelParser {
    
    @Override
    protected int skipEndRowCount() {
        return 1;  // 跳过汇总行
    }
}

技术细节

EasyExcel 回调时机分析

doAfterAllAnalysed() 的触发条件

EasyExcel 内部实现伪代码:

java 复制代码
public void doRead() {
    int totalRows = sheet.getLastRowNum();  // Excel 内部元数据记录总行数
    
    for (int i = 0; i <= totalRows; i++) {
        Row row = sheet.getRow(i);
        listener.invoke(parseRow(row), context);
    }
    
    // 循环结束后触发
    listener.doAfterAllAnalysed(context);
}

Excel 文件结构说明

.xlsx 文件本质是 ZIP 压缩包,其中 xl/worksheets/sheet1.xml 包含元数据:

xml 复制代码
<worksheet>
    <dimension ref="A1:C1001"/>  <!-- 数据范围元信息 -->
    <sheetData>
        <row r="1">...</row>
        <!-- ... -->
        <row r="1001">...</row>
    </sheetData>
</worksheet>

EasyExcel 解析 <dimension> 标签即可获知总行数,从而判断读取结束时机。


方案总结

优势

  1. 内存可控 :占用量仅为 O(batchSize + skipEndRows),与文件大小无关
  2. 逻辑清晰:核心实现仅 3 行代码,可读性强
  3. 扩展性好:通过模板方法模式支持子类灵活配置跳过行数

适用场景

  • 大规模 Excel 数据导入
  • 文件末尾包含汇总/统计行
  • 内存受限环境
  • 需要边读边处理的流式场景

参考资料

相关推荐
昵称画2 天前
POC验证怎么设计用例?不走过程的实操要点
大数据·数据库·人工智能·低代码·excel
Wang's Blog2 天前
Vibe Coding一人即团队系列62: Excel数据分析与可视化自动化
数据分析·自动化·excel
统计学小王子2 天前
数学建模国赛倒计时5天——《软件工具(Excel)——国赛数据处理的第一道防线》
数学建模·excel
Am-Chestnuts2 天前
用 DS随心转 把 AI 对话里的多组数据批量导出成 Excel
人工智能·excel
2601_960356382 天前
库存分析岗位秋招准备:SQL、Excel、WMS与供应链指标
数据库·sql·excel
统计学小王子2 天前
数学建模国赛倒计时5天——《软件工具(Excel)——快捷键总结》
数学建模·excel
2601_963906782 天前
网页版 Excel 合并工具
excel·软件需求
疯狂小猫咪2 天前
中小培训机构教务数字化踩坑复盘|从 Excel 过渡到教务系统经验总结
小程序·excel
开开心心_Every3 天前
实现鼠标键盘动作录制与重复播放的工具
微信·jupyter·pycharm·pdf·计算机外设·word·excel
智途 Tech3 天前
2026年分析多个Excel、CSV和网页数据的AI工具清单:Tabbit 浏览器多源引用
大数据·人工智能·excel