news 2026/10/13 1:19:45

EasyExcel实战:解决Apache POI内存溢出,高效读写Excel

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EasyExcel实战:解决Apache POI内存溢出,高效读写Excel

1. 项目缘起:为什么Java操作Excel会选EasyExcel

相信每个Java后端多少都碰过读写Excel的需求,固定格式报表导出、批量导入业务数据、定时任务生成对账文件,各种场景绕不开操作Excel。早些年我的做法是直接用Apache POI去写,但遇到的麻烦相当一致:导出几万行数据时内存直接飙升,动不动就OOM,调了JVM参数也救不回来;代码写起来也啰嗦,光一个单元格样式就能堆出三五十行代码。后来接触到阿里巴巴开源的EasyExcel,才发现原来Java操作Excel可以做得这么清爽,从工具选型到日常使用,这套方案我用到现在,几乎成为了所有项目的标配。

EasyExcel本质上是对Apache POI的轻量封装,但它没有停留在“简化API”这一层,而是把读写模式做了重构。写Excel时支持模型化映射,一行注解就能定义列名、顺序、宽度、日期格式;读Excel时走的是SAX事件解析模式,不是把整个工作簿一次性加载进内存,而是逐行解析触发回调,几百万行的文件也能稳定处理。这一点在实际生产中价值极大,服务器内存本来就宝贵,用POI加载一个50MB的Excel可能要占用1GB堆内存,换成EasyExcel做流式读取,内存占用可能只有几十MB。

实话实说,EasyExcel不是万能的,动画图表、复杂透视表、跨Sheet合并报表这种高级玩法它搞不定,该上POI的场景还得上POI。但如果你只需要做“把Java对象列表变成Excel文件”和“把Excel文件变成Java对象列表”这两件事,它几乎是无脑最优解。本文所有代码示例基于EasyExcel 3.x版本,建议读者使用3.1.0以上版本,后续内容我会把依赖引入、读写实操、高级特性和踩坑记录全部串起来讲,尽量保证照着做就能跑通。

2. 选型背后:EasyExcel到底解决了什么痛点

2.1 Apache POI模式下那些让人头疼的问题

在讲EasyExcel之前,我觉得有必要先复盘一下直接用POI的体验,不然你理解不了为什么EasyExcel的设计思路如此重要。POI有两种经典操作模式:HSSF对应Excel 2003的.xls格式,XSSF对应Excel 2007以上的.xlsx格式。XSSF用起来倒是直观,拿Workbook对象去创建Sheet、Row、Cell,但问题就在这个Workbook对象上——它默认会把整个文件的XML结构全部解析进内存,构建成一张巨大的对象树。你想想,一个十万行、每行二十列的Excel文件,底层XML里可能要拆出上百万个节点,每个节点又有各种属性、风格引用,这些全部塞进堆内存,量级迅速膨胀。

我记忆里最惨的一次,项目上线前导出一份六十万行左右的业务数据报表,测试环境直接OOM,日志里报的是堆内存溢出。后来加了-Xmx2g参数依然不稳定,因为POI对内存的消耗不是线性增长的,某些操作还会复制出一份新的对象树,峰值瞬间翻倍。就算内存撑住了,解析速度也慢,用户一听说生成文件要等十几秒甚至半分钟,体验直接崩塌。

2.2 EasyExcel的核心设计逻辑:从DOM到SAX

EasyExcel解决内存问题的核心思路,是抛弃了POI的DOM式一次性加载,改用SAX式流式解析。这个过程可以简单类比成看一本厚字典:POI是先把整本字典复制一遍放桌上,想看哪页翻哪页,快是快,但先把桌子占满了;EasyExcel是按顺序一页一页地翻,翻完一页就处理一页,处理完就丢到一边,桌上永远只留两三页纸。放到具体实现上,EasyExcel读取.xlsx文件时,底层解析器只关注Excel里那个被压缩的sheetXML文件流,每次解析出一个单元格的数据,立刻通过分析器通知Listener回调方法,当前行的数据用完即可被GC回收。

写入方向同理,EasyExcel采用分批写入、即时刷盘的策略。拿到一个待导出的对象列表后,它并不是把所有数据先翻译成内存里的单元格对象再整体落盘,而是每攒够一定批量的数据就写成一个小数据块,内存水位始终维持在较低水平。官方做过的压测里,用EasyExcel写一百万个数据行的内存占用比POI低一个数量级,这个提升在实际生产环境里是完全能感知到的。可能有人会觉得,这个原理听懂了,但跟我写代码有什么关系?关系很大,理解了推导过程你就能明白为什么读取大数据量文件必须用Listener模式、为什么写文件要控制批量大小,后面各个坑的解决思路全都是从这个核心逻辑延伸出来的。

3. 上手实操:依赖引入、快速写入与注解模型

3.1 Maven依赖引入与版本选择

EasyExcel的坐标是com.alibaba:easyexcel,在Maven中央仓库直接可用。我这几年用下来,建议直接上3.1.x之后的新版本。3.0以前的包名和API设计跟现在差异比较大,网上很多老教程里的“读Excel用AnalysisEventListener、写Excel用ExcelWriter”那套写法在新版本里虽然思路还在,但很多类名、方法签名已经变了,对着老教程写新版本代码会处处碰壁。

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

这个依赖会传递引入POI的3.17版本等底层库,一般情况下不需要自己再额外声明POI坐标。需要注意的是,如果项目里其他地方已经引入了POI,要留意版本冲突,最好统一对齐到EasyExcel传递进来的版本,我遇到过因为手动把POI升级到4.x导致EasyExcel底层API签名不匹配的情况,报错非常奇怪,排查起来很费劲,后来统一回归到依赖树里那一版就正常了。

3.2 用注解定义Excel模型

EasyExcel最舒服的用法是注解模型映射。你定义一个POJO,字段上加几个注解,写代码时把对象列表直接扔给EasyExcel,列名、列顺序、单元格样式它全都自己处理。我日常用的注解大概就这几个:@ExcelProperty、@ColumnWidth、@ContentRowHeight、@HeadRowHeight、@DateTimeFormat、@NumberFormat。

@Data public class UserExportVO { @ExcelProperty("用户ID") private Long userId; @ExcelProperty("用户名") private String username; @ExcelProperty(value = "年龄", order = 3) private Integer age; @ColumnWidth(20) @ExcelProperty(value = "创建时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private Date createTime; @ColumnWidth(15) @ExcelProperty("账户状态") private String statusDesc; }

这里有两个细节值得注意。第一个,@ExcelProperty里的value就是表头名称,不传value的话会用字段名当表头,但Java的驼峰字段名直接当表头很难看,所以我一般都会显式定义。第二个,字段顺序默认就是表头列的顺序,写代码时不需要刻意调整字段声明顺序的话,也可以通过order属性控制,负数可以从最右边开始排,这个用起来很灵活。

3.3 一行代码导出Excel文件

模型定义好之后,导出写起来简单到让人怀疑是不是漏了什么。核心API就是EasyExcel.write()方法链,指定文件路径、实体类型、Sheet名,然后调用sheet()和doWrite()传数据。

List<UserExportVO> userList = userService.listExportData(); String fileName = "/tmp/user_export.xlsx"; EasyExcel.write(fileName, UserExportVO.class) .sheet("用户列表") .doWrite(userList);

就这四行,一个带表头、带数据的Excel就生成好了。如果数据量特别大,几十万条那种,建议分批写,避免一次性把全量List堆在内存里。分批写法也很简单,用ExcelWriter配合WriteSheet循环写入:

String fileName = "/tmp/big_user_export.xlsx"; ExcelWriter excelWriter = EasyExcel.write(fileName, UserExportVO.class).build(); WriteSheet writeSheet = EasyExcel.writerSheet("用户列表").build(); // 每1万条写一次 for (List<UserExportVO> batch : batchListProvider()) { excelWriter.write(batch, writeSheet); } excelWriter.finish();

有人可能会问,如果我不想定义VO类,只想动态生成一批列怎么办?EasyExcel也支持免模型的方式,直接在writerSheet()方法里传入表头List,数据用List<List<String>>表示,代码大概长这样:

List<List<String>> head = Arrays.asList( Arrays.asList("姓名"), Arrays.asList("年龄") ); List<List<String>> rows = new ArrayList<>(); rows.add(Arrays.asList("张三", "25")); EasyExcel.write("/tmp/dynamic.xlsx") .head(head) .sheet("动态数据") .doWrite(rows);

这种动态头写法适用于列结构不固定的场景,比如用户自定义导出字段的报表系统。缺点也很明显,没有类型转换能力,日期、数字全都是字符串输出,并且无法使用注解提供的格式化能力,所以能用模型映射还是优先用模型映射。

3.4 写入场景的经验心得

用EasyExcel写文件,我摸索出几条实操经验:

  • 导出的文件流直接响应给前端下载时,不要用FileOutputStream,用ServletOutputStream传给EasyExcel.write()更稳妥,文件不必落地。但要注意设置响应头Content-Disposition,并且文件名做URL编码,否则浏览器下载中文文件名会乱码。
  • 导出的Excel在Windows下打开偶尔会提醒“文件已损坏”,排查过几次,发现是文件名里带了冒号、问号等非法字符,或者文件路径中文编码出了问题。Windows对文件名校验很严格,生成文件名时最好过滤掉\ / : * ? " < > |这几个字符。
  • doWrite()之后不需要手动关闭流,EasyExcel内部已经处理了流的关闭。但用ExcelWriter手动控制批量的场景,一定记得调用finish(),否则文件写不完整,这个坑我踩过一次,排查了半天才发现是没调finish导致文件缺失尾部数据。

4. 读取进阶:大数据量场景下的Listener模式

4.1 默认read方法的内存隐患

很多人第一次用EasyExcel的EasyExcel.read()方法时,容易照葫芦画瓢地写出这样的代码:

List<UserImportVO> list = new ArrayList<>(); EasyExcel.read("/tmp/user_import.xlsx", UserImportVO.class, new AnalysisEventListener<UserImportVO>() { @Override public void invoke(UserImportVO data, AnalysisContext context) { list.add(data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 全部解析完成 } }).sheet().doRead();

这样写小白也能跑通,小文件完全没问题。但注意,核心设计逻辑是流式解析,invoke方法每解析一行就会回调一次,如果你只是把每一行都塞进ArrayList,其实相当于把整个Excel数据又从流式模式搬回了全量内存模式。文件小无所谓,一旦到了几十万行甚至上百万行,内存压力立刻回来了。

4.2 用Listener配合分批落库

正确姿势是让invoke方法像流水线上的工人一样,每接到一批货就处理掉、清空暂存区,不让数据积压。最常见的用法就是分批写入数据库,攒到一定数量批量插入,然后清掉List,让GC回收掉这部分对象。

public class UserImportListener extends AnalysisEventListener<UserImportVO> { private static final int BATCH_COUNT = 500; private final List<UserImportVO> cachedList = new ArrayList<>(BATCH_COUNT); private final UserService userService; public UserImportListener(UserService userService) { this.userService = userService; } @Override public void invoke(UserImportVO data, AnalysisContext context) { cachedList.add(data); if (cachedList.size() >= BATCH_COUNT) { saveBatch(); cachedList.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 最后不足一批的数据也要处理 if (!cachedList.isEmpty()) { saveBatch(); } } private void saveBatch() { userService.saveBatch(cachedList); } }

使用方式变成:

String fileName = "/tmp/user_import.xlsx"; EasyExcel.read(fileName, UserImportVO.class, new UserImportListener(userService)) .sheet() .doRead();

数据入库后,Excel文件本身可以立即删除,内存里同时存活的也就是最多500行数据对象,这个内存水位在任何服务器上都没压力。个人建议BATCH_COUNT不要设得过大,500到1000比较合适,数据库批量插入的一次性能和数据对象的内存占用都能兼顾。设置到5000甚至更多的话,单次插入耗时变长,而且如果库里字段多、索引多,批量SQL过大容易触发数据库的连接超时或锁等待问题。

4.3 读取时的空行、错误行与部分字段读取

实际业务文件通常不是干干净净的,可能中间夹着空行、表头下混着汇总行、某些单元格明明是数字却填成了文本。EasyExcel自然能把数据映射到VO,但空行默认是不会回调invoke的,除非你显式设置ignoreEmptyRow(false)。合并单元格、公式单元格的处理也要心里有数,公式单元格易读的是公式本身而不是计算结果,如果业务要求读取公式结果,需要在模板里把公式转成静态值再导入。

只读取Excel里特定几列,也是常见需求。模型类里不加注解的字段,或者用index属性指定想读取的列号,就能实现按列抽取:

@Data public class UserPartialVO { @ExcelProperty(index = 1) private String username; @ExcelProperty(index = 3) private Date createTime; }

这样即使原Excel有二十列,也只解析第2列和第4列的数据,解析速度更快,模型也更清爽。但这里有个隐患:index写死之后,如果上游模板列结构调整过,数据就会串列,所以使用前一定要确认模板列的稳定性,或者在导入时加上列头校验逻辑。

4.4 读取方向的经验总结

数据导入这件事,线上最容易出问题的其实不是EasyExcel本身,而是上游文件的不可控性。我总结了一套比较稳妥的导入处理链路:读取阶段先用EasyExcel解析成VO,简单做一行级的格式校验(比如必填字段为空、手机号正则等),收集错误行号和错误原因;校验通过的行进入批量入库阶段;导入完成后统一返回一个“成功X条、失败Y条 + 错误明细文件下载链接”的结果。这样用户拿到手的是清晰的反馈,而不是一个中途崩掉的异常堆栈。错误明细文件最好也用EasyExcel生成,用List里的失败原因和原始数据拼成一个导出VO,对用户来说查起来非常直观。

5. 高级玩法:模板填充、动态头与自定义转换

5.1 基于模板的报表填充

有些导出场景不是简单的“表头+数据行”,而是固定格式的红头文件、复杂表头的统计报表。这种场景最合适的方案是提前做好一个Excel模板,里面预留好填充的占位符,然后用EasyExcel的填充功能把数据灌进去。

模板里写{name}表示填充单个变量,写{.list}表示填充列表数据,列表填充时还有{&list}这种横向填充的用法,可以把一行数据填充到整行多个单元格中。填充代码很简洁:

String templatePath = "/templates/report_template.xlsx"; String outputPath = "/tmp/report_output.xlsx"; Map<String, Object> data = new HashMap<>(); data.put("title", "2024年度用户增长报告"); data.put("generateDate", new Date()); data.put("list", userDataList); EasyExcel.write(outputPath) .withTemplate(templatePath) .sheet() .doFill(data);

使用模板填充有个要点:模板里的占位符只能写在单元格的文本内容中,不能跨单元格。如果模板里某个单元格写成{name}{date}这种组合,填充时会被视为一个整体占位符而匹配失败。模板制作时尽量每个占位符独占一个单元格,格式也提前在模板里定好,填充出来的报表样式就是模板的样式,这样交付出来的文件维持了原本的颜值。

5.2 动态合并单元格与自定义拦截器

EasyExcel导出普通列表容易,但有合并单元格需求时就要考虑拦截器了。比如导出一份“每个用户的多条订单记录”,期望相同用户ID的单元格跨行合并,可以在写出时注册一个CellWriteHandler来动态控制合并行为。

public class UserCellMergeHandler implements CellWriteHandler { private final int[] mergeColumnIndexes; private final int mergeRowIndex; public UserCellMergeHandler(int mergeRowIndex, int... mergeColumnIndexes) { this.mergeRowIndex = mergeRowIndex; this.mergeColumnIndexes = mergeColumnIndexes; } @Override public void afterCellDispose(CellWriteHandlerContext context) { // 这里拿到当前Sheet、当前行的单元格,判断相邻行的相同列值是否一致 // 如果一致,则调用Sheet.mergeRegions扩展合并区域 } }

需要说明的是,EasyExcel的写拦截器其实是在POI的写操作上做回调钩子,因此合并区域的最终实现方式还是借助POI的addMergedRegionUnsafe或者CellRangeAddress。我对合并单元格的建议是:能不在代码里合成就别在代码里合成,数据源提前排好序、把相同分组的记录相邻排列,合并逻辑会简单很多。如果数据本身是乱序的,合并出来的表格会非常混乱,用户看到大概率会直接打回。

5.3 自定义类型转换器

注解里内置的@DateTimeFormat和@NumberFormat能应付绝大多数常见类型,但业务场景往往有着奇怪的格式需求。比如数据库里存的是时间戳Long,导出到Excel希望展示成2024-05-20 13:14:00这种标准字符串;又比如枚举类型,希望在导出时显示描述文本、导入时能把描述文本反解析成枚举值。这种场景就需要自定义Converter。

EasyExcel的Converter接口核心就两个方法:convertToJavaData和convertToExcelData。前者是把Excel单元格数据转成Java对象,用于读取场景;后者是把Java对象转成Excel单元格数据,用于写入场景。

public class StatusConverter implements Converter<StatusEnum> { @Override public Class<StatusEnum> supportJavaTypeKey() { return StatusEnum.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public StatusEnum convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return StatusEnum.fromDesc(cellData.getStringValue()); } @Override public WriteCellData<?> convertToExcelData(StatusEnum value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return new WriteCellData<>(value.getDesc()); } }

使用方式是在字段注解上加converter属性:

@ExcelProperty(value = "状态", converter = StatusConverter.class) private StatusEnum status;

这个功能很实用,导出的文件里看到的是“启用/禁用”,读回来能自动映射成Java枚举,全程不会暴露代码里的魔法值。

6. 避坑指南:高频问题与排查实录

6.1 读入数字变成科学计数法

这是实际使用中最常见的问题。Excel单元格里输入身份证号、手机号这类长数字时,如果单元格格式是“常规”,Java读取出来往往带科学计数法,比如1.234567890123456E18。解决办法有几个方向:

  • 模型字段类型不要用Long或Integer,用String接收。EasyExcel对字符串类型的单元格原样读取,能保留原始数字文本。这个方案最简单,推荐优先用。
  • 如果必须用数值类型,并且数字长度在Java的Long范围内,可以给@NumberFormat注解指定格式为#来去除小数点尾巴,但科学计数法是否彻底消失取决于具体数据。
  • 最稳妥的是在数据源头控制,要求上游导出Excel时,把长数字列设置为“文本”格式。这个问题源头堵住,后面全链路都省心。

6.2 日期字段读出来是数字

Excel内部对日期的存储机制是“日期序列号”,比如45292这种整数,单元格格式决定它显示成2024-01-01还是别的样子。EasyExcel读取时如果字段是Date类型并且没加@DateTimeFormat,在某些格式不规范的模板里可能拿到的是一个数值,需要手动转换。解决办法很简单:模型类日期字段上加@DateTimeFormat("yyyy-MM-dd HH:mm:ss"),Excel就会按照指定格式解析日期字符串。

更隐蔽的坑是,Excel单元格里的日期格式跟注解读取格式不一致,比如模板显示的是2024/01/05但你指定的是yyyy-MM-dd,这会导致解析失败或拿到null。对齐格式是这类问题唯一的根治手段,拿到模板后先确认它的显示格式再定注解。

6.3 大文件读取卡顿或OOM

首先确认是不是用了Listener模式并分批处理,如果是动态自定义表头、多Sheet场景,可能需要在doRead()之前先调用ExcelReader去扫描Sheet结构。大文件OOM还有一个常见原因是JVM堆太小,EasyExcel虽然已经做到流式解析,但文件非常大(几百MB)时还是需要一定的堆空间,建议-Xmx至少给1GB以上,这个量级压测下来一般都能稳定。

另外,如果文件是.xls后缀(Excel 2003格式),EasyExcel不支持流式读取,会退回到POI老模式,内存本身就很高。遇到这种情况,最干净的办法是让上游把文件转成.xlsx再导入,或者写一个小工具先行转换。非要直接读取的话,内存限制很难绕开。

6.4 写文件时表头重复或乱序

表头重复这个坑,多数是因为同一个模型类被用于多个Sheet的写出,而WriteSheet缓存了表头。每次构建WriteSheet尽量调用EasyExcel.writerSheet("sheet名")新建,不要复用同一个WriteSheet对象去写不同模型。表头乱序则基本是字段声明顺序导致的,虽然order属性可以控制,但最省心的做法还是直接按需要的顺序声明字段。

6.5 代码里修改了字段但测试环境还是旧表头

这是缓存问题,EasyExcel在3.x版本里自带一些模板和头相关的缓存,修改模型类后旧的编译产物或临时文件没有清理。遇到这种情况,把target目录清理掉重新编译一般就能解决。更彻底的做法是排查项目里是否引入了多个不同版本的EasyExcel依赖,依赖树里有冲突版本时,类加载器加载到的旧类会遮蔽新代码。

6.6 导出文件在Mac/WPS打开正常,Excel打不开

不同Excel实现之间的容错率差别很大。WPS对很多不规范的文件结构容忍度高,但微软Excel校验严格,这通常意味着生成的.xlsx文件结构有瑕疵。排查方向上,先确认代码里是否正确调用了finish()或doWrite()完成了收尾;再检查是否有多个Writer实例并发写同一个文件;如果用了ExcelWriter的手动批量模式,finish()之后不要再有任何写操作,否则文件流状态错乱。模板填充场景则要检查模板本身是不是用不同版本的Excel/WPS反复编辑保存过,建议用固定工具制作模板并减少二次编辑。

7. 整理的速查表:日常调参最容易踩的配置项

把常用配置梳理成一张速查表,开发时候对着查比翻文档高效得多:

场景关键配置备注
读取时忽略空行.ignoreEmptyRow(false)默认true表示直接跳过空行
自动关闭流.autoCloseStream(true)默认true,手动管理流的场景需要关掉它
读取特定Sheet.sheet("Sheet1")/.sheet(0)按名字或下标取Sheet
读首个非空Sheet.sheet()默认取第一个非空Sheet
只读取部分行.headRowNumber(1)表示前1行是表头,不参与数据处理
同步读取非流式EasyExcel.read(...).sheet().doReadSync()返回List,简单场景可用,大文件慎用
写文件是否包含表头.needHead(false)默认true,导出纯数据可关闭
模板填充.withTemplate(模板路径)配合doFill()使用
字符集.charset(StandardCharsets.UTF_8)读取csv时需要格外注意

这些配置项本质上对应的是EasyExcel的ReadWorkbook、ReadSheet、WriteWorkbook、WriteSheet这四类内部属性对象,记住这一点,遇到陌生参数时思路会更清晰:读相关找Read前缀,写相关找Write前缀。

8. 最后的实战建议

在多个生产系统里用了EasyExcel这几年,最大的感受是“简单是它的表象,节制才是它的内核”。它把操作Excel的手续费降到了最低,但真正决定稳定性的还是使用姿势:写大数据量时想清楚批量策略,读文件时时刻记住流式解析而不是全量累积,遇到异常格式时先在上游规范数据而不是在代码里硬扛。我自己现在的习惯是,任何涉及Excel导入导出的模块,模型类、Listener、工具方法都会单独抽一个包里维护,后续换字段、加格式都集中改动,不会散落在业务代码里。

如果你要把这套玩法再向前推一步,可以关注EasyExcel官方后续版本对低版本POI的升级、对大数据量写入性能的优化,也可以研究一下结合模板能力做“批量生成多个报表文件并打包下载”的场景。实际操作中我试过把Excel工具类和文件存储服务解耦,导出文件直接写到对象存储,然后给前端返回一条下载链接,几十万人并发的下载高峰期页面也就毫秒级响应,比后端转发文件流舒服得多。这个方向很值得在实际项目中尝试,希望上面的内容对你着手使用EasyExcel能有些实实在在的帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:19:31

GNU M4 1.4.19 源码编译指南:从 tar.gz 到构建工具链实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:19:11

LSTM-MLP混合模型实现工业时序拐点精准预测

简介&#xff1a;本资源是一份面向计算机、电子信息工程及数学专业本科生的时序预测实践项目&#xff0c;聚焦LSTM与MLP混合神经网络建模&#xff0c;适用于课程设计、期末大作业及毕业设计等场景。压缩包共3个文件&#xff08;2个CSV数据集、1个Python主程序&#xff09;&…

作者头像 李华
网站建设 2026/10/13 1:17:49

UESTC编译原理实验:手写词法分析与递归下降语法分析全攻略

简介&#xff1a;面向UESTC编译原理课程设计与作业场景&#xff0c;这是一份词法分析器与语法分析器的实现打包。资源共8个文件&#xff0c;压缩包仅4KB&#xff0c;以两个Python脚本为核心&#xff0c;分别承担词法扫描与语法解析&#xff1b;其余文件涵盖文法定义、符号表/变…

作者头像 李华
网站建设 2026/10/13 1:17:48

Java枚举实现状态契约:统一前后端状态管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:17:37

ROS SLAM仿真入门:Gazebo建图与导航实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:17:33

eWebEditor v8.0部署实战:旧系统改造中的富文本编辑器配置与避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华