Excel操作工具(Easyexcel vs EEC)比照(一)

作者 : 开心源码 本文共8103个字,预计阅读时间需要21分钟 发布时间: 2022-05-13 共181人阅读

点击这里查看原文

本系列主要详情Easyexcel和EEC的功能并从便利性、性能、内存等多方面全面的进行评测。

1. 开始

关于Easyexcel

easyexcel是alibaba开发的快速、简单、且避免OOM的java解决Excel工具,于2018.2在github上开源。它是在Apache POI基础上包装而来,主要处理Apache POI高内存且API臃肿的诟病,easyexcel提供了比原生的POI简结很多的接口,读写Excel文件均可以一行代码完成,目前(2020.4)github上有14.2k个Star和3.7K个Fork。

引用作者总结核心原理:

  1. 文件解压、读取通过文件形式
  2. 避免将一律数据一次加载到内存(采用sax模式一行一行解析并使用观察者的模式通知解决)
  3. 抛弃不重要的数据(忽略样式,字体,宽度等数据)

点击这里查看作者原文

关于EEC

EEC是国内一个个人开发者开发并于2017.10月在github开源,EEC的底层并没有使用Apache POI包,所有的底层读写代码均由作者实现,事实上EEC仅依懒dom4j和slf4j,前者用于小文件xml读取,后者统一日志接口。

核心原理:

  1. 不缓存数据或者一些缓存
  2. 使用分片来解决较大的数据
  3. 单元格样式仅使用一个int值来保存,极大缩小内存使用
  4. 使用迭代模式读取行内容,不会将整个文件读入到内存

简单总结两个工具的不同:

  • 底层不同,easyexcel底层使用Apache POI,EEC使用IO/NIO
  • easyexcel最低支持JDK7,EEC最低支持JDK8
  • easyexcel简化了接口使得像设置样式这种基本功能非常困难,EEC默认带有便于阅读的样式也提供方法设置其它样式
  • easyexcel读取文件时忽略样式和字体也没有办法直接获取单元格的公式。
  • easyexcel对常用类型缺少支持(char, Timestamp, Time, LocalDate, LocalDateTime, LocalTime),假如实体类中有这些类型就必需为这些类型编写自己设置Converter

相比之下EEC更接近于Apache POI,而easyexcel更关注单元格的值而忽略其它不太关心的数据。

2. 写文件

2.1 一些数据

对于一些数据可以直接将内容放到数组/集合中一次写入,两个工具都能做到一行代码完成数据写入。下面展现两者的实现方式,代码中出现的defaultTestPath是文件路径事前已创立好。

easyexcel可以将文件直接写入OutputStream或者磁盘

public void test5(List<Item> data) {    EasyExcel.write(defaultTestPath.resolve("test5.xlsx").toString(), LargeData.class).sheet().doWrite(data);}

test5.xlsx

EEC同样可以写入OutputStream或者磁盘,使用writeTo方法指定输出位置

public void test6(List<Item> data) throws IOException {    new Workbook("test6").addSheet(new ListSheet<>(data)).writeTo(defaultTestPath);}

test6.xlsx

2.2 写多个worksheet页

两个工具都提供便利的方法实现多worksheet页写入,基本可以使用一行代码搞定。

easyexcel通过创立多个WriteSheet来实现

public void test7() {    EasyExcel.write(defaultTestPath.resolve("test7.xlsx").toString()).build()        .write(checks(), EasyExcel.writerSheet("帐单表").build())        .write(customers(), EasyExcel.writerSheet("用户表").build())        .write(c2CS(), EasyExcel.writerSheet("客户用户关系表").build())        .finish();}

test7.xlsx

EEC通过addSheet方法增加多个worksheet,看上去更直观更容易了解。

public void test8() throws IOException {    new Workbook("test8")        .addSheet(new ListSheet<>("帐单表", checks()))        .addSheet(new ListSheet<>("用户表", customers()))        .addSheet(new ListSheet<>("客户用户关系表", c2CS()))        .writeTo(defaultTestPath);}

test8.xlsx

2.3 大数据量

数据量较大时我们无法将数据一律装载到内存,此时需要分批写文件,好在easyexcel和EEC均支持分片解决做到边读数据边写文件。

easyexcel写大文件需要指定一个模板文件,而后调用fill方法循环写数据,需要注意假如数据量超出excel单页上限会抛异常

public void test1() {    ExcelWriter excelWriter = EasyExcel.write(defaultTestPath.resolve("Large easyexcel.xlsx").toFile())        .withTemplate(defaultTestPath.resolve("temp.xlsx").toFile()).build();    WriteSheet writeSheet = EasyExcel.writerSheet().build();    for (int j = 0; j < 100; j++) {        excelWriter.fill(data(), writeSheet);    }    excelWriter.finish();}

EEC分片写大文件时需要继承ListSheet<T>或者ListMapSheet而后重写more方法并返回批量数据

public void test2() {    new Workbook("Large EEC").addSheet(new ListSheet<LargeData>() {        int n = 0;        @Override        public List<LargeData> more() {            return n++ < 100 ? data() : null;        }    }).writeTo(defaultTestPath);}

这里的data()方法模拟取数据过程,返回List<LargeData>类型。相似如下代码

private List<LargeData> data() {    List<LargeData> list = new ArrayList<>();    for (int i = 0; i < 1000; i++) {        LargeData largeData = new LargeData();        list.add(largeData);        largeData.setStr1("str1-" + i);        largeData.setStr2("str2-" + i);        largeData.setStr3("str3-" + i);        largeData.setStr4("str4-" + i);        largeData.setStr5("str5-" + i);    }    return list}

以上是两个工具类解决大文件的不同方式,easyexcel采用push方式主动向ExcelWriter推送数据,EEC采用pull方式由工具决定何时拉取下一块数据,返回空数组或者null时表明没有更多数据,所以这里要注意控制分页参数防止出现死循环。

对于数据量巨大且使用关系型数据库的场景,EEC提供另一种方案,客户可以使用StatementSheetResultSetSheet两种方式,它们的工作方式是将SQL和参数交给EEC,EEC内部去查询并使用游标做到取一个值写一个值,省掉了将表数据转为Java实体的过程。

3. 读文件

easyexcel在读文件时使用ReadListener来监听每行数据,这样可以做到边解析文件边做业务逻辑(插库或者其它),不用把文件解析完成后再做业务逻辑,以下是解析图示:

easyexcel解析图示

EEC采用迭代模式,同样做到边解析文件边做业务逻辑,处理POI的高内存问题。

eec解析图示

从两者图示大致可以看出两者的设计与写文件时正好相反。easyexcel通过监听主动把行数据推给客户,EEC这边需要客户主动拉数据,只有当客户真正需要某行数据时才去解析它们来实现推迟读取。

3.1 easyexcel读文件

public void test3() {    EasyExcel.read(defaultTestPath.resolve("Large easyexcel.xlsx").toFile(), LargeData.class,    new AnalysisEventListener<LargeData>() {        @Override        public void invoke(LargeData data, AnalysisContext context) {            // 业务解决        }        @Override        public void doAfterAllAnalysed(AnalysisContext context) { }    }).headRowNumber(1).sheet().doRead();}

你需要实现一个ReaderListener来解决行数据。

3.2 EEC读文件

try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Large easyexcel.xlsx"))) {    reader.sheet("帐单表") // 解析指定worksheet        .flatMap(Sheet::dataRows) // 只取数据行,跳过表头        .map(row -> row.to(LargeData.class)) // 转为实体对象        .forEach(o -> {            // 业务解决        });} catch (IOException e) {    e.printStackTrace();}

EEC引入java8的stream+lambda功能,你可以像操作集合类一样来操作Excel,而不用担心OOM发生。

3.3 读取多个worksheet页

两个工具都提供方便的多worksheet读取,可以看示例

easyexcel示例

public void test9() {    ExcelReader excelReader = EasyExcel.read(defaultTestPath.resolve("test7.xlsx").toFile(), simpleListener).headRowNumber(0).build();    List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();    sheets.forEach(sheet -> {        System.out.println("----------" + sheet.getSheetName() + "-----------");        excelReader.read(sheet);    });}// 输出内容----------帐单表-----------{0=1.0, 1=100.8}{0=2.0, 1=34.2}{0=3.0, 1=983.0}----------用户表-----------{0=1001.0, 1=张三}{0=1002.0, 1=李四}----------客户用户关系表-----------{0=1.0, 1=1001.0}{0=2.0, 1=1002.0}{0=3.0, 1=1002.0}

EEC示例

public void test10() {    try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {        reader.sheets()            .peek(sheet -> System.out.println("----------" + sheet.getName() + "-----------"))            .flatMap(Sheet::rows)            .forEach(System.out::println);    } catch (IOException e) {        e.printStackTrace();    }}// 输出内容----------帐单表-----------id | total1 | 100.82 | 34.23 | 983----------用户表-----------id | name1001 | 张三1002 | 李四----------客户用户关系表-----------ch_id | cu_id1 | 10012 | 10023 | 1002

操作都还算方便,相较easyexcel来说EEC要更简单一点,假如不输出worksheet名那么一行命令即可以完成输出reader.sheets().flatMap(Sheet::rows).forEach(System.out::println);

4. EEC更多使用方式

因为EEC采用迭代模式因而可以使用JDK8的Stream一律功能,下面展现少量常用功能。

4.1 将内容转为集合

数据量小的时候可以将数据一律放入内存像下面这样

try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Large easyexcel.xlsx"))) {    List<LargeData> list = reader.sheets().flatMap(Sheet::dataRows)        .map(row -> row.to(LargeData.class)).collect(Collectors.toList());        // 保存到数据库    save(list);} catch (IOException e) {    e.printStackTrace();}

当然我们谁也无法意料文件中有多少数据量,直接转为集合可能产生OOM,此时你可以通过sheet#getDimension方法先获取Worksheet的维度再按实际情况来选择执行方式,就像下面这样:

public void test11() {    try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {        Sheet firstSheet = reader.sheet(0);        Dimension dimension = firstSheet.getDimension();        // lastRow - firstRow = 数据行的行数,不包含header        if (dimension.lastRow - dimension.firstRow > 1000) {            // 假如数据量超过1千则选择流式解决,forEach里也可以收集肯定量的实体再批量解决            firstSheet.dataRows().map(row -> row.too(Check.class)).forEach(check -> {                // TODO 业务解决            });        } else {            // 数据量小于1千则直接转为集合解决            List<Check> checks = firstSheet.dataRows().map(row -> row.to(Check.class)).collect(Collectors.toList());            // TODO 业务解决        }    } catch (IOException e) {        e.printStackTrace();    }}

4.2 取单列数据

EEC提供与JDBC相似的接口,客户可以使用row.getX(columnNumber)获取指定位置的值,对于读非规则表格或者非表格时这是非常有效的。

示例:获取”二年级学生.xlsx”中所有学生的姓名并去重

try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("二年级学生.xlsx"))) {    List<String> names = reader.sheet(0) // 只取第一个worksheet页        .dataRows()        .map(row -> row.getString("姓名")) // 只取姓名列        .distinct() // 去重        .collect(Collectors.toList());    // 业务解决} catch (IOException e) {    e.printStackTrace();}

4.3 过滤某些行

我相信很多时候都会遇到这样的需求,我们仅需要解决满足某些要求的数据而过滤掉检查失败的数据,这时候filter就派上用场了

比方我们需要打印帐单页金额大于100的记录

public void test9() {    try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {        reader.sheet("帐单表")            .dataRows()            .filter(row -> row.getDouble("total") > 100.0)            .forEach(System.out::println);    } catch (IOException e) {        e.printStackTrace();    }}// 输出结果1 | 100.83 | 983.0

4.4 其它少量亮眼功能

EEC还有少量比较亮眼的功能如高亮,水印等少量实用的功能,下面代码展现如何将低于60分的学生标红且将分数显示为不及格

public void testStyleConversion() throws IOException {    new Workbook("testStyleConversion") // 文件名        .setCreator("奈留·智库") // 作者        .setCompany("Copyright (c) 2020") // 公司名        .setWaterMark(WaterMark.of("Secret")) // 水印        .setAutoSize(true) // 自动计算列宽        .addSheet(new ListSheet<>("期末成绩", Student.randomTestData(20)            , new org.ttzero.excel.entity.Sheet.Column("学号", "id", int.class)            , new org.ttzero.excel.entity.Sheet.Column("姓名", "name", String.class)            , new org.ttzero.excel.entity.Sheet.Column("成绩", "score", int.class)                // 低于60分显示`不及格`                .setProcessor(n -> n < 60 ? "不及格" : n)                // 低于60分单元格标红                .setStyleProcessor((o, style, sst) -> {                    if ((int)o < 60) {                        style = Styles.clearFill(style) | sst.addFill(new Fill(Color.red));                    }                    return style;                })            )        )        .writeTo(defaultTestPath);}

最终生成文件如下

5. 后记

读excel时不要试图将数据转为Map类型,由于每个Map都需要保存表头和单元格值,这将极大的消耗内存。

简单总结: easyexcel和EEC两个工具都极大的简化了java操作excel,从本来Apache POI繁锁的API和高内存中解脱出来。其中easyexcel支持更低的JDK版本,而EEC使用了更灵活的设计模式,同时所有的底层代码均是独立实现,意味着它的依懒非常小。但是EEC现阶段鲜有人使用有很多BUG也就无从发现,稳固性还有待考验。

下一篇将比照两者在各数据量下的读写性能

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Excel操作工具(Easyexcel vs EEC)比照(一)

发表回复