Java開發(fā)小技巧(六):使用Apache POI讀取Excel

前言

在數(shù)據(jù)倉庫中坟漱,ETL最基礎(chǔ)的步驟就是從數(shù)據(jù)源抽取所需的數(shù)據(jù)购笆,這里所說的數(shù)據(jù)源并非僅僅是指數(shù)據(jù)庫髓帽,還包括excel叶雹、csv瞻鹏、xml等各種類型的數(shù)據(jù)接口文件规哲,而這些文件中的數(shù)據(jù)不一定是結(jié)構(gòu)化存儲的撩笆,比如各種各樣的報表文件唆貌,往往是一些復(fù)雜的表格結(jié)構(gòu)谣蠢,其中不僅有我們需要的數(shù)據(jù)粟耻,還有一些冗余的查近、無價值的數(shù)據(jù),這時我們就無法直接用一般數(shù)據(jù)加載工具直接讀取入庫了挤忙。也許你會想霜威,數(shù)據(jù)源導(dǎo)出文件前先處理好數(shù)據(jù)就行了。然而册烈,實際開發(fā)中數(shù)據(jù)源往往是多個的戈泼,而且涉及到不同的部門甚至公司,這其間難免會出現(xiàn)各種麻煩赏僧,甚至有些數(shù)據(jù)文件還是純手工處理的大猛,不一定能給到你滿意的數(shù)據(jù)格式。所以我們不討論誰該負責(zé)轉(zhuǎn)換的問題淀零,這里主要介紹如何使用Apache POI來從Excel數(shù)據(jù)文件中讀取我們想要的數(shù)據(jù)挽绩,以及用Bean Validation對數(shù)據(jù)內(nèi)容按照預(yù)定的規(guī)則進行校驗。

文章要點:

  • Apache POI是什么
  • 如何使用Apache POI讀取Excel文件
  • 使用Bean Validation進行數(shù)據(jù)校驗
  • Excel讀取工具類
  • 使用實例

Apache POI是什么

Apache POI是用Java編寫的免費開源的跨平臺的Java API窑滞,提供API給Java程式對Microsoft Office格式檔案進行讀和寫的操作琼牧。


如何使用Apache POI處理Excel文件

1、導(dǎo)入Maven依賴

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.17</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.17</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml-schemas</artifactId>
    <version>3.17</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>3.17</version>
</dependency>

2哀卫、創(chuàng)建Workbook實例

這里需要注意的是Excel文檔的版本問題巨坊,Excel2003及以前版本的文檔使用HSSFWorkbook對象,Excel2007及之后版本使用HSSFWorkbook對象

// Excel2003及以前版本
Workbook workbook = new XSSFWorkbook(new FileInputStream(path));
// Excel2007及之后版本
Workbook workbook = new HSSFWorkbook(new FileInputStream(path));

3此改、獲取Sheet表格頁對象

Sheet是Excel文檔中的工作簿即表格頁面趾撵,讀取前要先找到數(shù)據(jù)所在頁面,可以通過標簽名或者索引的方式獲取指定Sheet對象

// 按索引獲取
Sheet sheet = workbook.getSheetAt(index);
// 按標簽名獲取
Sheet sheet = workbook.getSheet(label);

4共啃、獲取Cell單元格對象

// 行索引row和列索引col都是以 0 起始
Cell cell = sheet.getRow(row).getCell(col);

5占调、獲取單元格內(nèi)容

獲取單元格的值之前首先要獲知單元格內(nèi)容的類型,在Excel中單元格有6種類型:

  1. CELL_TYPE_BLANK :空值
  2. CELL_TYPE_BOOLEAN :布爾型
  3. CELL_TYPE_ERROR : 錯誤
  4. CELL_TYPE_FORMULA :公式型
  5. CELL_TYPE_STRING:字符串型
  6. CELL_TYPE_NUMERIC:數(shù)值型

各種類型的內(nèi)容還需要進一步判斷其數(shù)據(jù)格式移剪,例如單元格的Type為CELL_TYPE_NUMERIC時究珊,它有可能是Date類型,在Excel中的Date類型是以Double類型的數(shù)字存儲的纵苛,不同類型的值要調(diào)用cell對象相應(yīng)的方法去獲取剿涮,具體情況具體分析

public Object getCellValue(Cell cell) {
    if(cell == null) {
        return null;
    }
    switch (cell.getCellType()) {
    case Cell.CELL_TYPE_STRING:
        return cell.getRichStringCellValue().getString();
    case Cell.CELL_TYPE_NUMERIC:
        if (DateUtil.isCellDateFormatted(cell)) {
            return cell.getDateCellValue();
        } else {
            return cell.getNumericCellValue();
        }
    case Cell.CELL_TYPE_BOOLEAN:
        return cell.getBooleanCellValue();
    case Cell.CELL_TYPE_FORMULA:
        return formula.evaluate(cell).getNumberValue();
    default:
        return null;
    }
}

6、關(guān)閉Workbook對象

workbook.close();

使用Bean Validation進行數(shù)據(jù)校驗

當你要處理一個業(yè)務(wù)邏輯時攻人,數(shù)據(jù)校驗是你不得不考慮和面對的事情取试,程序必須通過某種手段來確保輸入進來的數(shù)據(jù)從語義上來講是正確的或者符合預(yù)定義的格式,一個Java程序一般是分層設(shè)計的怀吻,而不同的層可能是不同的開發(fā)人員來完成瞬浓,這樣就很容易出現(xiàn)不同的層重復(fù)進行數(shù)據(jù)驗證邏輯,導(dǎo)致代碼冗余等問題蓬坡。為了避免這樣的情況發(fā)生猿棉,最好是將驗證邏輯與相應(yīng)的模型進行綁定磅叛。

Bean Validation 規(guī)范的目標就是避免多層驗證的重復(fù)性,它提供了對 Java EE 和 Java SE 中的 Java Bean 進行驗證的方式铺根。該規(guī)范主要使用注解的方式來實現(xiàn)對 Java Bean 的驗證功能宪躯,從而使驗證邏輯從業(yè)務(wù)代碼中分離出來。

Hibernate ValidatorBean Validation 規(guī)范的參考實現(xiàn)位迂,我們可以用它來實現(xiàn)數(shù)據(jù)驗證邏輯,其Maven依賴如下:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.1.3.Final</version>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>2.2.4</version>
</dependency>

關(guān)于Bean Validation的詳細介紹可參考以下文章:
JSR 303 - Bean Validation 介紹及最佳實踐
Bean Validation 技術(shù)規(guī)范特性概述


Excel讀取工具類

我們要達到的效果是详瑞,模擬游標的方式構(gòu)建一個Excel讀取工具類ExcelReadHelper掂林,然后加載Excel文件流來創(chuàng)建工具類實例,通過這個實例我們可以像游標一樣設(shè)置當前的行和列坝橡,定好位置之后讀取出單元格的值并進行校驗泻帮,完成對Excel文件的讀取校驗操作。既然是讀取還有校驗數(shù)據(jù)计寇,異常處理和提示當然是至關(guān)重要的锣杂,所以還要有人性化的異常處理方式,方便程序使用者發(fā)現(xiàn)Excel中格式或內(nèi)容有誤的地方番宁,具體到哪一行哪一項元莫,出現(xiàn)的問題是什么。

ExcelReadHelper工具類主體

public class ExcelReadHelper {
    private static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    //文件絕對路徑
    private String excelUrl;
    private Workbook workbook;
    private Sheet sheet;
    //Sheet總數(shù)
    private int sheetCount;
    //當前行
    private Row row;
    private Validator validator;
    
    public ExcelReadHelper(File excelFile) throws ExcelException {
        validator = factory.getValidator();
        excelUrl = excelFile.getAbsolutePath();
        //判斷工作簿版本
        String fileName = excelFile.getName();
        String suffix = fileName.substring(fileName.lastIndexOf("."));
        try {
            if(suffix.equals(".xlsx")) {
                workbook = new XSSFWorkbook(new FileInputStream(excelFile));
            } else if(suffix.equals(".xls")) {
                workbook = new HSSFWorkbook(new FileInputStream(excelFile));
            } else {
                throw new ExcelException("Malformed excel file");
            }
        } catch(Exception e) {
            throw new ExcelException(excelUrl, e);
        }
        sheetCount = workbook.getNumberOfSheets();
    }
    
    /**
     * 關(guān)閉工作簿
     * @throws ExcelException 
     * @throws IOException
     */
    public void close() throws ExcelException {
        if (workbook != null) {
            try {
                workbook.close();
            } catch (IOException e) {
                throw new ExcelException(excelUrl, e);
            }
        }
    }
    
    /**
     * 獲取單元格真實位置
     * @param row 行索引
     * @param col 列索引
     * @return [行,列]
     */
    public String getCellLoc(Integer row, Integer col) {
        return String.format("[%s,%s]", row + 1, CellReference.convertNumToColString(col));     
    }
    
    /**
     * 根據(jù)標簽設(shè)置Sheet
     * @param labels
     * @throws ExcelException
     */
    public void setSheetByLabel(String... labels) throws ExcelException {
        Sheet sheet = null;
        for(String label : labels) {
            sheet = workbook.getSheet(label);
            if(sheet != null) {
                break;
            }
        }
        if(sheet == null) {
            StringBuilder sheetStr = new StringBuilder();
            for (String label : labels) {
                sheetStr.append(label).append(",");
            }
            sheetStr.deleteCharAt(sheetStr.lastIndexOf(","));
            throw new ExcelException(excelUrl, sheetStr.toString(), "Sheet does not exist");
        }
        this.sheet = sheet;
    }
    
    /**
     * 根據(jù)索引設(shè)置Sheet
     * @param index
     * @throws ExcelException 
     */
    public void setSheetAt(Integer index) throws ExcelException {
        Sheet sheet = workbook.getSheetAt(index);
        if(sheet == null) {
            throw new ExcelException(excelUrl, index + "", "Sheet does not exist");
        }
        this.sheet = sheet;
    }

    /**
     * 獲取單元格內(nèi)容并轉(zhuǎn)為String類型
     * @param row 行索引
     * @param col 列索引
     * @return
     */
    @SuppressWarnings("deprecation")
    public String getValueAt(Integer row, Integer col) {
        Cell cell = sheet.getRow(row).getCell(col);
        String value = null;
        if (cell != null) {
          switch (cell.getCellType()) {
            case Cell.CELL_TYPE_STRING:
                value = cell.getStringCellValue() + "";
                break;
            case Cell.CELL_TYPE_NUMERIC:
                if (DateUtil.isCellDateFormatted(cell)) {
                    value = cell.getDateCellValue().getTime() + "";
                } else {
                    double num = cell.getNumericCellValue();
                    if(num % 1 == 0) {
                        value = Double.valueOf(num).intValue() + "";
                    } else {
                        value = num + "";
                    }
                }
                break;
            case Cell.CELL_TYPE_FORMULA:
                value = cell.getNumericCellValue() + "";
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                value = String.valueOf(cell.getBooleanCellValue()) + "";
                break;
          }
        }
        return (value == null || value.isEmpty()) ? null : value.trim();
    }
    
    /**
     * 獲取當前行指定列內(nèi)容
     * @param col 列索引
     * @return
     */
    public String getValue(Integer col) {
        return getValueAt(row.getRowNum(), col);
    }
    
    /**
     * 獲取Sheet名稱
     * @return
     */
    public String getSheetLabel() {
        String label = null;
        if(sheet != null) {
            label = sheet.getSheetName();
        }
        return label;
    }
    
    /**
     * 行偏移
     * @param offset 偏移量
     * @return
     */
    public Boolean offsetRow(Integer offset) {
        Boolean state = true;
        if(row == null) {
            row = sheet.getRow(offset-1);
        } else {
            row = sheet.getRow(row.getRowNum() + offset);
            if(row == null) {
                state = false;
            }
        }
        return state;
    }
    
    /**
     * 設(shè)置行
     * @param index 索引
     * @return
     */
    public Boolean setRow(Integer index) {
        row = sheet.getRow(index);
        return row != null;
    }
    
    /**
     * 偏移一行
     * @return
     */
    public Boolean nextRow() {
        return offsetRow(1);
    }
    
    /**
     * 偏移到下一個Sheet
     * @return
     */
    public Boolean nextSheet() {
        Boolean state = true;
        if(sheet == null) {
            sheet = workbook.getSheetAt(0);
        } else {
            int index = workbook.getSheetIndex(sheet) + 1;
            if(index >= sheetCount) {
                sheet = null;
            } else {
                sheet = workbook.getSheetAt(index);
            }
            
            if(sheet == null) {
                state = false;
            }
        }
        row = null;
        return state;
    }
    
    /**
     * 數(shù)據(jù)校驗
     * @param obj 校驗對象
     * @throws ExcelException
     */
    public <T> void validate(T obj) throws ExcelException {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj);
        if(constraintViolations.size() > 0) {
            Iterator<ConstraintViolation<T>> iterable = constraintViolations.iterator();
            ConstraintViolation<T> cv = iterable.next();
            throw new ExcelException(excelUrl, sheet.getSheetName(), row.getRowNum() + 1 + "", 
                    String.format("%s=%s:%s", cv.getPropertyPath(), cv.getInvalidValue(), cv.getMessage()));
        }
    }

    /**
     * 拋出當前Sheet指定行異常
     * @param row 異常發(fā)生行索引
     * @param message 異常信息
     * @return
     */
    public ExcelException excelRowException(Integer row, String message) {
        return new ExcelException(excelUrl, sheet.getSheetName(), row + 1 + "", message);
    }

    /**
     * 拋出當前行異常
     * @param message 異常信息
     * @return
     */
    public ExcelException excelCurRowException(String message) {
        return new ExcelException(excelUrl, sheet.getSheetName(), row.getRowNum() + 1 + "", message);
    }

    /**
     * 拋出自定義異常
     * @param message 異常信息
     * @return
     */
    public ExcelException excelException(String message) {
        return new ExcelException(excelUrl, message);
    }
}

ExcelException異常類

public class ExcelException extends Exception {

    public ExcelException() {
        super();
    }

    public ExcelException(String message) {
        super(message);
    }

    public ExcelException(String url, String message) {
        super(String.format("EXCEL[%s]:%s", url, message));
    }
    
    public ExcelException(String url, String sheet, String message) {
        super(String.format("EXCEL[%s],SHEET[%s]:%s", url, sheet, message));
    }
    
    public ExcelException(String url, String sheet, String row, String message) {
        super(String.format("EXCEL[%s],SHEET[%s],ROW[%s]:%s", url, sheet, row, message));
    }
    
    public ExcelException(String url, Throwable cause) {
        super(String.format("EXCEL[%s]", url), cause);
    }
    
}

使用實例

// 使用Excel文件對象初始化ExcelReadHelper
ExcelReadHelper excel = new ExcelReadHelper(file);
        
// 第一頁
excel.setSheetAt(0);
        
// “Sheet1”頁
excel.setSheetByLabel("Sheet1");
        
// 下一頁
excel.nextSheet();
        
// 第一行(以 0 起始)
excel.setRow(0);
        
// 下一行
excel.nextRow();
        
// 偏移兩行
excel.offsetRow(2);
        
// 當前行第一列的值
String value1 = excel.getValue(0);
        
// 第一行第一列的值
String value2 = excel.getValueAt(0,0);
        
// 獲取單元格真實位置(如索引都為0時結(jié)果為[1,A])
String location = excel.getCellLoc(0,0);
        
// 當前頁標題(如“Sheet1”)
String label = excel.getSheetLabel();
        
// 校驗讀取的數(shù)據(jù)
try {
    excel.validate(obj);
} catch (ExcelException e) {
    // 錯誤信息中包含具體錯誤位置以及原因
    e.printStackTrace();
}

//拋出異常蝶押,結(jié)果自動包含出現(xiàn)異常的Excel路徑
throw excel.excelException(message);

//拋出指定行異常踱蠢,結(jié)果自動包含出現(xiàn)錯誤的Excel路徑、當前頁位置
throw excel.excelRowException(0, message);

//拋出當前行異常棋电,結(jié)果自動包含出現(xiàn)錯誤的Excel路徑茎截、當前頁、當前行位置
throw excel.excelCurRowException(message);

//關(guān)閉工作簿W(wǎng)orkbook對象
excel.close();

本文為作者kMacro原創(chuàng)赶盔,轉(zhuǎn)載請注明來源:https://zkhdev.github.io/2018/10/14/java-dev6/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末企锌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子于未,更是在濱河造成了極大的恐慌撕攒,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沉眶,死亡現(xiàn)場離奇詭異打却,居然都是意外死亡,警方通過查閱死者的電腦和手機谎倔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門柳击,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人片习,你說我怎么就攤上這事捌肴〉虐龋” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵状知,是天一觀的道長秽五。 經(jīng)常有香客問我,道長饥悴,這世上最難降的妖魔是什么坦喘? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮西设,結(jié)果婚禮上瓣铣,老公的妹妹穿的比我還像新娘。我一直安慰自己贷揽,他們只是感情好棠笑,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著禽绪,像睡著了一般蓖救。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上印屁,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天循捺,我揣著相機與錄音,去河邊找鬼库车。 笑死巨柒,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的柠衍。 我是一名探鬼主播洋满,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼珍坊!你這毒婦竟也來了牺勾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤阵漏,失蹤者是張志新(化名)和其女友劉穎驻民,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體履怯,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡回还,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了叹洲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柠硕。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蝗柔,到底是詐尸還是另有隱情闻葵,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布癣丧,位于F島的核電站槽畔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏胁编。R本人自食惡果不足惜厢钧,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掏呼。 院中可真熱鬧坏快,春花似錦、人聲如沸憎夷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拾给。三九已至,卻和暖如春兔沃,著一層夾襖步出監(jiān)牢的瞬間蒋得,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工乒疏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留额衙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓怕吴,卻偏偏與公主長得像窍侧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子转绷,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架伟件,建立于...
    Hsinwong閱讀 22,313評論 1 92
  • 使用首先需要了解他的工作原理 1.POI結(jié)構(gòu)與常用類 (1)創(chuàng)建Workbook和Sheet (2)創(chuàng)建單元格 (...
    長城ol閱讀 8,402評論 2 25
  • 快樂是什么?是成功后激動的眼淚?還是獲獎的那一刻?對我而言,快樂就是突破自我议经。 老師邁著沉重的步子...
    小羅琳閱讀 482評論 0 1
  • 近來小寶會獨自扶床走路了斧账,寶貝長大了,又進步了煞肾! 每當看到兩個寶貝在一起嬉鬧玩耍時咧织,我的心里就滿滿的,很幸福籍救! 寶...
    藍楓zdd閱讀 123評論 0 0
  • 初三的時候钧忽,心理十分陰暗毯炮,眼前的世界只剩下黑白逼肯,望著冰冷的現(xiàn)實,就以為這樣墮落下去桃煎,可這個世界就是這么有趣篮幢,一個女...
    宴夜閱讀 295評論 0 1