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

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 | 湖北省 | 宜昌市 | xx4号 | 王** | 江苏省 | 南京市 | xx5号 | 周**
1512518776 | 广东省 | 佛山市 | xx6号 | 王** | 广东省 | 广州市 | xx7号 | 周**
1473338301 | 浙江省 | 杭州市 | xx4号 | 王** | 湖北省 | 黄冈市 | xx7号 | 周**
1484573956 | 湖北省 | 武汉市 | xx4号 | 王** | 江苏省 | 南京市 | xx9号 | 周**
1409795643 | 湖北省 | 黄冈市 | xx3号 | 王** | 江苏省 | 南京市 | xx1号 | 周**

多级表头将以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.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类型

Version 0.5.17 (2024-07-18)

  • 修复部分情况下Row#toMap抛下标越界问题(#380)

更多...