Skip to content

A fast and lower memory excel write/read tool.一个非POI底层,支持流式处理的高效且超低内存的Excel读写工具

License

Notifications You must be signed in to change notification settings

wangguanquan/eec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

cb1cd1a · Dec 24, 2024
Nov 10, 2024
Apr 22, 2019
Dec 24, 2024
Jun 10, 2019
Dec 24, 2024
Feb 28, 2019
Dec 24, 2024
Dec 24, 2024

Repository files navigation

EEC介绍

Release License

EEC(Excel Export Core)是一款轻量且高效的Excel读写工具,它具有包体小、接入代码量少和运行时消耗资源少等优点

EEC的设计初衷是为了解决Apache POI内存高、速度慢且API臃肿的诟病。EEC的底层并不依赖POI包,所有的底层代码均自己实现,事实上EEC仅依赖dom4jslf4j,前者用于小文件xml读取,后者统一日志接口。

EEC在JVM参数-Xmx10m -Xms10m下读写100w行x29列内存使用截图,下载 eec-benchmark 项目进行性能测试

write_read 100w

使用场景

EEC是线程不安全的它不支持多线程读写,同时其为流式设计且只能顺序向后,这意味着不能通过指定行列坐标来随机读写,通常可以使用EEC来做一些日常的导入/导出功能,推荐在大数据量性能/内存要求较高的场景或者没有随机读写的场景下使用。

目前已实现worksheet类型有以下七种,也可以继承已有Worksheet来实现自定义数据源

主要功能

  1. 支持大数据量导出行数无上限,超过单个Sheet上限会自动分页
  2. 超低内存,无论是xlsx还是xls格式,大部分情况下可以在10MB以内完成十万级甚至百万级行数据读写
  3. 支持动态样式,如导出库存时将低于预警阈值的行背景标黄显示
  4. 支持一键设置斑马线,利于阅读
  5. 自适应列宽对中文更精准
  6. 采用Stream流读文件,按需加载不会将整个文件读入到内存
  7. 支持Iterator和Stream+Lambda读文件,你可以像操作集合类一样操作Excel
  8. 支持csv与excel格式相互转换

WIKI

阅读WIKI 了解更多用法

Gitee

国内用户可访问Gitee, 在Gitee提issue开发者也同样会及时回复

使用方法

pom.xml添加

<dependency>
    <groupId>org.ttzero</groupId>
    <artifactId>eec</artifactId>
    <version>${eec.version}</version>
</dependency>

示例

1. 简单导出

使用SimpleSheet简单工作表导出Excel可以不必定义对象或者Map,直接添加单元格的值即可导出。

// 准备导出数据
List<Object> rows = new ArrayList<>();
rows.add(new String[] {"列1", "列2", "列3"});
rows.add(new int[] {1, 2, 3, 4});
rows.add(new Object[] {5, new Date(), 7, null, "字母", 9, 10.1243});

new Workbook()
    .addSheet(new SimpleSheet<>(rows)) // 添加一个简单工作表
    .writeTo(Paths.get("f:/excel")); // 导出到F:/excel目录下

simple_sheet

2. 对象导出

对象数组是使用最广泛的工作表,导出时需要在对象的属性上添加注解@ExcelColumn("列名")来标识当前属性可被导出,默认情况下导出的列顺序与字段在对象中的定义顺序一致,可以通过指定colIndex来重置列顺序。

// 定义导出对象
public class Student {
    @ExcelColumn("学号")
    private int id;

    @ExcelColumn("姓名")
    private String name;
}

// 创建一个名为"一年级学生表"的excel文件
new Workbook("一年级学生表")

    // 添加"工作表"并指定导出数据,可以通过addSheet添加多个worksheet
    .addSheet(new ListSheet<>("学生信息", students))

    // 指定输出位置,如果做文件导出可以直接输出到`respone.getOutputStream()`
    .writeTo(Paths.get("f:/excel"));

3. 动态样式

动态样式和数据转换都是使用@FunctionalInterface实现,通常用于突出或高亮显示一些重要的单元格或行,下面展示如何将低下60分的成绩输出为"不合格"并将整行标为橙色

new Workbook("2021小五班期未考试成绩")
    .addSheet(new ListSheet<>("期末成绩", students
         , new Column("学号", "id", int.class)
         , new Column("姓名", "name", String.class)
         , new Column("成绩", "score", int.class, n -> (int) n < 60 ? "不合格" : n)
    ).setStyleProcessor((o, style, sst) -> 
            o.getScore() < 60 ? sst.modifyFill(style, new Fill(PatternType.solid, Color.orange)) : style)
    ).writeTo(Paths.get("f:/excel"));

期未成绩

4. 支持模板导出

TemplateSheet工作表支持xls和xlsx模板格式,使用模板工作表可以合并多个Excel文件也可以和其它工作表混用,关于模板工作表请参考3-模板导出

new Workbook()
    
    // 复制[企业名片.xls]文件的[封面]工作表
    .addSheet(new TemplateSheet(Paths.get("./template/企业名片.xls", "封面"))
    
    // 复制[商品导入模板]的第一个工作表,并添加导出数据
    .addSheet(new TemplateSheet(Paths.get("./template/商品导入模板.xlsx"))
        .setData(Entity.mock()) // 设置对象 对应占位符${*}
        // 分片拉取数据 对应占位符${list.*}
        .setData("list", (i,lastOne) -> scrollQuery(i > 0 ? ((Product)lastOne).getId() : 0))
    ).writeTo(Paths.get("f:/excel"));

5. 自适应列宽更精准

// 测试类
public static class WidthTestItem {
    @ExcelColumn(value = "整型", format = "#,##0_);[Red]-#,##0_);0_)")
    private Integer nv;
    @ExcelColumn("字符串(en)")
    private String sen;
    @ExcelColumn("字符串(中文)")
    private String scn;
    @ExcelColumn(value = "日期时间", format = "yyyy-mm-dd hh:mm:ss")
    private Timestamp iv;
}

new Workbook("Auto Width Test")
    .setAutoSize(true) // <- 自适应列宽
    .addSheet(new ListSheet<>(randomTestData()))
    .writeTo(Paths.get("f:/excel"));

自动列宽

6. 支持多级表头

EEC使用多个ExcelColumn注解来实现多级表头,名称一样的行或列将自动合并

 public static class RepeatableEntry {
    @ExcelColumn("运单号")
    private String orderNo;
    @ExcelColumn("收件地址")
    @ExcelColumn("省")
    private String rProvince;
    @ExcelColumn("收件地址")
    @ExcelColumn("市")
    private String rCity;
    @ExcelColumn("收件地址")
    @ExcelColumn("详细地址")
    private String rDetail;
    @ExcelColumn("收件人")
    private String recipient;
    @ExcelColumn("寄件地址")
    @ExcelColumn("省")
    private String sProvince;
    @ExcelColumn("寄件地址")
    @ExcelColumn("市")
    private String sCity;
    @ExcelColumn("寄件地址")
    @ExcelColumn("详细地址")
    private String sDetail;
    @ExcelColumn("寄件人")
    private String sender;
}

多行表头

7. 报表轻松制作

现在使用普通的ListSheet就可以导出漂亮的报表。示例请跳转到 WIKI

报表1 报表2

8. 支持28种预设图片样式

导出图片时添加内置样式使其更美观,关于图片样式请参考1-导出Excel#导出图片

effect

读取示例

EEC使用ExcelReader#read静态方法读文件,其支持标准Stream所以可以直接使用mapfiltercollect等JDK内置函数,读取Excel就像操作集合类一样简单,极大降低学习成本。

1. 使用Stream

try (ExcelReader reader = ExcelReader.read(Paths.get("./User.xlsx"))) {
    // 读取所有worksheet并输出
    reader.sheets().flatMap(Sheet::rows).forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

2. 读入到数组或List中

try (ExcelReader reader = ExcelReader.read(Paths.get("./User.xlsx"))) {
    List<User> users = reader.sheet(0) // 读取第1个Sheet页
        .header(6)                     // 指定第6行为表头
        .rows()                        // 读取数据行
        .map(row -> row.to(User.class))// 将每行数据转换为User对象
        .collect(Collectors.toList()); // 收集数据进行后续处理
} catch (IOException e) {
    e.printStackTrace();
}

3. 过滤和聚合

EEC支持Stream的大部分功能,以下代码使用filter来演示如何过滤平台为"iOS"的注册用户

reader.sheet(0).header(6)
    .rows()
    // 过滤平台为"iOS"的用户
    .filter(row -> "iOS".equals(row.getString("platform")))
    .map(row -> row.to(User.class))
    .collect(Collectors.toList());

4. 多级表头读取

多级表头可以使用header方法来指定表头所在的多个行号,多级表头将使用:拼接多个行单元格来组成一个聚合头

reader.sheet(0)
    .header(1, 2)    // <- 指定第1、2行均为表头
    .rows()
    .map(Row::toMap) // <- Row 转 Map
    .forEach(System.out::println);

# 输出如下数据均随机生成所以输出与示例不一致运单号 | 收件地址: | 收件地址: | 收件地址:详细地址 | 收件人 | 寄件地址: | 寄件地址: | 寄件地址:详细地址 | 寄件人
921674764 | 湖北省 | 宜昌市 | xx街4号 | ** | 江苏省 | 南京市 | xx街5号 | **
1512518776 | 广东省 | 佛山市 | xx街6号 | ** | 广东省 | 广州市 | xx街7号 | **
1473338301 | 浙江省 | 杭州市 | xx街4号 | ** | 湖北省 | 黄冈市 | xx街7号 | **
1484573956 | 湖北省 | 武汉市 | xx街4号 | ** | 江苏省 | 南京市 | xx街9号 | **
1409795643 | 湖北省 | 黄冈市 | xx街3号 | ** | 江苏省 | 南京市 | xx街1号 | **

多级表头将以A1:A2:A3这种格式进行纵向拼接,读取第6个示例中的运单数据读取结果将以运单号收件地址:省收件地址:市呈现,这样就可以解决出现两个导致错乱的问题

更多关于多表头使用方法可以参考 WIKI

xls格式支持

pom.xml添加如下依赖,添加好后即完成了xls的兼容,是的!你不需要为xls格式添加任何一行代码。

<dependency>
    <groupId>org.ttzero</groupId>
    <artifactId>eec-e3-support</artifactId>
    <version>${eec-e3-support.version}</version>
</dependency>

读取xls的方法与xlsx完全一样,外部不需要判断是哪种格式,EEC为其提供了完全一样的接口,内部会根据文件头去判断具体类型,这种方式比简单判断文件后缀准确得多。

两个工具的兼容性 参考此表

CSV与Excel格式互转

  • CSV => Excel:向Workbook中添加一个CSVSheet工作表
  • Excel => CSV:读Excel时调用saveAsCSV另存为csv格式

代码示例

// 直接保存为csv生成测试文件,对于数据量较多的场合也可以使用#more方法分批获取数据
new Workbook()
    .addSheet(createTestData())
    .saveAsCSV() // 指定输出格式为csv
    .writeTo(Paths.get("d:\\abc.csv"));

// CSV转Excel
new Workbook()
    .addSheet(new CSVSheet(Paths.get("d:\\abc.csv"))) // 添加CSVSheet并指定csv路径
    .writeTo(Paths.get("d:\\abc.xlsx"));
    
// Excel转CSV
try (ExcelReader reader = ExcelReader.read(Paths.get("d:\\abc.xlsx"))) {
    // 读取Excel使用saveAsCSV保存为CSV格式
    reader.sheet(0).saveAsCSV(Paths.get("./"));
} catch (IOException e) {
    e.printStackTrace();
}

CHANGELOG

Version 0.5.21 (2024-12-24)

  • 提升Excel转CSV时对时间类型的兼容性(#409)
  • Excel转CSV时保持Excel中的空行位置
  • 提升添加批注的便利性并开放批注的字体属性
  • 模板导出支持File和Buffer图片类型
  • 修复CSVSheet默认分割符设置为0x0的过失问题
  • 修复导出批注数量超过57344时抛异常的BUG(#404)
  • Converter方法增强,入参由String改为Row和Cell方便扩展

Version 0.5.20 (2024-11-13)

  • 新增SimpleSheet简单工作表,简化导出的数据格式
  • CSVSheetWriter新增分隔符delimiter属性
  • 提升OpenJDK8-21的兼容性

Version 0.5.19 (2024-09-22)

  • Workbook支持增加自定义属性
  • Workbook支持设置"只读"标识,设置只读后打开Excel后无法编辑
  • 删除部分已标记为过时的方法

Version 0.5.18 (2024-08-13)

  • 增加CSVSheet的兼容性, Excel转CSV支持保存BOM
  • 增加ResultSetSheet的类型兼容性
  • ListMapSheet支持泛型
  • 删除I18N相关代码降低复杂度
  • 精简BloomFilter降低复杂度,精简后仅支持String类型

更多...