利用poi包裝一個簡單的Excel讀取器.一(適配一個Reader並提供readLine方法)
通常,讀文字我們會使用BufferedReader,它裝飾或者說管理了InputStreamReader,同時提供readLine()簡化了我們對文字行的讀取。就像從流水線上獲取產品一樣,每當取完一件後,它自動準備好下一件並等待我們獲取,一直到全部取完結束。所以我們的目標就是希望也能管理poi並提供一個readLine()一樣的方法來讀取Excel。
1、先來看一個有點類似Excel讀取的文字需求:讀取一類文字檔案,文中每行內容由若干欄位組成,這些欄位由約定好的分隔符來分割。說它類似Excel的讀取是因為它們的每行內容都是由若干欄位或者列組成。這裡你可以直接用BufferedReader讀取行,然後再分割,但我們希望可以直接從讀取的行資料中得到各個欄位,而不用使用者在獲取行之後再去分割解析行內容。
先定義下讀取的行為和行資料的形式:
1 public interface IReader extends Closeable { 2 3 /** 4 * 讀取下一行 5 * @return ReaderRow 6 * @throws Exception Exception 7 */ 8 IRow readRow() throws Exception; 9 10 }
1 public interface IRow { 2 3 /** 4 * 獲取當前行索引 5 * @return 6 */ 7 int getRowIndex(); 8 9 /** 10 * 行內容是否為空 11 * @return 12 */ 13 boolean isEmpty(); 14 15 /** 16 * 獲取行列資料 17 * @return 18 */ 19 List<String> getColumnList(); 20 }
對文字讀取的實現很簡單:直接將行讀取的操作委託給BufferedReader,然後用給定的分隔符對行內容進行分割
1 public class TextReader implements IReader { 2 3 private BufferedReader reader; 4 5 private int rowIndex; 6 7 private String regex; 8 9 public TextReader(InputStream inputStream, String regex) throws Exception { 10 this.regex = regex; 11 reader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8")); 12 } 13 14 @Override 15 public TextRow readRow() throws Exception { 16 String line = reader.readLine(); 17 rowIndex++; 18 if(line == null){ 19 return null; 20 } 21 22 boolean isEmpty = StringUtils.isBlank(line); 23 24 if(regex == null){ 25 TextRow row = new TextRow(rowIndex - 1, Arrays.asList(line)); 26 row.setEmpty(isEmpty); 27 return row; 28 } 29 TextRow row = new TextRow(rowIndex - 1, Arrays.asList(line.split(regex))); 30 row.setEmpty(isEmpty); 31 return row; 32 } 33 34 @Override 35 public void close() throws IOException { 36 IoUtil.close(reader); 37 } 38 }
對於行內容,使用List來儲存分割的所有欄位
1 public class TextRow implements IRow { 2 3 private int rowIndex; 4 5 private List<String> columnList; 6 7 private boolean isEmpty; 8 9 public TextRow(int rowIndex, List<String> rowData){ 10 this.rowIndex = rowIndex; 11 this.columnList = rowData; 12 } 13 14 @Override 15 public int getRowIndex() { 16 return rowIndex; 17 } 18 19 @Override 20 public boolean isEmpty() { 21 return isEmpty; 22 } 23 24 @Override 25 public List<String> getColumnList() { 26 return columnList; 27 } 28 29 void setEmpty(boolean isEmpty) { 30 this.isEmpty = isEmpty; 31 } 32 }
2、再來看下Excel讀取時要解決的問題
2.1、 由於excel是由多個獨立的sheet構成,所以對於讀取的行結果,除了記錄當前行索引之外,還需要記錄當前sheet的名稱和索引,以及當前行是否是當前sheet的最後一行。這些都是有必要的,因為使用者可能要根據這些資訊去對資料進行識別處理。
1 public class ExcelRow implements IRow{ 2 3 private int rowIndex; 4 5 private List<String> columnList; 6 7 private int sheetIndex; 8 9 private String sheetName; 10 11 private boolean isLastRow; 12 13 private boolean isEmpty; 14 15 public ExcelRow(int rowIndex, List<String> rowData){ 16 this.rowIndex = rowIndex; 17 this.columnList = rowData; 18 } 19 20 @Override 21 public int getRowIndex() { 22 return rowIndex; 23 } 24 25 @Override 26 public boolean isEmpty() { 27 return isEmpty; 28 } 29 30 @Override 31 public List<String> getColumnList() { 32 return columnList; 33 } 34 35 public boolean isLastRow() { 36 return isLastRow; 37 } 38 39 public int getSheetIndex() { 40 return sheetIndex; 41 } 42 43 public String getSheetName() { 44 return sheetName; 45 } 46 47 void setEmpty(boolean isEmpty) { 48 this.isEmpty = isEmpty; 49 } 50 51 void setLastRow(boolean isLastRow) { 52 this.isLastRow = isLastRow; 53 } 54 55 void setSheetIndex(int sheetIndex) { 56 this.sheetIndex = sheetIndex; 57 } 58 59 void setSheetName(String sheetName) { 60 this.sheetName = sheetName; 61 } 62 }
2.2、同樣由於excel是由多個獨立的sheet構成,使用者可能希望能夠對Excel中要讀取的sheet進行過濾,或者重排序。
簡單的辦法是在讀取流程中抽出兩個方法交給子類繼承實現,但這對使用者不太舒服,想過濾還得先繼承實現你的Reader。更好的做法是利用組合,先定義一個過濾器,如果使用者需要,就實現一個過濾器並交給讀取器即可。
1 public interface ExcelSheetFilter { 2 3 /** 4 * 根據sheet索引和sheet名稱過濾需要處理的sheet 5 * @param sheetIndex 從1開始 6 * @param sheetName 7 * @return 8 */ 9 boolean filter(int sheetIndex, String sheetName); 10 11 /** 12 * 重排sheet的讀取順序,或者清除不需處理的sheet 13 * @param nameList 14 */ 15 void resetSheetListForRead(List<String> nameList); 16 }
2.2、Excel檔案分為xls和xlsx兩種型別,但我們解析時可能直接的是檔案流,無法通過哪種特徵來判斷檔案流型別。所以只能將型別的判斷交給使用者自己解決,我們可以預定義兩種處理型別。
2.3、同樣由於excel是由多個獨立的sheet構成,使用者可能希望能夠像讀取下一行資料一樣讀取下一個sheet的資料。
由於我們不能假設使用者的呼叫行為,有可能他一會readRow()一會又readSheet(),所以必須先規定下readSheet()的語義:如果當前sheet已經讀取過一些行並且還有剩餘,那麼直接返回當前sheet剩餘的行,否則直接返回下一個sheet的所有行。這樣的定義也更符合流的概念。
1 public interface IExcelReader extends IReader { 2 3 public static final String EXCEL_XLS = "xls"; 4 5 public static final String EXCEL_XLSX = "xlsx"; 6 7 /** 8 * read next row 9 * @return 10 * @throws Exception Exception 11 */ 12 ExcelRow readRow() throws Exception; 13 14 /** 15 * read next sheet 16 * @return if the current sheet has remaining then return the rest, otherwise return the data of next sheet 17 * @throws Exception 18 */ 19 List<ExcelRow> readSheet() throws Exception; 20 21 /** 22 * set sheet filter 23 * @param sheetFilter 24 */ 25 void setSheetFilter(ExcelSheetFilter sheetFilter); 26 }
3、實現思路
首先我們要知道Workbook在初始化的時候,它已經把整個Excel都解析完成了。我們做的只是維護這些結果,同時提供一些能夠更方便獲取資料的方法。
1. 在Workbook初始化後首先維護Excel的原sheet列表並保持原順序:sheetNameList,以及一個sheet名稱與sheet資料的對映集:sheetMap
2. 另外初始化一個sheetNameGivenList列表,後面sheet過濾時或者資料讀取時使用的都是sheetNameGivenList,而sheetNameList永遠保持不變,它僅僅用作參考。比如讀取時,首先從sheetNameGivenList中獲取下一個要讀取的sheet名稱,然後再通過名稱從sheetNameList中獲取它真正的索引sheetIndex,只有這樣保證了sheet原來的索引順序不變,使用者對sheet的過濾或者重排序才能有意義。
3. 還需要維護一些表示當前讀取位置的索引:使用sheetIndexReading維護當前讀取的sheet在sheetNameGivenList中的位置以及它的sheetName,根據sheetName我們可以從sheetNameList中獲取到它的sheetIndex;另外使用rowIndex以及cellIndex維護當前讀取的行索引和列索引。
4. 讀取過程就是依照sheetNameGivenList中的順序依次讀取sheet,如果當前sheet讀取到最後一行,就另起下一個sheet,當讀完最後一個sheet的最後一行,再讀取就返回null。
具體實現可以這樣:
1 public class ExcelReader implements IExcelReader { 2 3 private static final Logger LOG = Logger.getLogger(ExcelReader.class); 4 5 private InputStream inputStream; 6 7 private String type; 8 9 private Workbook workbook = null; 10 11 private Map<String, Sheet> sheetMap = new HashMap<>(); 12 13 private List<String> sheetNameList = new ArrayList<>(); 14 15 private List<String> sheetNameGivenList = new ArrayList<>(); 16 17 private int sheetIndexReading = 0; 18 19 private Sheet sheet; 20 21 private int sheetIndex = 0; 22 23 private String sheetName; 24 25 private int rowIndex = 0; 26 27 private int rowCount; 28 29 private int cellIndex = 0; 30 31 private ExcelSheetFilter sheetFilter; 32 33 private boolean inited = false; 34 35 public ExcelReader(InputStream inputStream, String type) throws IOException { 36 this.type = type; 37 this.inputStream = inputStream; 38 init(); 39 } 40 41 @Override 42 public void setSheetFilter(ExcelSheetFilter sheetFilter) { 43 this.sheetFilter = sheetFilter; 44 } 45 46 private void init() throws IOException{ 47 if(EXCEL_XLS.equalsIgnoreCase(type)){ 48 workbook = new HSSFWorkbook(inputStream); 49 }else if(EXCEL_XLSX.equalsIgnoreCase(type)){ 50 workbook = new XSSFWorkbook(inputStream); 51 }else{ 52 throw new UnsupportedOperationException("Excel file name must end with .xls or .xlsx"); 53 } 54 int sheetCount = workbook.getNumberOfSheets(); 55 for(int i = 0;i < sheetCount;i++){ 56 Sheet shee = workbook.getSheetAt(i); 57 sheetNameList.add(shee.getSheetName()); 58 sheetMap.put(shee.getSheetName(), shee); 59 } 60 //cann't let the customer code to directly modify sheetNameList 61 sheetNameGivenList.addAll(sheetNameList); 62 } 63 64 @Override 65 public List<ExcelRow> readSheet() throws Exception { 66 List<ExcelRow> list = new ArrayList<>(); 67 ExcelRow row = null; 68 while((row = readRow()) != null){ 69 if(!row.isLastRow()){ 70 list.add(row); 71 }else{ 72 return list; 73 } 74 } 75 return null; 76 } 77 78 @Override 79 public ExcelRow readRow() { 80 if(!inited){ 81 inited = true; 82 if(sheetFilter != null){ 83 sheetFilter.resetSheetListForRead(sheetNameGivenList); 84 } 85 initSheet(); 86 } 87 while(true){ 88 if(sheet == null){ 89 return null; 90 } 91 if(sheetFilter != null && !sheetFilter.filter(sheetIndex, sheetName)){ 92 if(++sheetIndexReading >= sheetNameGivenList.size()){ 93 return null; 94 } 95 initSheet(); 96 }else{ 97 if(rowIndex >= rowCount){ 98 if(sheetIndexReading >= sheetNameGivenList.size() - 1){ 99 return null; 100 }else{ 101 sheetIndexReading++; 102 initSheet(); 103 continue; 104 } 105 }else{ 106 Row row = sheet.getRow(rowIndex); 107 rowIndex++; 108 109 //row not exist, don't know why 110 if(row == null){ 111 ExcelRow data = new ExcelRow(rowIndex, new ArrayList<String>(0)); 112 data.setSheetIndex(sheetIndex); 113 data.setSheetName(sheetName); 114 data.setEmpty(true); 115 data.setLastRow(rowIndex == rowCount); 116 return data; 117 } 118 119 int cellCount = row.getLastCellNum(); 120 //Illegal Capacity: -1 121 if(cellCount <= 0){ 122 ExcelRow data = new ExcelRow(rowIndex, new ArrayList<String>(0)); 123 data.setSheetIndex(sheetIndex); 124 data.setSheetName(sheetName); 125 data.setEmpty(true); 126 data.setLastRow(rowIndex == rowCount); 127 return data; 128 } 129 List<String> list = new ArrayList<>(cellCount); 130 131 boolean isEmpty = true; 132 for(cellIndex = 0; cellIndex < cellCount; cellIndex++){ 133 String value = getCellValue(row.getCell(cellIndex)); 134 if(isEmpty && !StringUtils.isBlank(value)){ 135 isEmpty = false; 136 } 137 list.add(value); 138 } 139 ExcelRow rowData = new ExcelRow(rowIndex, list); 140 rowData.setSheetIndex(sheetIndex); 141 rowData.setSheetName(sheetName); 142 rowData.setEmpty(isEmpty); 143 rowData.setLastRow(rowIndex == rowCount); 144 return rowData; 145 } 146 } 147 } 148 } 149 150 private void initSheet(){ 151 rowIndex = 0; 152 sheetName = sheetNameGivenList.get(sheetIndexReading); 153 sheetIndex = sheetNameList.indexOf(sheetName) + 1; 154 while((sheet = sheetMap.get(sheetName)) == null){ 155 sheetIndexReading++; 156 if(sheetIndexReading >= sheetNameGivenList.size()){ 157 sheet = null; 158 return; 159 }else{ 160 sheetName = sheetNameGivenList.get(sheetIndexReading); 161 sheetIndex = sheetNameList.indexOf(sheetName); 162 } 163 } 164 rowCount = sheet.getLastRowNum() + 1;//poi row num start with 0 165 } 166 167 private String getCellValue(Cell cell) { 168 if (cell == null) { 169 return ""; 170 } 171 172 switch (cell.getCellType()) { 173 case NUMERIC: 174 double value = cell.getNumericCellValue(); 175 if(DateUtil.isCellDateFormatted(cell)){ 176 Date date = DateUtil.getJavaDate(value); 177 return String.valueOf(date.getTime()); 178 }else{ 179 try{ 180 return double2String(value); 181 }catch(Exception e){ 182 LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex, e); 183 return String.valueOf(value); 184 } 185 } 186 case STRING: 187 return cell.getStringCellValue(); 188 case BOOLEAN: 189 return String.valueOf(cell.getBooleanCellValue()); 190 case FORMULA: 191 try { 192 return double2String(cell.getNumericCellValue()); 193 } catch (IllegalStateException e) { 194 try { 195 return cell.getRichStringCellValue().toString(); 196 } catch (IllegalStateException e2) { 197 LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex, e2); 198 return ""; 199 } 200 } catch (Exception e) { 201 LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex, e); 202 return ""; 203 } 204 case BLANK: 205 return ""; 206 case ERROR: 207 LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex); 208 return ""; 209 default: 210 return ""; 211 } 212 } 213 214 static String double2String(Double d) { 215 return formatDouble(d.toString()); 216 } 217 218 static String formatDouble(String doubleStr) { 219 boolean b = doubleStr.contains("E"); 220 int indexOfPoint = doubleStr.indexOf('.'); 221 if (b) { 222 int indexOfE = doubleStr.indexOf('E'); 223 BigInteger xs = new BigInteger(doubleStr.substring(indexOfPoint + BigInteger.ONE.intValue(), indexOfE)); 224 int pow = Integer.parseInt(doubleStr.substring(indexOfE + BigInteger.ONE.intValue())); 225 int xsLen = xs.toByteArray().length; 226 int scale = xsLen - pow > 0 ? xsLen - pow : 0; 227 doubleStr = String.format("%." + scale + "f", doubleStr); 228 } else { 229 Pattern p = Pattern.compile(".0$"); 230 Matcher m = p.matcher(doubleStr); 231 if (m.find()) { 232 doubleStr = doubleStr.replace(".0", ""); 233 } 234 } 235 return doubleStr; 236 } 237 238 @Override 239 public void close() throws IOException { 240 IoUtil.close(workbook); 241 IoUtil.close(inputStream); 242 } 243 }
4、使用方式
1 public static void test() throws Exception{ 2 IExcelReader reader = new ExcelReader("D:/1.xlsx"); 3 reader.setSheetFilter(new ExcelSheetFilter(){ 4 @Override 5 public boolean filter(int sheetIndex, String sheetName) { 6 return true; 7 } 8 9 @Override 10 public void resetSheetListForRead(List<String> nameList) { 11 nameList.remove(0); 12 } 13 }); 14 15 ExcelRow row = null; 16 while((row = reader.readRow()) != null){ 17 System.out.println(row); 18 } 19 reader.close(); 20 }
readSheet()的使用方式與readRow()相同。通過將Workbook交給ExcelReader維護,我們可以直接面向資料,而不用去處理Excel的結構,也不用維護讀取的狀態。
5、存在問題
記憶體佔用問題:上面提到Workbook在初始化的時候就解析了Excel,它將整個流全部讀取並解析完成後維護在記憶體中,我們對它的讀取其實就是對它結果的遍歷,這種方式對記憶體的消耗大得超乎想象!
引用程式碼詳見:https://github.com/shanhm1991/fom