Java開發小技巧:使用Apache POI讀取Excel
前言
在資料倉庫中,ETL最基礎的步驟就是從資料來源抽取所需的資料,這裡所說的資料來源並非僅僅是指資料庫,還包括excel、csv、xml等各種型別的資料介面檔案,而這些檔案中的資料不一定是結構化儲存的,比如各種各樣的報表檔案,往往是一些複雜的表格結構,其中不僅有我們需要的資料,還有一些冗餘的、無價值的資料,這時我們就無法直接用一般資料載入工具直接讀取入庫了。也許你會想,資料來源匯出檔案前先處理好資料就行了。然而,實際開發中資料來源往往是多個的,而且涉及到不同的部門甚至公司,這其間難免會出現各種麻煩,甚至有些資料檔案還是純手工處理的,不一定能給到你滿意的資料格式。所以我們不討論誰該負責轉換的問題,這裡主要介紹如何使用Apache POI來從Excel資料檔案中讀取我們想要的資料,以及用Bean Validation對資料內容按照預定的規則進行校驗。
文章要點:
- Apache POI是什麼
- 如何使用Apache POI讀取Excel檔案
- 使用Bean Validation進行資料校驗
- Excel讀取工具類
- 使用例項
Apache POI是什麼
Apache POI是用Java編寫的免費開源的跨平臺的Java API,提供API給Java程式對Microsoft Office格式檔案進行讀和寫的操作。
如何使用Apache POI處理Excel檔案
1、匯入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、建立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文件中的工作簿即表格頁面,讀取前要先找到資料所在頁面,可以通過標籤名或者索引的方式獲取指定Sheet物件
// 按索引獲取 Sheet sheet = workbook.getSheetAt(index); // 按標籤名獲取 Sheet sheet = workbook.getSheet(label);
4、獲取Cell單元格物件
// 行索引row和列索引col都是以 0 起始 Cell cell = sheet.getRow(row).getCell(col);
5、獲取單元格內容
獲取單元格的值之前首先要獲知單元格內容的型別,在Excel中單元格有6種類型:
- CELL_TYPE_BLANK :空值
- CELL_TYPE_BOOLEAN :布林型
- CELL_TYPE_ERROR : 錯誤
- CELL_TYPE_FORMULA :公式型
- CELL_TYPE_STRING:字串型
- CELL_TYPE_NUMERIC:數值型
各種型別的內容還需要進一步判斷其資料格式,例如單元格的Type為CELL_TYPE_NUMERIC時,它有可能是Date型別,在Excel中的Date型別是以Double型別的數字儲存的,不同型別的值要呼叫cell物件相應的方法去獲取,具體情況具體分析
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、關閉Workbook物件
workbook.close();
使用Bean Validation進行資料校驗
當你要處理一個業務邏輯時,資料校驗是你不得不考慮和麵對的事情,程式必須通過某種手段來確保輸入進來的資料從語義上來講是正確的或者符合預定義的格式,一個Java程式一般是分層設計的,而不同的層可能是不同的開發人員來完成,這樣就很容易出現不同的層重複進行資料驗證邏輯,導致程式碼冗餘等問題。為了避免這樣的情況發生,最好是將驗證邏輯與相應的模型進行繫結。
Bean Validation 規範的目標就是避免多層驗證的重複性,它提供了對 Java EE 和 Java SE 中的 Java Bean 進行驗證的方式。該規範主要使用註解的方式來實現對 Java Bean 的驗證功能,從而使驗證邏輯從業務程式碼中分離出來。
Hibernate Validator 是 Bean Validation 規範的參考實現,我們可以用它來實現資料驗證邏輯,其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>
關於Bean Validation的詳細介紹可參考以下文章:
JSR 303 - Bean Validation 介紹及最佳實踐
Bean Validation 技術規範特性概述
Excel讀取工具類
我們要達到的效果是,模擬遊標的方式構建一個Excel讀取工具類ExcelReadHelper,然後載入Excel檔案流來建立工具類例項,通過這個例項我們可以像遊標一樣設定當前的行和列,定好位置之後讀取出單元格的值並進行校驗,完成對Excel檔案的讀取校驗操作。既然是讀取還有校驗資料,異常處理和提示當然是至關重要的,所以還要有人性化的異常處理方式,方便程式使用者發現Excel中格式或內容有誤的地方,具體到哪一行哪一項,出現的問題是什麼。
ExcelReadHelper工具類主體
public class ExcelReadHelper { private static ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); //檔案絕對路徑 private String excelUrl; private Workbook workbook; private Sheet sheet; //Sheet總數 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(); } /** * 關閉工作簿 * @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)); } /** * 根據標籤設定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; } /** * 根據索引設定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; } /** * 獲取單元格內容並轉為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(); } /** * 獲取當前行指定列內容 * @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; } /** * 設定行 * @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; } /** * 資料校驗 * @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 異常發生行索引 * @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時結果為[1,A]) String location = excel.getCellLoc(0,0); // 當前頁標題(如“Sheet1”) String label = excel.getSheetLabel(); // 校驗讀取的資料 try { excel.validate(obj); } catch (ExcelException e) { // 錯誤資訊中包含具體錯誤位置以及原因 e.printStackTrace(); } //丟擲異常,結果自動包含出現異常的Excel路徑 throw excel.excelException(message); //丟擲指定行異常,結果自動包含出現錯誤的Excel路徑、當前頁位置 throw excel.excelRowException(0, message); //丟擲當前行異常,結果自動包含出現錯誤的Excel路徑、當前頁、當前行位置 throw excel.excelCurRowException(message); //關閉工作簿Workbook物件 excel.close();
歡迎工作一到八年的Java工程師朋友們加入Java高階交流群:854630135
本群提供免費的學習指導 架構資料 以及免費的解答
不懂得問題都可以在本群提出來 之後還會有直播平臺和講師直接交流噢
哦對了,喜歡就別忘了關注一下哦~