主頁 > 企業開發 > 聊聊Excel決議:如何處理百萬行EXCEL檔案

聊聊Excel決議:如何處理百萬行EXCEL檔案

2023-07-04 08:33:01 企業開發

一、引言

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,其中包含列號和表頭:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelCol {

    /**
     * 當前列數
     */
    int index() default 0;

    /**
     * 當前列的表頭名稱
     */
    String header() default "";
}

接下來,根據Sku欄位定義Sku物件,并添加注解,列號分別為0,1,2,并指定表頭名稱:

import lombok.Data;
import org.shy.xlsx.annotation.ExcelCol;

@Data
public class Sku {

    @ExcelCol(index = 0, header = "sku")
    private Long id;

    @ExcelCol(index = 1, header = "名稱")
    private String name;

    @ExcelCol(index = 2, header = "價格")
    private Double price;
}

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

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.shy.domain.pojo.Sku;
import org.shy.xlsx.annotation.ExcelCol;

import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MyUserModel {

    public static void main(String[] args) throws Exception {
        List<Sku> skus = parseSkus("D:\sunhaoyu8\Documents\Files\skus.xlsx");
        System.out.println(JSON.toJSONString(skus));
    }

    public static List<Sku> parseSkus(String filePath) throws Exception {
        FileInputStream in = new FileInputStream(filePath);
        Workbook wk = new XSSFWorkbook(in);
        Sheet sheet = wk.getSheetAt(0);
        // 轉換成的資料串列
        List<Sku> skus = new ArrayList<>();

        // 獲取Sku的注解資訊
        Map<Integer, Field> fieldMap = new HashMap<>(16);
        for (Field field : Sku.class.getDeclaredFields()) {
            ExcelCol col = field.getAnnotation(ExcelCol.class);
            if (col == null) {
                continue;
            }
            field.setAccessible(true);
            fieldMap.put(col.index(), field);
        }

        for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
            Row r = sheet.getRow(rowNum);
            Sku sku = new Sku();
            for (int cellNum = 0; cellNum < fieldMap.size(); cellNum++) {
                Cell c = r.getCell(cellNum);
                if (c != null) {
                    setFieldValue(fieldMap.get(cellNum), getCellValue(c), sku);
                }
            }
            skus.add(sku);
        }
        return skus;
    }

    public static void setFieldValue(Field field, String value, Sku sku) throws Exception {
        if (field == null) {
            return;
        }
        //得到此屬性的型別
        String type = field.getType().toString();
        if (StringUtils.isBlank(value)) {
            field.set(sku, null);
        } else if (type.endsWith("String")) {
            field.set(sku, value);
        } else if (type.endsWith("long") || type.endsWith("Long")) {
            field.set(sku, Long.parseLong(value));
        } else if (type.endsWith("double") || type.endsWith("Double")) {
            field.set(sku, Double.parseDouble(value));
        } else {
            field.set(sku, value);
        }
    }

    public static String getCellValue(Cell cell) {
        DecimalFormat df = new DecimalFormat("#.##");
        if (cell == null) {
            return "";
        }
        switch (cell.getCellType()) {
            case NUMERIC:
                return df.format(cell.getNumericCellValue());
            case STRING:
                    return cell.getStringCellValue().trim();
            case BLANK:
                return null;
        }
        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>
    <sku id="345000">
        <name>電腦A</name>
        <price>5999.0</price>
   </sku>
    <sku id="345001">
        <name>手機C</name>
        <price>4599.0</price>
   </sku>
</skus>

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

import lombok.Data;

@Data
public class Sku {
    private Long id;
    private String name;
    private Double price;
}

自定義事件處理類SkuHandler:

import com.alibaba.fastjson.JSON;
import org.shy.domain.pojo.Sku;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class SkuHandler extends DefaultHandler {
    /**
     * 當前正在處理的sku
     */
    private Sku sku;
    /**
     * 當前正在處理的節點名稱
     */
    private String tagName;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if ("sku".equals(qName)) {
            sku = new Sku();
            sku.setId(Long.valueOf((attributes.getValue("id"))));
        }
        tagName = qName;
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("sku".equals(qName)) {
            System.out.println(JSON.toJSONString(sku));
            // 處理業務邏輯
            // ...
        }
        tagName = null;
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if ("name".equals(tagName)) {
            sku.setName(new String(ch, start, length));
        }
        if ("price".equals(tagName)) {
            sku.setPrice(Double.valueOf(new String(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方法如下:

import org.shy.xlsx.sax.handler.SkuHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.InputStream;

public class MySax {
    public static void main(String[] args) throws Exception {
        parseSku();
    }

    public static void parseSku() throws Exception {
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        SAXParser saxParser = saxParserFactory.newSAXParser();
        InputStream inputStream = ClassLoader.getSystemResourceAsStream("skus.xml");
        saxParser.parse(inputStream, new SkuHandler());
    }
}

輸出結果如下:

{"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)SXSSF

SXSSF簡介

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)EasyExcel

EasyExcel簡介

為了解決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物體類:

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class Sku {
    @ExcelProperty(index = 0)
    private Long id;

    @ExcelProperty(index = 1)
    private String name;

    @ExcelProperty(index = 2)
    private Double price;
}

接下來,重寫AnalysisEventListener中的invoke和doAfterAllAnalysed方法,這兩個方法分別在監聽到單行決議完成的事件時和全部決議完成的事件時呼叫,每次單行決議完成時,我們列印決議結果,代碼如下:

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import org.shy.domain.pojo.easyexcel.Sku;

public class MyEasyExcel {
    public static void main(String[] args) {
        parseSku();
    }

    public static void parseSku() {
        //讀取檔案路徑
        String fileName = "D:\sunhaoyu8\Documents\Files\excel.xlsx";
        //讀取excel
        EasyExcel.read(fileName, Sku.class, new AnalysisEventListener<Sku>() {
            @Override
            public void invoke(Sku sku, AnalysisContext analysisContext) {
                System.out.println("第" + analysisContext.getCurrentRowNum() + "行:" + JSON.toJSONString(sku));
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                System.out.println("全部決議完成");
            }
        }).sheet().doRead();
    }
}

測驗一下,用它決議一個十萬行的excel,該檔案用UserModel讀取會OOM,如下:

運行結果:

(4)Xlsx-streamer

Xlsx-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:

import com.monitorjbl.xlsx.StreamingReader;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;

import java.io.FileInputStream;

public class MyXlsxStreamer {
    public static void main(String[] args) throws Exception {
        parseSku();
    }

    public static void parseSku() throws Exception {
        FileInputStream in = new FileInputStream("D:\sunhaoyu8\Documents\Files\excel.xlsx");
        Workbook wk = StreamingReader.builder()
                //快取到記憶體中的行數,默認是10
                .rowCacheSize(100)
                //讀取資源時,快取到記憶體的位元組大小,默認是1024
                .bufferSize(4096)
                //打開資源,必須,可以是InputStream或者是File
                .open(in);
        Sheet sheet = wk.getSheetAt(0);

        for (Row r : sheet) {
            System.out.print("第" + r.getRowNum() + "行:");
            for (Cell c : 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>
    <sku id="345000">
        <name>電腦A</name>
        <price>5999.0</price>
   </sku>
    <sku id="345001">
        <name>手機C</name>
        <price>4599.0</price>
   </sku>
</skus>

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

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.shy.domain.pojo.Sku;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.io.InputStream;
import java.util.Iterator;


public class MyStax {

    /**
     * 當前正在處理的sku
     */
    private static Sku sku;
    /**
     * 當前正在處理的節點名稱
     */
    private static String tagName;

    public static void main(String[] args) throws Exception {
        parseSku();
    }
    
    public static void parseSku() throws Exception {
        XMLInputFactory inputFactory = XMLInputFactory.newInstance();
        InputStream inputStream = ClassLoader.getSystemResourceAsStream("skus.xml");
        XMLEventReader xmlEventReader = inputFactory.createXMLEventReader(inputStream);
        while (xmlEventReader.hasNext()) {
            XMLEvent event = xmlEventReader.nextEvent();
            // 開始節點
            if (event.isStartElement()) {
                StartElement startElement = event.asStartElement();
                String name = startElement.getName().toString();
                if ("sku".equals(name)) {
                    sku = new Sku();
                    Iterator iterator = startElement.getAttributes();
                    while (iterator.hasNext()) {
                        Attribute attribute = (Attribute) iterator.next();
                        if ("id".equals(attribute.getName().toString())) {
                            sku.setId(Long.valueOf(attribute.getValue()));
                        }
                    }
                }
                tagName = name;
            }
            // 字符
            if (event.isCharacters()) {
                String data = https://www.cnblogs.com/jingdongkeji/p/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()) {
                String name = 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,歡迎大家多多探索和創新,

頁面鏈接:

POI官網: https://poi.apache.org/

EasyExcel官網:https://easyexcel.opensource.alibaba.com

Xlsx-streamer Github: https://github.com/monitorjbl/excel-streaming-reader

作者:京東保險 孫昊宇

來源:京東云開發者社區

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/556550.html

標籤:Html/Css

上一篇:前端Vue自定義可自由滾動精美tabs選項卡標簽欄標題欄 可設定背景顏色,

下一篇:返回列表

標籤雲
其他(162041) Python(38266) JavaScript(25520) Java(18286) C(15238) 區塊鏈(8275) C#(7972) AI(7469) 爪哇(7425) MySQL(7281) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5876) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4609) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2438) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1985) HtmlCss(1985) 功能(1967) Web開發(1951) C++(1942) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1882) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 聊聊Excel決議:如何處理百萬行EXCEL檔案

    如何恰當地處理資料量龐大的Excel檔案,避免記憶體溢位問題?本文將對比分析業界主流的Excel決議技術,并給出解決方案。 ......

    uj5u.com 2023-07-04 08:33:01 more
  • 前端Vue自定義可自由滾動精美tabs選項卡標簽欄標題欄 可設定背景

    #### 前端Vue自定義可自由滾動精美tabs選項卡標簽欄標題欄 可設定背景顏色, 下載完整代碼請訪問uni-app插件市場地址:https://ext.dcloud.net.cn/plugin?id=13313 #### 效果圖如下: #### ![](https://p3-juejin.byt ......

    uj5u.com 2023-07-04 08:32:58 more
  • 1.5 撰寫自定位ShellCode彈窗

    在筆者上一篇文章中簡單的介紹了如何運用匯編語言撰寫一段彈窗代碼,雖然簡易`ShellCode`可以被正常執行,但卻存在很多問題,由于采用了硬編址的方式來呼叫相應API函式的,那么就會存在一個很大的缺陷,如果作業系統的版本不統或系統重啟過,那么基址將會發生變化,此時如果再次呼叫基址引數則會呼叫失敗,本... ......

    uj5u.com 2023-07-04 08:32:06 more
  • 1.5 撰寫自定位ShellCode彈窗

    在筆者上一篇文章中簡單的介紹了如何運用匯編語言撰寫一段彈窗代碼,雖然簡易`ShellCode`可以被正常執行,但卻存在很多問題,由于采用了硬編址的方式來呼叫相應API函式的,那么就會存在一個很大的缺陷,如果作業系統的版本不統或系統重啟過,那么基址將會發生變化,此時如果再次呼叫基址引數則會呼叫失敗,本... ......

    uj5u.com 2023-07-04 08:30:25 more
  • 前端Vue基于騰訊地圖Api實作的選擇位置組件 回傳地址名稱詳細地

    #### 前端Vue基于騰訊地圖Api實作的選擇位置組件 回傳地址名稱詳細地址經緯度資訊, 下載完整代碼請訪問uni-app插件市場地址:https://ext.dcloud.net.cn/plugin?id=13310 #### 效果圖如下: ![](https://p3-juejin.bytei ......

    uj5u.com 2023-07-03 07:45:11 more
  • 1.4 撰寫簡易ShellCode彈窗

    在前面的章節中相信讀者已經學會了使用Metasploit工具生成自己的ShellCode代碼片段了,本章將繼續深入探索關于ShellCode的相關知識體系,ShellCode 通常是指一個原始的可執行代碼的有效載荷,攻擊者通常會使用這段代碼來獲得被攻陷系統上的互動Shell的訪問權限,而現在用于描述... ......

    uj5u.com 2023-07-03 07:44:13 more
  • 1.4 撰寫簡易ShellCode彈窗

    在前面的章節中相信讀者已經學會了使用Metasploit工具生成自己的ShellCode代碼片段了,本章將繼續深入探索關于ShellCode的相關知識體系,ShellCode 通常是指一個原始的可執行代碼的有效載荷,攻擊者通常會使用這段代碼來獲得被攻陷系統上的互動Shell的訪問權限,而現在用于描述... ......

    uj5u.com 2023-07-03 07:43:04 more
  • css學習(一)

    ### css引入 1. 行內樣式 ```css 我是div元素 ``` 2.內部樣式 ```css ``` 3. 外部樣式 ```css /* 可以通過@import引入其他的css資源 */ @import url(./style.css); @import url(./test.css); . ......

    uj5u.com 2023-07-02 08:01:55 more
  • 什么是 CSR、SSR、SSG、ISR - 渲染模式詳解

    本文以 `React`、`Vue` 為例,介紹下主流的渲染模式以及在主流框架中如何實作上述的渲染模式。 ## 前置知識介紹 看渲染模式之前我們先看下幾個主流框架所提供的相關能力,了解的可跳到下個章節。 ### 掛載組件到 DOM 節點 這是主流框架最基本的能力,就是將組件渲染到指定的 `DOM` 節 ......

    uj5u.com 2023-07-02 07:56:36 more
  • vue中封裝服務器地址/介面與設定請求頭

    1. 設定請求頭 首先創建一個放置服務器地址的js,如http.js,然后在http.js中引入axios `import axios from "axios";` 如果沒有axios,需要先安裝,npm i axios或者yarn add axois,然后重啟服務器 ...直接上代碼 點擊查看代碼 ......

    uj5u.com 2023-07-02 07:56:27 more