POI及EasyExcel筆記(狂神說)


視頻地址(狂神說):https://www.bilibili.com/video/BV1Ua4y1x7BK

記得三連狂神呀

介紹

https://baike.baidu.com/item/POI/8886826?fr=aladdin

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.example</groupId>
    <artifactId>POI</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!--xls(03)-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.9</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>
 
</project>

Excel 中有幾個操作對象:

工作薄:org.apache.poi.ss.usermodel.Workbook;

img

工作表:org.apache.poi.ss.usermodel.Sheet;

行:org.apache.poi.ss.usermodel.Row;

單元格(列):org.apache.poi.ss.usermodel.Cell;

POI-Excel寫操作

package com.poi;
 
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
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 org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.joda.time.DateTime;
import org.junit.Test;
 
import java.io.FileOutputStream;
import java.io.IOException;
 
public class ExcelWrite {
 
    private String PATH="F:\\ideaProject\\study\\";
 
    @Test
    public void write03() throws IOException {
        // 1、創建工作薄
        Workbook workbook = new HSSFWorkbook();
        //2、創建工作表
        Sheet sheet = workbook.createSheet("test1");
 
        //3、創建行(0:第一行)
        Row row1 = sheet.createRow(0);
        //4、創建單元格(0:第一行的第一個格子)
        Cell cell00 = row1.createCell(0);
        cell00.setCellValue("測試數據");
        //第一行的第二個格子
        Cell cell01 = row1.createCell(1);
        cell01.setCellValue("testData");
 
        //第二行
        Row row2 = sheet.createRow(1);
        //第二行的第一個格子
        Cell cell10 = row2.createCell(0);
        cell10.setCellValue("時間");
        //第二行的第二個格子
        Cell cell11 = row2.createCell(1);
        cell11.setCellValue(new DateTime().toString("yyyy-MM-dd HH:mm:ss"));
 
        //生成Excel(IO流)
        //03版本的Excel是使用 .xls 結尾!!!
        FileOutputStream fileOutputStream = new FileOutputStream(PATH + "POI03測試.xls");
 
        workbook.write(fileOutputStream);
 
        //關閉流
        fileOutputStream.close();
 
        System.out.println("03Excel輸出完畢");
    }
 
    @Test
    public void write07() throws IOException {
        // 1、創建工作薄
        Workbook workbook = new XSSFWorkbook();
        //2、創建工作表
        Sheet sheet = workbook.createSheet("test1");
 
        //3、創建行(0:第一行)
        Row row1 = sheet.createRow(0);
        //4、創建單元格(0:第一行的第一個格子)
        Cell cell00 = row1.createCell(0);
        cell00.setCellValue("測試數據");
        //第一行的第二個格子
        Cell cell01 = row1.createCell(1);
        cell01.setCellValue("testData");
 
        //第二行
        Row row2 = sheet.createRow(1);
        //第二行的第一個格子
        Cell cell10 = row2.createCell(0);
        cell10.setCellValue("時間");
        //第二行的第二個格子
        Cell cell11 = row2.createCell(1);
        cell11.setCellValue(new DateTime().toString("yyyy-MM-dd HH:mm:ss"));
 
        //生成Excel(IO流)
        //03版本的Excel是使用 .xlsx 結尾!!!
        FileOutputStream fileOutputStream = new FileOutputStream(PATH + "POI07測試.xlsx");
 
        workbook.write(fileOutputStream);
 
        //關閉流
        fileOutputStream.close();
 
        System.out.println("07Excel輸出完畢");
    }
}

注意:對象的區別、文件的后綴!!!

數據批量寫入

HSSF

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

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

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

@Test
public void write03BigData() 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);
        }
    }
    System.out.println("over");
    FileOutputStream fileOutputStream = new FileOutputStream(PATH + "TestWrite03BigData.xls");
    workbook.write(fileOutputStream);
    fileOutputStream.close();
 
    long end = System.currentTimeMillis();
 
    System.out.println((double) (end-begin)/1000);
}

XSSF

缺點:寫數據時速度非常慢,非常耗內存,也會發生內存溢出,如 100萬條

優點:可以寫較大的數據量,如 20萬條

@Test
public void write07BigData() throws IOException {
    long begin = System.currentTimeMillis();
 
    //創建簿
    Workbook workbook = new XSSFWorkbook();
    //創建表
    Sheet sheet = workbook.createSheet();
    //寫數據
    for (int rowNum = 0; rowNum < 100000; rowNum++) {
        Row row = sheet.createRow(rowNum);
        for (int cellNum = 0; cellNum < 10; cellNum++) {
            Cell cell = row.createCell(cellNum);
            cell.setCellValue(cellNum);
        }
    }
    System.out.println("over");
    FileOutputStream fileOutputStream = new FileOutputStream(PATH + "TestWrite07BigData.xlsx");
    workbook.write(fileOutputStream);
    fileOutputStream.close();
 
    long end = System.currentTimeMillis();
 
    System.out.println((double) (end-begin)/1000);
}

SXSSF

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

注意:

過程中可能產生臨時文件,需要清理臨時文件

默認由 100 條記錄被保存在內存中,如果超過這數量,則最前面的數據被寫入臨時文件

如果想自定義內存中數據的數量,可以使用 new SXSSFWorkbook(amount)

@Test
public void write07BigDataS() throws IOException {
    long begin = System.currentTimeMillis();
 
    //創建簿
    Workbook workbook = new SXSSFWorkbook();
    //創建表
    Sheet sheet = workbook.createSheet();
    //寫數據
    for (int rowNum = 0; rowNum < 100000; rowNum++) {
        Row row = sheet.createRow(rowNum);
        for (int cellNum = 0; cellNum < 10; cellNum++) {
            Cell cell = row.createCell(cellNum);
            cell.setCellValue(cellNum);
        }
    }
    System.out.println("over");
    FileOutputStream fileOutputStream = new FileOutputStream(PATH + "TestWrite07BigDataS.xlsx");
    workbook.write(fileOutputStream);
    fileOutputStream.close();
    //清除臨時文件
    ((SXSSFWorkbook) workbook).dispose();
 
    long end = System.currentTimeMillis();
    System.out.println((double) (end-begin)/1000);
}

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

請注意,仍然可能會消耗大量內存,這些內存基於正在使用的功能,例如合並區域,注釋......仍然只存儲在內存中,因此如果廣泛使用,可能需要大量內存。

POI-Excel讀操作

03

@Test
public void testRead03() throws IOException {
    //獲取文件流
    FileInputStream fileInputStream = new FileInputStream(PATH + "POIPOI03測試.xls");
 
    //1、獲取工作簿
    Workbook workbook = new HSSFWorkbook(fileInputStream);
    //2、得到表
    Sheet sheet = workbook.getSheetAt(0);
    //3、得到行
    Row row = sheet.getRow(0);
    //4、得到列
    Cell cell = row.getCell(0);
 
    //讀取值時一定要注意類型
    System.out.println(cell.getStringCellValue());
 
    fileInputStream.close();
}

07

@Test
public void testRead07() throws IOException {
    //獲取文件流
    FileInputStream fileInputStream = new FileInputStream(PATH + "POIPOI07測試.xlsx");
 
    //1、獲取工作簿
    Workbook workbook = new XSSFWorkbook(fileInputStream);
    //2、得到表
    Sheet sheet = workbook.getSheetAt(0);
    //3、得到行
    Row row = sheet.getRow(0);
    //4、得到列
    Cell cell = row.getCell(0);
 
    //讀取值時一定要注意類型
    System.out.println(cell.getStringCellValue());
 
    fileInputStream.close();
}

注意獲取值的類型!!!

讀取不同的數據類型

@Test
public void testCellType() throws Exception {
    //獲取文件
    FileInputStream fileInputStream = new FileInputStream(PATH + "會員消費商品明細表.xls");
 
    //獲取工作薄
    Workbook workbook = new HSSFWorkbook(fileInputStream);
    //得到表
    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 cellCout = rowTitle.getPhysicalNumberOfCells();
            for (int cellNum = 0; cellNum < cellCout; cellNum++) {
                System.out.print("【" + (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_NUMERIC://數字類型
                            System.out.print("【NUMERIC】");
 
                            if (HSSFDateUtil.isCellDateFormatted(cell)){//日期
                                System.out.print("【日期】");
                                Date date = cell.getDateCellValue();
                                cellValue = new DateTime().toString("yyyy-MM-dd");
                            }else{
                                // 不是日期格式,則防止當數字過長時以科學計數法顯示
                                System.out.print("【轉換成字符串】");
                                cell.setCellType(HSSFCell.CELL_TYPE_STRING);
                                cellValue = cell.toString();
                            }
                            break;
                        case HSSFCell.CELL_TYPE_BLANK://空
                            System.out.print("【BLANK】");
                            break;
                        case Cell.CELL_TYPE_ERROR:
                            System.out.print("【數據類型錯誤】");
                            break;
                    }
                    System.out.println(cellValue);
                }
            }
        }
    }
    fileInputStream.close();
}

計算公式

@Test
public void testFormula() throws Exception {
    FileInputStream fileInputStream = new FileInputStream(PATH + "計算公式.xls");
    Workbook workbook = new HSSFWorkbook(fileInputStream);
    Sheet sheetAt = workbook.getSheetAt(0);
 
    //獲取計算公式所在的行
    Row row = sheetAt.getRow(4);
    //計算公式的第幾個單元格
    Cell cell = row.getCell(0);
 
    //拿到計算公式
    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;
    }
}

EasyExcel操作

點擊跳轉:https://www.yuque.com/easyexcel/doc/easyexcel

pom.xml

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.6</version>
</dependency>

固定套路:

1、寫入,固定類格式進行寫入

2、讀取,根據監聽器設置的規則進行讀取

3.1 准備素材

image-20200423152949092

我們以上面這個表格為例來進行讀寫操作,觸類旁通

3.2 寫操作

先來個實體類方便插入數據

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

import java.util.Date;

@Data   //lombok
public class DemoData {
    @ExcelProperty("字符串標題")
    private String string;
    @ExcelProperty("日期標題")
    private Date date;
    @ExcelProperty("數字標題")
    private Double doubleData;
    /**
     * 忽略這個字段
     */
    @ExcelIgnore   //注意這個注解是高版本的easyexcel依賴才有
    private String ignore;
}

再來一個工具類方便我們寫數據

public class utilList {
    public static List<DemoData> data() {
        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;
    }
}

進行寫

public class EasyTest {
    static String path="F:\\demo\\javapoi\\demopoi\\";
    @Test
    public void simpleWrite() {
        // 寫法1
        String fileName = path + "EasyTest.xlsx";
        // 這里 需要指定寫用哪個class去寫,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
        // 如果這里想使用03 則 傳入excelType參數即可
        EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(utilList.data());

        // 寫法2
//        fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
//        // 這里 需要指定寫用哪個class去寫
//        ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build();
//        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
//        excelWriter.write(data(), writeSheet);
//        // 千萬別忘記finish 會幫忙關閉流
//       excelWriter.finish();
    }
}

寫完就有了這樣一個表格

擴展知識

重復多次寫,根據數據庫的分頁查詢再寫入到表格中,這里如果要寫到多個表的話,需要建立多個writeSheet對象

    @Test
    public void repeatedWrite() {
        // 方法1 如果寫到同一個sheet
        String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 這里 需要指定寫用哪個class去寫
        ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build();
        // 這里注意 如果同一個sheet只要創建一次
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
        // 去調用寫入,這里我調用了五次,實際使用時根據數據庫分頁的總的頁數來
        for (int i = 0; i < 5; i++) {
            // 分頁去數據庫查詢數據 這里可以去數據庫查詢每一頁的數據
            List<DemoData> data = data();
            excelWriter.write(data, writeSheet);
        }
        /// 千萬別忘記finish 會幫忙關閉流
        excelWriter.finish();

WEB中的寫(重點)

這個方法經常用到,導出Excel,失敗的時候返回json

    /**
     * 文件下載並且失敗的時候返回json(默認失敗了會返回一個有部分數據的Excel)
     *
     * @since 2.1.1
     */
    @GetMapping("downloadFailedUsingJson")
    public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {
        // 這里注意 有同學反應使用swagger 會導致各種問題,請直接用瀏覽器或者用postman
        try {
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            // 這里URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關系
            String fileName = URLEncoder.encode("測試", "UTF-8");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
            // 這里需要設置不關閉流
            EasyExcel.write(response.getOutputStream(), DownloadData.class).autoCloseStream(Boolean.FALSE).sheet("模板")
                .doWrite(data());
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            Map<String, String> map = new HashMap<String, String>();
            map.put("status", "failure");
            map.put("message", "下載文件失敗" + e.getMessage());
            response.getWriter().println(JSON.toJSONString(map));
        }
    }

3.2 讀操作

首先我們一個監聽器,因為和poi的不同,easyExcel是spring接管的,自己監控和改寫方法

package com.example.easyExcel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import com.example.Dao.DemoDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
// 有個很重要的點 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));
        System.out.println( 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("存儲數據庫成功!");
    }
}

這里的saveData是為了給讀取前台的表格之后可以執行這個然后通過下面的方法持久化到數據庫,而且這里默認是5條持久一次

/**
 * 假設這個是你的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() {
        String fileName = path + "EasyTest.xlsx";
        EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
    }

image-20200423154502718

WEB中的讀

    /**
     * 文件上傳
     * <p>
     * 1. 創建excel對應的實體對象 參照{@link UploadData}
     * <p>
     * 2. 由於默認一行行的讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link UploadDataListener}
     * <p>
     * 3. 直接讀即可
     */
    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
        return "success";
    }

總結

  • easyExcel的確比poi方便,但是它的讀需要編寫監聽器
  • 建議大數據用easyExcel,因為大數據時poi對於內存消耗非常大
  • 由於apache poi和jxl,excelPOI都有一個嚴重的問題,就是非常消耗內存,特別處理數據量多時,速度慢並且時有異常發生,所以改用由阿里研發的easyExcel更可靠一些,它的官方建議對於1000行以內的采用原來poi的寫法一次讀寫,但於1000行以上的數據,有用了一行行進行解析的方案,這樣避免了內存的溢出。
  • asyExcel擴展功能很多,且Api式調用真的輕松很多

筆記參考自:https://blog.csdn.net/weixin_44544465/article/details/110393211

筆記參考自:https://blog.csdn.net/jiohfgj/article/details/105724073


免責聲明!

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



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