1. 程式人生 > >利用poi包裝一個簡單的Excel讀取器.一(適配一個Reader並提供readLine方法)

利用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