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

點擊這里查看原文

本系列主要介紹Easyexcel和EEC的功能并從便利性尚猿、性能、內(nèi)存等多方面全面的進行評測。

1. 開始

關(guān)于Easyexcel

easyexcel是alibaba開發(fā)的快速奕谭、簡單涣觉、且避免OOM的java處理Excel工具,于2018.2在github上開源血柳。它是在Apache POI基礎(chǔ)上包裝而來官册,主要解決Apache POI高內(nèi)存且API臃腫的詬病,easyexcel提供了比原生的POI簡結(jié)很多的接口混驰,讀寫Excel文件均可以一行代碼完成攀隔,目前(2020.4)github上有14.2k個Star和3.7K個Fork。

引用作者總結(jié)核心原理:

  1. 文件解壓栖榨、讀取通過文件形式
  2. 避免將全部數(shù)據(jù)一次加載到內(nèi)存(采用sax模式一行一行解析并使用觀察者的模式通知處理)
  3. 拋棄不重要的數(shù)據(jù)(忽略樣式昆汹,字體,寬度等數(shù)據(jù))

點擊這里查看作者原文

關(guān)于EEC

EEC是國內(nèi)一個個人開發(fā)者開發(fā)并于2017.10月在github開源婴栽,EEC的底層并沒有使用Apache POI包满粗,所有的底層讀寫代碼均由作者實現(xiàn),事實上EEC僅依懶dom4j和slf4j愚争,前者用于小文件xml讀取映皆,后者統(tǒng)一日志接口。

核心原理:

  1. 不緩存數(shù)據(jù)或少量緩存
  2. 使用分片來處理較大的數(shù)據(jù)
  3. 單元格樣式僅使用一個int值來保存轰枝,極大縮小內(nèi)存使用
  4. 使用迭代模式讀取行內(nèi)容捅彻,不會將整個文件讀入到內(nèi)存

簡單總結(jié)兩個工具的不同:

  • 底層不同,easyexcel底層使用Apache POI鞍陨,EEC使用IO/NIO
  • easyexcel最低支持JDK7步淹,EEC最低支持JDK8
  • easyexcel簡化了接口使得像設(shè)置樣式這種基本功能非常困難,EEC默認帶有便于閱讀的樣式也提供方法設(shè)置其它樣式
  • easyexcel讀取文件時忽略樣式和字體也沒有辦法直接獲取單元格的公式诚撵。
  • easyexcel對常用類型缺少支持(char, Timestamp, Time, LocalDate, LocalDateTime, LocalTime)缭裆,如果實體類中有這些類型就必須為這些類型編寫自定義Converter

相比之下EEC更接近于Apache POI,而easyexcel更關(guān)注單元格的值而忽略其它不太關(guān)心的數(shù)據(jù)寿烟。

2. 寫文件

2.1 少量數(shù)據(jù)

對于少量數(shù)據(jù)可以直接將內(nèi)容放到數(shù)組/集合中一次寫入澈驼,兩個工具都能做到一行代碼完成數(shù)據(jù)寫入。下面展示兩者的實現(xiàn)方式筛武,代碼中出現(xiàn)的defaultTestPath是文件路徑事先已創(chuàng)建好缝其。

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頁

兩個工具都提供便利的方法實現(xiàn)多worksheet頁寫入徘六,基本可以使用一行代碼搞定内边。

easyexcel通過創(chuàng)建多個WriteSheet來實現(xiàn)

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("用戶客戶關(guān)系表").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<>("用戶客戶關(guān)系表", c2CS()))
        .writeTo(defaultTestPath);
}
test8.xlsx

2.3 大數(shù)據(jù)量

數(shù)據(jù)量較大時我們無法將數(shù)據(jù)全部裝載到內(nèi)存假残,此時需要分批寫文件缭贡,好在easyexcel和EEC均支持分片處理做到邊讀數(shù)據(jù)邊寫文件炉擅。

easyexcel寫大文件需要指定一個模板文件辉懒,然后調(diào)用fill方法循環(huán)寫數(shù)據(jù),需要注意如果數(shù)據(jù)量超出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方法并返回批量數(shù)據(jù)

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()方法模擬取數(shù)據(jù)過程谍失,返回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推送數(shù)據(jù)快鱼,EEC采用pull方式由工具決定何時拉取下一塊數(shù)據(jù)颠印,返回空數(shù)組或null時表明沒有更多數(shù)據(jù),所以這里要注意控制分頁參數(shù)防止出現(xiàn)死循環(huán)抹竹。

對于數(shù)據(jù)量巨大且使用關(guān)系型數(shù)據(jù)庫的場景线罕,EEC提供另一種方案,用戶可以使用StatementSheetResultSetSheet兩種方式窃判,它們的工作方式是將SQL和參數(shù)交給EEC钞楼,EEC內(nèi)部去查詢并使用游標做到取一個值寫一個值,省掉了將表數(shù)據(jù)轉(zhuǎn)為Java實體的過程袄琳。

3. 讀文件

easyexcel在讀文件時使用ReadListener來監(jiān)聽每行數(shù)據(jù)询件,這樣可以做到邊解析文件邊做業(yè)務(wù)邏輯(插庫或其它),不用把文件解析完成后再做業(yè)務(wù)邏輯唆樊,以下是解析圖示:

easyexcel解析圖示

EEC采用迭代模式宛琅,同樣做到邊解析文件邊做業(yè)務(wù)邏輯,解決POI的高內(nèi)存問題逗旁。


eec解析圖示

從兩者圖示大致可以看出兩者的設(shè)計與寫文件時正好相反嘿辟。easyexcel通過監(jiān)聽主動把行數(shù)據(jù)推給用戶,EEC這邊需要用戶主動拉數(shù)據(jù)痢艺,只有當用戶真正需要某行數(shù)據(jù)時才去解析它們來實現(xiàn)延遲讀取仓洼。

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) {
            // 業(yè)務(wù)處理
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) { }
    }).headRowNumber(1).sheet().doRead();
}

你需要實現(xiàn)一個ReaderListener來處理行數(shù)據(jù)。

3.2 EEC讀文件

try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Large easyexcel.xlsx"))) {
    reader.sheet("帳單表") // 解析指定worksheet
        .flatMap(Sheet::dataRows) // 只取數(shù)據(jù)行堤舒,跳過表頭
        .map(row -> row.to(LargeData.class)) // 轉(zhuǎn)為實體對象
        .forEach(o -> {
            // 業(yè)務(wù)處理
        });
} catch (IOException e) {
    e.printStackTrace();
}

EEC引入java8的stream+lambda功能色建,你可以像操作集合類一樣來操作Excel,而不用擔心OOM發(fā)生舌缤。

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);
    });
}

// 輸出內(nèi)容
----------帳單表-----------
{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=李四}
----------用戶客戶關(guān)系表-----------
{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();
    }
}

// 輸出內(nèi)容
----------帳單表-----------
id | total
1 | 100.8
2 | 34.2
3 | 983
----------客戶表-----------
id | name
1001 | 張三
1002 | 李四
----------用戶客戶關(guān)系表-----------
ch_id | cu_id
1 | 1001
2 | 1002
3 | 1002

操作都還算方便,相較easyexcel來說EEC要更簡單一點国撵,如果不輸出worksheet名那么一行命令就可以完成輸出reader.sheets().flatMap(Sheet::rows).forEach(System.out::println);

4. EEC更多使用方式

由于EEC采用迭代模式因此可以使用JDK8的Stream全部功能陵吸,下面展示一些常用功能。

4.1 將內(nèi)容轉(zhuǎn)為集合

數(shù)據(jù)量小的時候可以將數(shù)據(jù)全部放入內(nèi)存像下面這樣

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());
    
    // 保存到數(shù)據(jù)庫
    save(list);
} catch (IOException e) {
    e.printStackTrace();
}

當然我們誰也無法預料文件中有多少數(shù)據(jù)量介牙,直接轉(zhuǎn)為集合可能產(chǎn)生OOM壮虫,此時你可以通過sheet#getDimension方法先獲取Worksheet的維度再按實際情況來選擇執(zhí)行方式,就像下面這樣:

public void test11() {
    try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("test8.xlsx"))) {
        Sheet firstSheet = reader.sheet(0);

        Dimension dimension = firstSheet.getDimension();
        // lastRow - firstRow = 數(shù)據(jù)行的行數(shù),不包含header
        if (dimension.lastRow - dimension.firstRow > 1000) {
            // 如果數(shù)據(jù)量超過1千則選擇流式處理囚似,forEach里也可以收集一定量的實體再批量處理
            firstSheet.dataRows().map(row -> row.too(Check.class)).forEach(check -> {
                // TODO 業(yè)務(wù)處理
            });
        } else {
            // 數(shù)據(jù)量小于1千則直接轉(zhuǎn)為集合處理
            List<Check> checks = firstSheet.dataRows().map(row -> row.to(Check.class)).collect(Collectors.toList());
            // TODO 業(yè)務(wù)處理
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

4.2 取單列數(shù)據(jù)

EEC提供與JDBC類似的接口剩拢,用戶可以使用row.getX(columnNumber)獲取指定位置的值,對于讀非規(guī)則表格或非表格時這是非常有效的饶唤。

示例:獲取"二年級學生.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());
    // 業(yè)務(wù)處理

} catch (IOException e) {
    e.printStackTrace();
}

4.3 過濾某些行

我相信很多時候都會遇到這樣的需求徐伐,我們僅需要處理滿足某些要求的數(shù)據(jù)而過濾掉檢查失敗的數(shù)據(jù),這時候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();
    }
}

// 輸出結(jié)果
1 | 100.8
3 | 983.0

4.4 其它一些亮眼功能

EEC還有一些比較亮眼的功能如高亮募狂,水印等一些實用的功能办素,下面代碼展示如何將低于60分的學生標紅且將分數(shù)顯示為不及格

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時不要試圖將數(shù)據(jù)轉(zhuǎn)為Map類型,因為每個Map都需要保存表頭和單元格值祸穷,這將極大的消耗內(nèi)存性穿。

簡單總結(jié): easyexcel和EEC兩個工具都極大的簡化了java操作excel,從原本Apache POI繁鎖的API和高內(nèi)存中解脫出來雷滚。其中easyexcel支持更低的JDK版本季二,而EEC使用了更靈活的設(shè)計模式,同時所有的底層代碼均是獨立實現(xiàn)揭措,意味著它的依懶非常小胯舷。但是EEC現(xiàn)階段鮮有人使用有很多BUG也就無從發(fā)現(xiàn),穩(wěn)定性還有待考驗绊含。

下一篇將對比兩者在各數(shù)據(jù)量下的讀寫性能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桑嘶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子躬充,更是在濱河造成了極大的恐慌逃顶,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件充甚,死亡現(xiàn)場離奇詭異以政,居然都是意外死亡,警方通過查閱死者的電腦和手機伴找,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門盈蛮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人技矮,你說我怎么就攤上這事抖誉。” “怎么了衰倦?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵袒炉,是天一觀的道長。 經(jīng)常有香客問我樊零,道長我磁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮夺艰,結(jié)果婚禮上叛溢,老公的妹妹穿的比我還像新娘。我一直安慰自己劲适,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布厢蒜。 她就那樣靜靜地躺著霞势,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斑鸦。 梳的紋絲不亂的頭發(fā)上愕贡,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音巷屿,去河邊找鬼固以。 笑死,一個胖子當著我的面吹牛嘱巾,可吹牛的內(nèi)容都是我干的憨琳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼旬昭,長吁一口氣:“原來是場噩夢啊……” “哼篙螟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起问拘,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤遍略,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后骤坐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绪杏,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年纽绍,在試婚紗的時候發(fā)現(xiàn)自己被綠了蕾久。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡拌夏,死狀恐怖腔彰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辖佣,我是刑警寧澤霹抛,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站卷谈,受9級特大地震影響杯拐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一端逼、第九天 我趴在偏房一處隱蔽的房頂上張望朗兵。 院中可真熱鬧,春花似錦顶滩、人聲如沸余掖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盐欺。三九已至,卻和暖如春仅醇,著一層夾襖步出監(jiān)牢的瞬間冗美,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工析二, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粉洼,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓叶摄,卻偏偏與公主長得像属韧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛤吓,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355