POI與EasyExcel 狂神說


1.應用場景

  1. 將用戶信息導出為excel表格
  2. 講Excel表中的信息錄入到網站數據庫,大大減小網站數據的錄入量!

開發中經常會涉及到excel的處理,如導出Excel到數據庫中!

操作Excel目前比較流行的就是Apache POI和阿里巴巴的easyExcel

2.Apache POI

簡介

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

在這里插入圖片描述
HSSF 對應 Excel 03 版本,最多支持65535行

XSSF對應 Excel 07 版本,行數無限制

缺點:

  • 使用比較麻煩
  • 數據量大的時候會可能報OOM異常

項目准備

創建maven項目,作為父項目,去掉src目錄
在這里插入圖片描述
創建module模塊,也是maven項目poi-study
在這里插入圖片描述
導入依賴

<dependencies>
    <!--xLs(03)-->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>4.1.2</version>
    </dependency>
    <!--xLsx(07)-->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.9</version>
    </dependency>
    <!--日期格式化工具-->
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.10.1</version>
    </dependency>
    <!--test-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

創建兩個版本的Excel文件
在這里插入圖片描述
打開可以看到,03版最多支持到65536行,而07版不受限制,理論上無限
在這里插入圖片描述
二者文件名后綴不同,對應操作的Java工具類也不同

明確幾個概念,工作簿、工作表、行、單元格,分別對應了各自的對象

在這里插入圖片描述

代碼演示

寫測試,創建類

public class ExcelWriteTest {
    // 構建路徑
    String PATH = "F:\\WorkSpace\\IDEA\\Test\\excel-study\\";
    @Test
    public void testWrite03() throws Exception {
        // 創建工作簿
        Workbook workbook = new HSSFWorkbook();
        // 創建工作表
        Sheet sheet = workbook.createSheet("考核成績表");
        // 創建第一行
        Row row1 = sheet.createRow(0);// 第一行
        // 創建單元格
        Cell cell1 = row1.createCell(0);// 第一行的第一列
        cell1.setCellValue("數學");
        Cell cell2 = row1.createCell(1);
        cell2.setCellValue(100);
        // 第二行
        Row row2 = sheet.createRow(1);// 第一行
        Cell cell21 = row2.createCell(0);// 第一行的第一列
        cell21.setCellValue("時間");
        Cell cell22 = row2.createCell(1);
        cell22.setCellValue(new DateTime().toString("yyyy-MM-dd HH:mm:ss"));
        // 生成表,IO流,03版本使用xls后綴
        FileOutputStream fileOutputStream = new FileOutputStream(PATH+"考核成績表03.xls");
        workbook.write(fileOutputStream);
        // 關閉流
        fileOutputStream.close();
        System.out.println("考核成績表03輸出完畢");
    }
    @Test
    public void testWrite07() throws Exception {
        // 創建工作簿
        Workbook workbook = new XSSFWorkbook();
        // 創建工作表
        Sheet sheet = workbook.createSheet("考核成績表");
        // 創建第一行
        Row row1 = sheet.createRow(0);// 第一行
        // 創建單元格
        Cell cell1 = row1.createCell(0);// 第一行的第一列
        cell1.setCellValue("語文");
        Cell cell2 = row1.createCell(1);
        cell2.setCellValue(100);
        // 第二行
        Row row2 = sheet.createRow(1);// 第一行
        Cell cell21 = row2.createCell(0);// 第一行的第一列
        cell21.setCellValue("時間");
        Cell cell22 = row2.createCell(1);
        cell22.setCellValue(new DateTime().toString("yyyy-MM-dd HH:mm:ss"));
        // 生成表,IO流,07版本使用xlsx后綴
        FileOutputStream fileOutputStream = new FileOutputStream(PATH+"考核成績表07.xlsx");
        workbook.write(fileOutputStream);
        // 關閉流
        fileOutputStream.close();
        System.out.println("考核成績表07輸出完畢");
    }
}

testWrite07 運行報異常,為什么呢??
在這里插入圖片描述

大文件寫HSSF-03

缺點:最多只能處理65536行,否則會報異常

java.lang.IllegalArgumentException:Invalid row number (65536) outside allowable range (0.. 65535)

優點:過程中寫入緩存,不操作磁盤,最后一次性寫入磁盤,速度快

    @Test
    public void testwrite03BigData() throws IOException {
        //時間
        long begin = System.currentTimeMillis();
        //創建一個薄
        Workbook workbook = new HSSFWorkbook();
        //創建表
        Sheet sheet = workbook.createSheet();
        //寫入數據
        for (int rowNum = 0; rowNum < 65536; rowNum++) {
            Row row = sheet.createRow(rowNum);
            for (int cellNum = 0; cellNum < 10; cellNum++) {
                Cell cell = row.createCell(cellNum);
                cell.setCellValue(cellNum);
            }
        }
        FileOutputStream fos = new FileOutputStream(PATH + "03版本Excel大量數據測試.xls");
        workbook.write(fos);
        fos.close();
        System.out.println("over");
        long end = System.currentTimeMillis();
        System.out.println((double) (end - begin) / 1000);
    }

運行結果
在這里插入圖片描述

在這里插入圖片描述
第二次運行更快
在這里插入圖片描述

大文件寫XSSF-07

  • 缺點:寫數據時速度非常慢,非常耗內存,也會發生內存溢出,如100萬條數據
  • 優點:可以寫較大的數據量,如20萬條數據

只做一下修改

Workbook workbook = new XSSFWorkbook();
Fileoutputstream ops = new Fileoutputstream(PATH +"07版本Excel大量數據測試.xlsx");

時間較長,但是可以寫更多數據

大文件寫SXSSF-07升級版

優點:可以寫非常大量的數據庫,如100萬條甚至更多條,寫數據速度快,占用更少的內存

注意:

  • 過程中會產生臨時文件,需要在程序運行結束后清理臨時文件
  • 默認由100條記錄被保存在內存中,如果超出這數量,則最前面的數據被寫入臨時文件
  • 如果想自定義內存中數據的數量,可以使用new SXSSFWorkbook(數量)

修改為

Workbook workbook = new SXSSFWorkbook();
Fileoutputstream ops = new Fileoutputstream(PATH +"07版本Excel大量數據測試.xlsx");

    @Test
    public void testwrite07_S_BigData() throws IOException {
        //時間
        long begin = System.currentTimeMillis();
        //創建一個薄
        Workbook workbook = new SXSSFWorkbook(100);
        //創建表
        Sheet sheet = workbook.createSheet();
        //寫入數據
        for (int rowNum = 0; rowNum < 65536; rowNum++) {
            Row row = sheet.createRow(rowNum);
            for (int cellNum = 0; cellNum < 10; cellNum++) {
                Cell cell = row.createCell(cellNum);
                cell.setCellValue(cellNum);
            }
        }
        FileOutputStream fos = new FileOutputStream(PATH + "07_S_版本Excel大量數據測試.xlsx");
        workbook.write(fos);
        fos.close();
        //清除臨時緩存
        ((SXSSFWorkbook)workbook).dispose();
        System.out.println("over");
        long end = System.currentTimeMillis();
        System.out.println((double) (end - begin) / 1000);
    }


SXSSWorkbook 來自官方解釋:實現:BigGridDemo策略的流式XSSFWorkbook版本。這允許寫入非常大的文件而不會耗盡內存,因為任何時候只有可配置的行部分被保存在內存中。

POI-Excel讀

03類型

	@Test
    public void testRead03() throws Exception {
        //獲取文件流
        FileInputStream fis = new FileInputStream(PATH + "03版本測試.xls");
        //1、創建一個工作簿。使用 exceL能操作的這邊他都可以操作!
        Workbook workbook = new HSSFWorkbook(fis);
        //2、得到表
        Sheet sheet = workbook.getSheetAt(0);
        //3、得到行
        Row row = sheet.getRow(0);
        //4、得到列
        Cell cell = row.getCell(1);

        //讀取值的時候,一定要注意類型!
        //getStringCellValue 字符串類型
        System.out.println(cell.getNumericCellValue());
        fis.close();
    }

07類型

    @Test
    public void testRead07() throws Exception {
        //獲取文件流
        FileInputStream fis = new FileInputStream(PATH + "07版本測試.xlsx");
        //1、創建一個工作簿。使用 exceL能操作的這邊他都可以操作!
        Workbook workbook = new XSSFWorkbook(fis);
        //2、得到表
        Sheet sheet = workbook.getSheetAt(0);
        //3、得到行
        Row row = sheet.getRow(0);
        //4、得到列
        Cell cell = row.getCell(0);

        //讀取值的時候,一定要注意類型!
        //getStringCellValue 字符串類型
        System.out.println(cell.getStringCellValue());
        fis.close();
    }

注意獲取值的類型即可

讀取不同的數據類型,是工作上的重點,這段類型匹配代碼工作時直接復制

判斷不同的數據類型

    @Test
    public void testCellType() throws Exception {

        //獲取文件流
        FileInputStream fis = new FileInputStream(PATH +"課題信息表20190701.xlsx");

        //創建一個工作簿。使用 excel能操作的這邊他都可以操作
        Workbook workbook = new XSSFWorkbook(fis);
        Sheet sheet = workbook.getSheetAt(0);

        //獲取標題內容
        Row rowTitle = sheet.getRow(0);
        if (rowTitle != null) {
            //得到一行有多少列有數據
            int cellCount = rowTitle.getPhysicalNumberOfCells();
            for (int cellNum = 0; cellNum < cellCount; cellNum++) {
                Cell cell = rowTitle.getCell(cellNum);
                if (cell != null) {
                    int cellType = cell.getCellType();
                    String cellValue = cell.getStringCellValue();
                    System.out.print(cellValue + "|");
                }
            }
            System.out.println();
        }

        //獲取表中的內容
        //獲取表中有多少行有數據
        int rowCount = sheet.getPhysicalNumberOfRows();
        for (int rowNum = 1; rowNum < rowCount; rowNum++) {
            Row rowData = sheet.getRow(rowNum);
            if (rowData != null) {
                //讀取列
                int cellCount = rowTitle.getPhysicalNumberOfCells();
                for (int cellNum = 0; cellNum < cellCount; cellNum++) {
                    System.out.println("[" + (rowNum + 1) + "-" + (cellNum + 1) + "]");

                    Cell cell = rowData.getCell(cellNum);
                    //匹配列的數據類型
                    if (cell != null) {
                        int cellType = cell.getCellType();
                        String cellValue = "";

                        switch (cellType) {
                            case HSSFCell.CELL_TYPE_STRING://字符
                                System.out.print("【 String】");
                                cellValue = cell.getStringCellValue();
                                break;
                            case HSSFCell.CELL_TYPE_BOOLEAN://布爾
                                System.out.print("【 BOOLEAN】");
                                cellValue = String.valueOf(cell.getBooleanCellValue());
                                break;
                            case HSSFCell.CELL_TYPE_BLANK://空
                                System.out.print("【 BLANK】");
                                break;
                            case HSSFCell.CELL_TYPE_NUMERIC://數字(日期、普通數字)
                                System.out.print("【 NUMERIC】");
                                if (HSSFDateUtil.isCellDateFormatted(cell)) {// 日期
                                    System.out.print("--【日期】");
                                    Date date = cell.getDateCellValue();
                                    cellValue = new DateTime(date).toString("yyyy-MM-dd");
                                } else {
                                    //不是日期格式,防止數字過長!
                                    System.out.print("--【轉換為字符串輸出】");
                                    cell.setCellType(HSSFCell.CELL_TYPE_STRING);
                                    cellValue = cell.toString();
                                }
                                break;
                            case HSSFCell.CELL_TYPE_ERROR://錯誤
                                System.out.print("【 數據類型錯誤】");
                                break;
                        }
                        System.out.println(cellValue);
                    }
                }
            }
        }
        //關閉流
        fis.close();
    }

計算公式

    @Test
    public void testFormula() throws Exception {
        FileInputStream fis = new FileInputStream(PATH+"公式.xls");
        //創建一個工作簿。使用 excel能操作的這邊他都可以操作
        Workbook workbook = new HSSFWorkbook(fis);
        Sheet sheet = workbook.getSheetAt(0);

        Row row = sheet.getRow(4);
        Cell cell = row.getCell(0);

        //拿到計算公司 evaL
        FormulaEvaluator FormulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook);

        //輸出單元格的內容
        int cellType = cell.getCellType();
        switch (cellType) {
            case Cell.CELL_TYPE_FORMULA://公式
                String formula = cell.getCellFormula();
                System.out.println(formula);

                //計算
                CellValue evaluate = FormulaEvaluator.evaluate(cell);
                String cellValue = evaluate.formatAsString();
                System.out.println(cellValue);
                break;
        }
    }

運行結果

SUM(A2:A4)
600.0

3.easyExcel

簡介

easyExcel官網地址: https://github.com/alibaba/easyexcel
在這里插入圖片描述
EasyExcel是阿里巴巴開源的一個excel處理框架,以使用簡單,節約內存著稱。

EasyExcel能大量減少占用內存的主要原因是在解析Excel時沒有將文件數據一次性全部加載到內存中,而是從一個磁盤上一行行讀取數據,逐個解析。

下圖是EasyExcel和POI在解析Excel時的對比圖(時間與空間的相互取舍)。

在這里插入圖片描述
官方文檔: https://www.yuque.com/easyexcel/doc/easyexcel

項目准備

添加依賴

	<!--easyexcel-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version >2.2.0-beta2</version>
        </dependency>

由於easyexcel依賴中包含POI相關依賴,有可能發生沖突,所以注釋掉

    <!--導入依賴-->
    <dependencies>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version >1.2.62</version>
        </dependency>

        <!--easyexcel-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version >2.2.0-beta2</version>
        </dependency>
    <!--lombok-->
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version >1.18.12</version>
	</dependency>

<!--        &lt;!&ndash;xLs(03)&ndash;&gt;
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.9</version>
        </dependency>
        &lt;!&ndash;xLsx(07)&ndash;&gt;
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.9</version>
        </dependency>-->
        <!--日期格式化工具-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.1</version>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

實體類

@Data
public class DemoData {
    @ExcelProperty("字符串標題")
    private String string;
    @ExcelProperty("日期標題")
    private Date date;
    @ExcelProperty("數字標題")
    private Double doubleData;
    //忽略這個字段
    @ExcelIgnore
    private String ignore;
}

EasyExcel寫入操作

package com.kuang.easy;

import com.alibaba.excel.EasyExcel;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class EasyTest {
    String PATH = "ExcelCreate\\";

    //模擬寫入數據
    private List<DemoData> data() {
        java.util.List<DemoData> list = new ArrayList<DemoData>();
        for (int i = 0; i < 10; i++) {
            DemoData data = new DemoData();
            data.setString("字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56);
            list.add(data);
        }
        return list;
    }

    //根據ist寫 excel
    @Test
    public void simplewrite() {
        String fileName = PATH + "EasyTest.xlsx";
        //這里需要指定寫用哪個 class去寫,然后寫到第一個 sheet,名字為模板然后文件流會自動關閉
        //write(fileName,格式類)
        //sheet(表名)
        //doWrite(數據)
        EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
    }
}

在這里插入圖片描述

EasyExcel讀取操作

實體類

@Data
public class DemoData {
    private String string;
    private Date date;
    private Double doubleData;
}

監聽器

// 有個很重要的點 DemoDataListener 不能被spring管理,要每次讀取excel都要new,然后里面用到spring可以構造方法傳進去
public class DemoDataListener extends AnalysisEventListener<DemoData> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);
    /**
     * 每隔5條存儲數據庫,實際使用中可以3000條,然后清理list ,方便內存回收
     */
    private static final int BATCH_COUNT = 5;
    List<DemoData> list = new ArrayList<DemoData>();
    /**
     * 假設這個是一個DAO,當然有業務邏輯這個也可以是一個service。當然如果不用存儲這個對象沒用。
     */
    private DemoDAO demoDAO;
    public DemoDataListener() {
        // 這里是demo,所以隨便new一個。實際使用如果到了spring,請使用下面的有參構造函數
        demoDAO = new DemoDAO();
    }
    /**
     * 如果使用了spring,請使用這個構造方法。每次創建Listener的時候需要把spring管理的類傳進來
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoDAO demoDAO) {
        this.demoDAO = demoDAO;
    }
    /**
     * 這個每一條數據解析都會來調用
     *
     * @param data
     *            one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        LOGGER.info("解析到一條數據:{}", JSON.toJSONString(data));
        list.add(data);
        // 達到BATCH_COUNT了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            saveData();
            // 存儲完成清理 list
            list.clear();
        }
    }
    /**
     * 所有數據解析完成了 都會來調用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 這里也要保存數據,確保最后遺留的數據也存儲到數據庫
        saveData();
        LOGGER.info("所有數據解析完成!");
    }
    /**
     * 加上存儲數據庫
     */
    private void saveData() {
        LOGGER.info("{}條數據,開始存儲數據庫!", list.size());
        demoDAO.save(list);
        LOGGER.info("存儲數據庫成功!");
    }
}


持久層

/**
 * 假設這個是你的DAO存儲。當然還要這個類讓spring管理,當然你不用需要存儲,也不需要這個類。
 **/
public class DemoDAO {
    public void save(List<DemoData> list) {
        // 如果是mybatis,盡量別直接調用多次insert,自己寫一個mapper里面新增一個方法batchInsert,所有數據一次性插入
    }
}

讀取代碼

/**
 * 最簡單的讀
 * <p>1. 創建excel對應的實體對象 參照{@link DemoData}
 * <p>2. 由於默認一行行的讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link DemoDataListener}
 * <p>3. 直接讀即可
 */
@Test
public void simpleRead() {
    // 有個很重要的點 DemoDataListener 不能被spring管理,要每次讀取excel都要new,然后里面用到spring可以構造方法傳進去
    // 寫法1:
    String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    // 這里 需要指定讀用哪個class去讀,然后讀取第一個sheet 文件流會自動關閉
    EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
    // 寫法2:
    fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    ExcelReader excelReader = null;
    try {
        excelReader = EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).build();
        ReadSheet readSheet = EasyExcel.readSheet(0).build();
        excelReader.read(readSheet);
    } finally {
        if (excelReader != null) {
            // 這里千萬別忘記關閉,讀的時候會創建臨時文件,到時磁盤會崩的
            excelReader.finish();
        }
    }
}

固定套路

  • 寫入,固定類格式進行寫入
  • 讀取,根據監聽器設置的規則進行讀取!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM