日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者 | 京東云開發者-京東保險 孫昊語

一、引言

Excel 表格在后臺管理系統中使用非常廣泛,多用來進行批量配置、數據導出工作。在日常開發中,我們也免不了進行 Excel 數據處理。

那么,如何恰當地處理數據量龐大的 Excel 文件,避免內存溢出問題?本文將對比分析業界主流的 Excel 解析技術,并給出解決方案。

如果這是您第一次接觸 Excel 解析,建議您從第二章了解本文基礎概念;如果您已經對 POI 有所了解,請跳轉第三章閱讀本文重點內容。

二、基礎篇 - POI

說到 Excel 讀寫,就離不開這個圈子的的老大哥 ——POI。

Apache POI 是一款 Apache 軟件基金會用 JAVA 編寫的免費開源的跨平臺的 Java API,全稱 Poor Obfuscation Implementation,“簡潔版的模糊實現”。它支持我們用 Java 語言和包括 word、Excel、PowerPoint、Visio 在內的所有 Microsoft office 文檔交互,進行數據讀寫和修改操作。

(1)“糟糕” 的電子表格

在 POI 中,每種文檔都有一個與之對應的文檔格式,如 97-2003 版本的 Excel 文件(.xls),文檔格式為 HSSF——Horrible SpreadSheet Format,意為 “糟糕的電子表格格式”。雖然 Apache 幽默而謙虛地將自己的 API 冠以 “糟糕” 之名,不過這確實是一款全面而強大的 API。

以下是部分 “糟糕” 的 POI 文檔格式,包括 Excel、Word 等:

Office 文檔 對應 POI 格式
Excel (.xls) HSSF (Horrible SpreadSheet Format)
Word (.doc) HWPF (Horrible Word Processor Format)
Visio (.vsd) HDGF (Horrible DiaGram Format)
PowerPoint(.ppt) HSLF(Horrible Slide Layout Format)
(2)OOXML 簡介

微軟在 Office 2007 版本推出了基于 XML 的技術規范:Office Open XML,簡稱 OOXML。不同于老版本的二進制存儲,在新規范下,所有 Office 文檔都使用了 XML 格式書寫,并使用 ZIP 格式進行壓縮存儲,大大提升了規范性,也提高了壓縮率,縮小了文件體積,同時支持向后兼容。簡單來說,OOXML 定義了如何用一系列的 XML 文件來表示 Office 文檔。

Xlsx 文件的本質是 XML

讓我們看看一個采用 OOML 標準的 Xlsx 文件的構成。我們右鍵點擊一個 Xlsx 文件,可以發現它可以被 ZIP 解壓工具解壓(或直接修改擴展名為.zip 后解壓),這說明: Xlsx 文件是用 ZIP 格式壓縮的。解壓后,可以看到如下目錄格式:

打開其中的 “/xl” 目錄,這是這個 Excel 的主要結構信息:

其中 workbook.xml 存儲了整個 Excel 工作簿的結構,包含了幾張 sheet 表單,而每張表單結構存儲在 /wooksheets 文件夾中。styles.xml 存放單元格的格式信息,/theme 文件夾存放一些預定義的字體、顏色等數據。為了減少壓縮體積,表單中所有的字符數據被統一存放在 sharedStrings.xml 中。經過分析不難發現, Xlsx 文件的主體數據都以 XML 格式書寫。

XSSF 格式

為了支持新標準的 Office 文檔,POI 也推出了一套兼容 OOXML 標準的 API,稱作 poi-ooxml。如 Excel 2007 文件(.xlsx)對應的 POI 文檔格式為 XSSF(XML SpreadSheet Format)。

以下是部分 OOXML 文檔格式:

Office 文檔 對應 POI 格式
Excel (.xlsx) XSSF (XML SpreadSheet Format)
Word (.docx) XWPF (XML Word Processor Format)
Visio (.vsdx) XDGF (XML DiaGram Format)
PowerPoint (.pptx) XSLF (XML Slide Layout Format)
(3)UserModel

在 POI 中為我們提供了兩種解析 Excel 的模型, UserModel(用戶模型)和 EventModel(事件模型)。兩種解析模式都可以處理 Excel 文件,但解析方式、處理效率、內存占用量都不盡相同。最簡單和實用的當屬 UserModel。

UserModel & DOM 解析

用戶模型定義了如下接口:

  1. Workbook - 工作簿,對應一個 Excel 文檔。根據版本不同,有 HSSFWorkbook、XSSFWorkbook 等類。
  2. Sheet - 表單,一個 Excel 中的若干個表單,同樣有 HSSFSheet、XSSFSheet 等類。
  3. Row - 行,一個表單由若干行組成,同樣有 HSSFRow、XSSFRow 等類。
  4. Cell - 單元格,一個行由若干單元格組成,同樣有 HSSFCell、XSSFCell 等類。

可以看到,用戶模型十分貼合 Excel 用戶的習慣,易于理解,就像我們打開一個 Excel 表格一樣。同時用戶模型提供了豐富的 API,可以支持我們完成和 Excel 中一樣的操作,如創建表單、創建行、獲取表的行數、獲取行的列數、讀寫單元格的值等。

為什么 UserModel 支持我們進行如此豐富的操作?因為在 UserModel 中,Excel 中的所有 XML 節點都被解析成了一棵 DOM 樹,整棵 DOM 樹都被加載進內存,因此可以進行方便地對每個 XML 節點進行 隨機訪問。

UserModel 數據轉換

了解了用戶模型,我們就可以直接使用其 API 進行各種 Excel 操作。當然,更方便的辦法是使用用戶模型將一個 Excel 文件轉化成我們想要的 Java 數據結構,更好地進行數據處理。

我們很容易想到關系型數據庫 —— 因為二者的實質是一樣的。類比數據庫的數據表,我們的思路就有了:

  1. 將一個 Sheet 看作表頭和數據兩部分,這二者分別包含表的結構和表的數據。
  2. 對表頭(第一行),校驗表頭信息是否和實體類的定義的屬性匹配。
  3. 對數據(剩余行),從上向下遍歷每一個 Row,將每一行轉化為一個對象,每一列作為該對象的一個屬性,從而得到一個對象列表,該列表包含 Excel 中的所有數據。

接下來我們就可以按照我們的需求處理我們的數據了,如果想把操作后的數據寫回 Excel,也是一樣的邏輯。

使用 UserModel

讓我們看看如何使用 UserModel 讀取 Excel 文件。此處使用 POI 4.0.0 版本,首先引入 poi 和 poi-ooxml 依賴:

< dependency>

< groupId> org.apache.poi </ groupId>

< artifactId> poi </ artifactId>

< version> 4.0.0 </ version>

</ dependency>

< dependency>

< groupId> org.apache.poi </ groupId>

< artifactId> poi-ooxml </ artifactId>

< version> 4.0.0 </ version>

</ dependency>

我們要讀取一個簡單的 Sku 信息表,內容如下:

如何將 UserModel 的信息轉化為數據列表?

我們可以通過實現反射 + 注解的方式定義表頭到數據的映射關系,幫助我們實現 UserModel 到數據對象的轉換。實現基本思路是:① 自定義注解,在注解中定義列號,用來標注實體類的每個屬性對應在 Excel 表頭的第幾列。② 在實體類定義中,根據表結構,為每個實體類的屬性加上注解。③ 通過反射,獲取實體類的每個屬性對應在 Excel 的列號,從而到相應的列中取得該屬性的值。

以下是簡單的實現,首先準備自定義注解 ExcelCol,其中包含列號和表頭:

importjava.lang.annotation.ElementType;

importjava.lang.annotation.Retention;

importjava.lang.annotation.RetentionPolicy;

importjava.lang.annotation.Target;

@ Target({ ElementType.FIELD})

@ Retention( RetentionPolicy. RUNTIME)

public @interface ExcelCol {

/**

* 當前列數

*/

intindexdefault0;

/**

* 當前列的表頭名稱

*/

Stringheaderdefault"";

}

接下來,根據 Sku 字段定義 Sku 對象,并添加注解,列號分別為 0,1,2,并指定表頭名稱:

importlombok.Data;

importorg.shy.xlsx. annotation.ExcelCol;

@Data

publicclassSku{

@ExcelCol(index = 0, header = "sku")

privateLongid;

@ExcelCol(index = 1, header = "名稱")

privateString name;

@ExcelCol(index = 2, header = "價格")

privateDoubleprice;

}

然后,用反射獲取表頭的每一個 Field,并以列號為索引,存入 Map 中。從 Excel 的第二行開始(第一行是表頭),遍歷后面的每一行,對每一行的每個屬性,根據列號拿到對應 Cell 的值,并為數據對象賦值。根據單元格中值類型的不同,如文本 / 數字等,進行不同的處理。以下為了簡化邏輯,只對表頭出現的類型進行了處理,其他情況的處理邏輯類似。全部代碼如下:

importcom.alibaba.fastjson.JSON;

importorg.apache.commons.lang3.StringUtils;

importorg.apache.poi.ss.usermodel.*;

importorg.apache.poi.xssf.usermodel.XSSFWorkbook;

importorg.shy.domAIn.pojo.Sku;

importorg.shy.xlsx.annotation.ExcelCol;

importjava.io.FileInputStream;

importjava.lang.reflect.Field;

importjava.text.DecimalFormat;

importjava.util.ArrayList;

importjava.util.HashMap;

importjava.util.List;

importjava.util.Map;

publicclassMyUserModel{

publicstaticvoid main( String[] args) throwsException{

List< Sku> skus = parseSkus( "D:sunhaoyu8DocumentsFilesskus.xlsx");

System.out. println( JSON.toJSONString(skus));

}

publicstaticList< Sku> parseSkus( StringfilePath) throwsException{

FileInputStreamin= new FileInputStream(filePath);

Workbookwk = new XSSFWorkbook( in);

Sheetsheet = wk.getSheetAt( 0);

// 轉換成的數據列表

List< Sku> skus = new ArrayList<>;

// 獲取Sku的注解信息

Map< Integer, Field> fieldMap = new HashMap<>( 16);

for( Fieldfield : Sku. class. getDeclaredFields) {

ExcelColcol = field.getAnnotation( ExcelCol. class);

if( col== null) {

continue;

}

field.setAccessible( true);

fieldMap.put(col.index, field);

}

for(int rowNum = 1; rowNum <= sheet.getLastRowNum; rowNum++) {

Rowr = sheet.getRow(rowNum);

Skusku = new Sku;

for(int cellNum = 0; cellNum < fieldMap.size; cellNum++) {

Cellc= r.getCell(cellNum);

if( c!= null) {

setFieldValue(fieldMap. get(cellNum), getCellValue( c), sku);

}

}

skus.add(sku);

}

returnskus;

}

publicstaticvoid setFieldValue( Fieldfield, Stringvalue, Skusku) throwsException{

if(field == null) {

return;

}

//得到此屬性的類型

Stringtype = field.getType. toString;

if( StringUtils.isBlank(value)) {

field. set(sku, null);

} elseif(type.endsWith( "String")) {

field. set(sku, value);

} elseif(type.endsWith( "long") || type.endsWith( "Long")) {

field. set(sku, Long.parseLong(value));

} elseif(type.endsWith( "double") || type.endsWith( "Double")) {

field. set(sku, Double.parseDouble(value));

} else{

field. set(sku, value);

}

}

publicstaticStringgetCellValue( Cellcell) {

DecimalFormatdf = new DecimalFormat( "#.##");

if(cell == null) {

return"";

}

switch(cell.getCellType) {

caseNUMERIC:

returndf.format(cell.getNumericCellValue);

caseSTRING:

returncell.getStringCellValue.trim;

caseBLANK:

returnnull;

}

return"";

}

最后,將轉換完成的數據列表打印出來。運行結果如下:

[{ "id": 345000, "name": "電腦A", "price": 5999.0},{ "id": 345001, "name": "手機C", "price": 4599.0}]

Tips:如果您的程序出現 “NoClassDefFoundError”,請引入 ooxml-schemas 依賴:

< dependency>

< groupId> org.apache.poi </ groupId>

< artifactId> ooxml-schemas </ artifactId>

< version> 1.4 </ version>

</ dependency>

版本選擇見下表,如 POI 4.0.0 對應 ooxml-schemas 1.4 版本:

UserModel 的局限

以上處理邏輯對于大部分的 Excel 文件都很適用,但最大的缺點是內存開銷大,因為所有的數據都被加載入內存。實測,以上 3 列的 Excel 文件在 7 萬行左右就會出現 OOM,而 XLS 文件最大行數為 65535 行,XLSX 更是達到了 1048576 行,如果將幾萬甚至百萬級別的數據全部讀入內存,內存溢出風險極高。

那么,該如何解決傳統 UserModel 無法處理大批量 Excel 的問題呢?開發者們給出了許多精彩的解決方案,請看下一章。

三、進階篇 - 內存優化的探索

接下來介紹本文重點內容,同時解決本文所提出的問題: 如何進行 Excel 解析的內存優化,從而處理百萬行 Excel 文件?

(1)EventModel

前面我們提到,除了 UserModel 外,POI 還提供了另一種解析 Excel 的模型:EventModel 事件模型。不同于用戶模型的 DOM 解析,事件模型采用了 SAX 的方式去解析 Excel。

EventModel & SAX 解析

SAX 的全稱是 Simple API for XML,是一種基于事件驅動的 XML 解析方法。不同于 DOM 一次性讀入 XML,SAX 會采用邊讀取邊處理的方式進行 XML 操作。簡單來講,SAX 解析器會逐行地去掃描 XML 文檔,當遇到標簽時會觸發解析處理器,從而觸發相應的事件 Handler。我們要做的就是繼承 DefaultHandler 類,重寫一系列事件處理方法,即可對 Excel 文件進行相應的處理。

下面是一個簡單的 SAX 解析的示例,這是要解析的 XML 文件:一個 sku 表,其中包含兩個 sku 節點,每個節點有一個 id 屬性和三個子節點。

<?xml version="1.0" encoding="UTF-8"?>

< skus>

< skuid= "345000">

< name> 電腦A </ name>

< price> 5999.0 </ price>

</ sku>

< skuid= "345001">

< name> 手機C </ name>

< price> 4599.0 </ price>

</ sku>

</ skus>

對照 XML 結構,創建 Java 實體類:

importlombok.Data;

@Data

publicclassSku{

privateLongid;

privateString name;

privateDoubleprice;

}

自定義事件處理類 SkuHandler:

importcom.alibaba.fastjson.JSON;

importorg.shy.domain.pojo.Sku;

importorg.xml.sax.Attributes;

importorg.xml.sax.SAXException;

importorg.xml.sax.helpers.DefaultHandler;

publicclassSkuHandler extendsDefaultHandler {

/**

* 當前正在處理的sku

*/

privateSku sku;

/**

* 當前正在處理的節點名稱

*/

privateStringtagName;

@Override

publicvoidstartElement( Stringuri, StringlocalName, StringqName, Attributes attributes) throws SAXException {

if( "sku".equals(qName)) {

sku = newSku;

sku.setId(Long.valueOf((attributes.getValue( "id"))));

}

tagName = qName;

}

@Override

publicvoidendElement( Stringuri, StringlocalName, StringqName) throws SAXException {

if( "sku".equals(qName)) {

System.out.println( JSON.toJSONString(sku));

// 處理業務邏輯

// ...

}

tagName = null;

}

@Override

publicvoidcharacters(char[] ch, int start, int length) throws SAXException {

if( "name".equals(tagName)) {

sku.setName( newString(ch, start, length));

}

if( "price".equals(tagName)) {

sku.setPrice(Double.valueOf( newString(ch, start, length)));

}

}

}

其中,SkuHandler 重寫了三個事件響應方法:

startElement—— 每當掃描到新 XML 元素時,調用此方法,傳入 XML 標簽名稱 qName,XML 屬性列表 attributes;

characters—— 每當掃描到未在 XML 標簽中的字符串時,調用此方法,傳入字符數組、起始下標和長度;

endElement—— 每當掃描到 XML 元素的結束標簽時,調用此方法,傳入 XML 標簽名稱 qName。

我們用一個變量 tagName 存儲當前掃描到的節點信息,每次掃描節點發送變化時,更新 tagName;

用一個 Sku 實例維護當前讀入內存的 Sku 信息,每當該 Sku 讀取完成時,我們打印該 Sku 信息,并執行相應業務邏輯。這樣,就可以做到一次讀取一條 Sku 信息,邊解析邊處理。由于每行 Sku 結構相同,因此,只需要在內存維護一條 Sku 信息即可,避免了一次性把所有信息讀入內存。

調用 SAX 解析器時,使用 SAXParserFactory 創建解析器實例,解析輸入流即可,Main 方法如下:

importorg.shy.xlsx.sax.handler.SkuHandler;

importjavax.xml.parsers.SAXParser;

importjavax.xml.parsers.SAXParserFactory;

importjava.io.InputStream;

publicclassMySax{

publicstaticvoidmain(String[] args)throwsException {

parseSku;

}

publicstaticvoidparseSkuthrowsException {

SAXParserFactory saxParserFactory = SAXParserFactory.newInstance;

SAXParser saxParser = saxParserFactory.newSAXParser;

InputStream inputStream = ClassLoader.getSystemResourceAsStream( "skus.xml");

saxParser.parse(inputStream, newSkuHandler);

}

}

輸出結果如下:

{ "id": 345000, "name": "電腦A", "price": 5999.0}

{ "id": 345001, "name": "手機C", "price": 4599.0}

以上演示了 SAX 解析的基礎原理。EventModel 的 API 更復雜,同樣通過重寫 Event handler,實現 SAX 解析。有興趣的讀者,請參見 POI 官網的示例代碼: https://poi.apache.org/components/spreadsheet/how-to.html

EventModel 的局限

POI 官方提供的 EventModel API 雖然使用 SAX 方式解決了 DOM 解析的問題,但是存在一些局限性:

① 屬于 low level API,抽象級別低,相對比較復雜,學習使用成本高。

② 對于 HSSF 和 XSSF 類型的處理方式不同,代碼需要根據不同類型分別做兼容。

③ 未能完美解決內存溢出問題,內存開銷仍有優化空間。

④ 僅用于 Excel 解析,不支持 Excel 寫入。

因此,筆者 不建議使用 POI 原生的 EventModel,至于有哪些更推薦的工具,請看下文。

(2)SXSSFSXSSF 簡介

SXSSF,全稱 Streaming XML SpreadSheet Format,是 POI 3.8-beta3 版本后推出的低內存占用的流式 Excel API,旨在解決 Excel 寫入時的內存問題。它是 XSSF 的擴展,當需要將大批量數據寫入 Excel 中時,只需要用 SXSSF 替換 XSSF 即可。SXSSF 的原理是滑動窗口 —— 在內存中保存一定數量的行,其余行存儲在磁盤。這么做的好處是內存優化,代價是失去了隨機訪問的能力。SXSSF 可以兼容 XSSF 的絕大多數 API,非常適合了解 UserModel 的開發者。

內存優化會難以避免地帶來一定限制:

① 在某個時間點只能訪問有限數量的行,因為其余行并未被加載入內存。

② 不支持需要隨機訪問的 XSSF API,如刪除 / 移動行、克隆 sheet、公式計算等。

③ 不支持 Excel 讀取操作。

④ 正因為它是 XSSF 的擴展,所以不支持寫入 Xls 文件。

UserModel、EventModel、SXSSF 對比

到這里就介紹完了所有的 POI Excel API,下表是所有這些 API 的功能對比,來自 POI 官網:

可以看到,UserModel 基于 DOM 解析,功能是最齊全的,支持隨機訪問,唯一缺點是 CPU 和內存效率不穩定;

EventModel 是 POI 提供的流式讀取方案,基于 SAX 解析,僅支持向前訪問,其余 API 不支持;

SXSSF 是 POI 提供的流式寫入方案,同樣僅能向前訪問,支持部分 XSSF API。

(3)EasyExcelEasyExcel 簡介

為了解決 POI 原生的 SAX 解析的問題,阿里基于 POI 二次開發了 EasyExcel。下面是引用自 EasyExcel 官網的介紹:

Java 解析、生成 Excel 比較有名的框架有 Apache poi、jxl。但他們都存在一個嚴重的問題就是非常的耗內存,poi 有一套 SAX 模式的 API 可以一定程度的解決一些內存溢出的問題,但 POI 還是有一些缺陷,比如 07 版 Excel 解壓縮以及解壓后存儲都是在內存中完成的,內存消耗依然很大。easyexcel 重寫了 poi 對 07 版 Excel 的解析,一個 3M 的 excel 用 POI sax 解析依然需要 100M 左右內存,改用 easyexcel 可以降低到幾 M,并且再大的 excel 也不會出現內存溢出;03 版依賴 POI 的 sax 模式,在上層做了模型轉換的封裝,讓使用者更加簡單方便。

如介紹所言,EasyExcel 同樣采用 SAX 方式解析,但由于重寫了 xlsx 的 SAX 解析,優化了內存開銷;對 xls 文件,在上層進一步進行了封裝,降低了使用成本。API 上,采用注解的方式去定義 Excel 實體類,使用方便;通過事件監聽器的方式做 Excel 讀取,相比于原生 EventModel,API 大大簡化;寫入數據時,EasyExcel 對大批數據,通過重復多次寫入的方式從而降低內存開銷。

EasyExcel 最大的優勢是使用簡便,十分鐘可以上手。由于對 POI 的 API 都做了高級封裝,所以適合不想了解 POI 基礎 API 的開發者。總之,EasyExcel 是一款值得一試的 API。

使用 EasyExcel

引入 easyexcel 依賴:

< dependency>

< groupId> com.alibaba </ groupId>

< artifactId> easyexcel </ artifactId>

< version> 2.2.3 </ version>

</ dependency>

首先,用注解定義 Excel 實體類:

importcom.alibaba.excel. annotation.ExcelProperty;

importlombok.Data;

@Data

publicclassSku{

@ExcelProperty(index = 0)

privateLongid;

@ExcelProperty(index = 1)

privateString name;

@ExcelProperty(index = 2)

privateDoubleprice;

}

接下來,重寫 AnalysisEventListener 中的 invoke 和 doAfterAllAnalysed 方法,這兩個方法分別在監聽到單行解析完成的事件時和全部解析完成的事件時調用。每次單行解析完成時,我們打印解析結果,代碼如下:

importcom.alibaba.excel.EasyExcel;

importcom.alibaba.excel.context.AnalysisContext;

importcom.alibaba.excel.event.AnalysisEventListener;

importcom.alibaba.fastjson.JSON;

importorg.shy.domain.pojo.easyexcel.Sku;

publicclassMyEasyExcel{

publicstaticvoidmain(String[] args){

parseSku;

}

publicstaticvoidparseSku{

//讀取文件路徑

String fileName = "D:sunhaoyu8DocumentsFilesexcel.xlsx";

//讀取excel

EasyExcel.read(fileName, Sku . class, newAnalysisEventListener< Sku> {

@Override

publicvoidinvoke(Sku sku, AnalysisContext analysisContext){

System.out.println( "第"+ analysisContext.getCurrentRowNum + "行:"+ JSON.toJSONString(sku));

}

@Override

publicvoiddoAfterAllAnalysed(AnalysisContext analysisContext){

System.out.println( "全部解析完成");

}

}).sheet.doRead;

}

}

測驗一下,用它解析一個十萬行的 excel,該文件用 UserModel 讀取會 OOM,如下:

運行結果:

(4)Xlsx-streamerXlsx-streamer 簡介

Xlsx-streamer 是一款用于流式讀取 Excel 的工具,同樣基于 POI 二次開發。雖然 EasyExcel 可以很好地解決 Excel 讀取的問題,但解析方式為 SAX,需要通過實現監聽器以事件驅動的方式進行解析。有沒有其他的解析方式呢?Xlsx-streamer 給出了答案。

譯自官方文檔的描述:

如果您過去曾使用 Apache POI 讀取 Excel 文件,您可能會注意到它的內存效率不是很高。閱讀整個工作簿會導致嚴重的內存使用高峰,這會對服務器造成嚴重破壞。Apache 必須讀取整個工作簿的原因有很多,但其中大部分與該庫允許您使用隨機地址進行讀寫有關。如果(且僅當)您只想以快速且內存高效的方式讀取 Excel 文件的內容,您可能不需要此功能。不幸的是,POI 庫中唯一用于讀取流式工作簿的東西要求您的代碼使用類似 SAX 的解析器。該 API 中缺少所有友好的類,如 Row 和 Cell。該庫充當該流式 API 的包裝器,同時保留標準 POI API 的語法。繼續閱讀,看看它是否適合您。注意:這個庫只支持讀取 XLSX 文件。

如介紹所言,Xlsx-streamer 最大的便利之處是兼容了用戶使用 POI UserModel 的習慣,它對所有的 UserModel 接口都給出了自己的流式實現,如 StreamingSheet、StreamingRow 等,對于熟悉 UserModel 的開發者來說,幾乎沒有學習門檻,可以直接使用 UserModel 訪問 Excel。

Xlsx-streamer 的實現原理和 SXSSF 相同,都是滑動窗口 —— 限定讀入內存中的數據大小,將正在解析的數據讀到內存緩沖區中,形成一個臨時文件,以防止大量使用內存。緩沖區的內容會隨著解析的過程不斷變化,當流關閉后,臨時文件也將被刪除。由于內存緩沖區的存在,整個流不會被完整地讀入內存,從而防止了內存溢出。

與 SXSSF 一樣,因為內存中僅加載入部分行,故犧牲了隨機訪問的能力,僅能通過遍歷順序訪問整表,這是不可避免的局限。換言之,如果調用 StreamingSheet.getRow (int rownum) 方法,該方法會獲取 sheet 的指定行,會拋出 “不支持該操作” 的異常。

Xlsx-streamer 最大的優勢是兼容 UserModel,尤其適合那些熟悉 UserModel 又不想使用繁瑣的 EventModel 的開發者。它和 SXSSF 一樣,都通過實現 UserModel 接口的方式給出解決內存問題的方案, 很好地填補了 SXSSF 不支持讀取的空白,可以說它是 “讀取版” 的 SXSSF。

使用 Xlsx-streamer

引入 pom 依賴:

< dependency>

< groupId> com.monitorjbl </ groupId>

< artifactId> xlsx-streamer </ artifactId>

< version> 2.1.0 </ version>

</ dependency>

下面是一個使用 xlsx-streamer 的 demo:

importcom.monitorjbl.xlsx.StreamingReader;

importorg.apache.poi.ss.usermodel.Cell;

importorg.apache.poi.ss.usermodel.Row;

importorg.apache.poi.ss.usermodel.Sheet;

importorg.apache.poi.ss.usermodel.Workbook;

importjava.io.FileInputStream;

publicclassMyXlsxStreamer{

publicstaticvoid main( String[] args) throwsException{

parseSku;

}

publicstaticvoid parseSku throwsException{

FileInputStreamin= new FileInputStream( "D:sunhaoyu8DocumentsFilesexcel.xlsx");

Workbookwk = StreamingReader.builder

//緩存到內存中的行數,默認是10

.rowCacheSize( 100)

//讀取資源時,緩存到內存的字節大小,默認是1024

.bufferSize( 4096)

//打開資源,必須,可以是InputStream或者是File

. open( in);

Sheetsheet = wk.getSheetAt( 0);

for( Rowr : sheet) {

System.out. print( "第"+ r.getRowNum + "行:");

for( Cellc: r) {

if( c!= null) {

System.out. print( c.getStringCellValue + " ");

}

}

System.out. println;

}

}

}

如代碼所示,Xlsx-streamer 的使用方法為:使用 StreamingReader 進行參數配置和流式讀取,我們可以手動配置固定的滑動窗口大小,有兩個指標,分別是緩存在內存中的最大行數和緩存在內存的最大字節數,這兩個指標會同時限制該滑動窗口的上限。接下來,我們可以使用 UserModel 的 API 去遍歷訪問讀到的表格。

使用十萬行量級的 excel 文件實測一下,運行結果:

StAX 解析

Xlsx-streamer 底層采用的解析方式,被稱作 StAX 解析。StAX 于 2004 年 3 月在 JSR 173 規范中引入,是 JDK 6.0 推出的新特性。它的全稱是 Streaming API for XML,流式 XML 解析。更準確地講,稱作 “流式拉分析”。之所以稱作拉分析,是因為它和 “流式推分析”——SAX 解析相對。

之前我們提到,SAX 解析是一種事件驅動的解析模型,每當解析到標簽時都會觸發相應的事件 Handler,將事件 “推” 給響應器。在這樣的推模型中,解析器是主動,響應器是被動,我們不能選擇想要響應哪些事件,因此這樣的解析比較不靈活。

為了解決 SAX 解析的問題,StAX 解析采用了 “拉” 的方式 —— 由解析器遍歷流時,原來的響應器變成了驅動者,主動遍歷事件解析器(迭代器),從中拉取一個個事件并處理。在解析過程中,StAX 支持使用 peek 方法來 "偷看" 下一個事件,從而決定是否有必要分析下一個事件,而不必從流中讀取事件。這樣可以有效提高靈活性和效率。

下面用 StAX 的方式再解析一下相同的 XML:

<?xml version="1.0" encoding="UTF-8"?>

< skus>

< skuid= "345000">

< name> 電腦A </ name>

< price> 5999.0 </ price>

</ sku>

< skuid= "345001">

< name> 手機C </ name>

< price> 4599.0 </ price>

</ sku>

</ skus>

這次我們不需要監聽器,把所有處理的邏輯集成在一個方法中:

importcom.alibaba.fastjson.JSON;

importorg.apache.commons.lang3.StringUtils;

importorg.shy.domain.pojo.Sku;

importjavax.xml.stream.XMLEventReader;

importjavax.xml.stream.XMLInputFactory;

importjavax.xml.stream.events.Attribute;

importjavax.xml.stream.events.StartElement;

importjavax.xml.stream.events.XMLEvent;

importjava.io.InputStream;

importjava.util.Iterator;

publicclassMyStax{

/**

* 當前正在處理的sku

*/

privatestaticSkusku;

/**

* 當前正在處理的節點名稱

*/

privatestaticStringtagName;

publicstaticvoid main( String[] args) throwsException{

parseSku;

}

publicstaticvoid parseSku throwsException{

XMLInputFactoryinputFactory = XMLInputFactory.newInstance;

InputStreaminputStream = ClassLoader.getSystemResourceAsStream( "skus.xml");

XMLEventReaderxmlEventReader = inputFactory.createXMLEventReader(inputStream);

while(xmlEventReader.hasNext) {

XMLEventevent = xmlEventReader.nextEvent;

// 開始節點

if(event.isStartElement) {

StartElementstartElement = event.asStartElement;

Stringname = startElement.getName. toString;

if( "sku".equals(name)) {

sku = new Sku;

Iteratoriterator = startElement.getAttributes;

while(iterator.hasNext) {

Attributeattribute = ( Attribute) iterator.next;

if( "id".equals(attribute.getName. toString)) {

sku.setId( Long.valueOf(attribute.getValue));

}

}

}

tagName = name;

}

// 字符

if(event.isCharacters) {

Stringdata = event.asCharacters.getData.trim;

if( StringUtils.isNotEmpty(data)) {

if( "name".equals(tagName)) {

sku.setName(data);

}

if( "price".equals(tagName)) {

sku.setPrice( Double.valueOf(data));

}

}

}

// 結束節點

if(event.isEndElement) {

Stringname = event.asEndElement.getName. toString;

if( "sku".equals(name)) {

System.out. println( JSON.toJSONString(sku));

// 處理業務邏輯

// ...

}

}

}

}

}

以上代碼與 SAX 解析的邏輯是等價的,用 XMLEventReader 作為迭代器從流中讀取事件,循環遍歷事件迭代器,再根據事件類型做分類處理。有興趣的小伙伴可以自己動手嘗試一下,探索更多 StAX 解析的細節。

四、結論

EventModel、SXSSF、EasyExcel 和 Xlsx-streamer 分別針對 UserModel 的內存占用問題給出了各自的解決方案,下面是對所有本文提到的 Excel API 的對比:

  UserModel EventModel SXSSF EasyExcel Xlsx-streamer
內存占用量 較低
全表隨機訪問
讀 Excel
讀取方式 DOM SAX -- SAX StAX
寫 Excel

建議您根據自己的使用場景選擇適合的 API:

  1. 處理大批量 Excel 文件的需求,推薦選擇 POI UserModel、EasyExcel;
  2. 讀取大批量 Excel 文件,推薦選擇 EasyExcel、Xlsx-streamer;
  3. 寫入大批量 Excel 文件,推薦選擇 SXSSF、EasyExcel。

使用以上 API,一定可以滿足關于 Excel 開發的需求。當然 Excel API 不止這些,還有許多同類型的 API,歡迎大家多多探索和創新。

分享到:
標簽:Excel
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定