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種類型:

  1. CELL_TYPE_BLANK :空值
  2. CELL_TYPE_BOOLEAN :布爾型
  3. CELL_TYPE_ERROR : 錯誤
  4. CELL_TYPE_FORMULA :公式型
  5. CELL_TYPE_STRING:字符串型
  6. 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 ValidatorBean 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();

本文為作者kMacro原創,轉載請注明來源:https://zkhdev.github.io/2018/10/14/java-dev6/


免責聲明!

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



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