通常,读文本我们会使用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

版权声明:本文为shanhm1991原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/shanhm1991/p/11466856.html