通常,读文本我们会使用BufferedReader,它装饰或者说管理了InputStreamReader,同时提供readLine()简化了我们对文本行的读取。就像从流水线上获取产品一样,每当取完一件后,它自动准备好下一件并等待我们获取,一直到全部取完结束。所以我们的目标就是希望也能管理poi并提供一个readLine()一样的方法来读取Excel。

1、先来看一个有点类似Excel读取的文本需求:读取一类文本文件,文中每行内容由若干字段组成,这些字段由约定好的分隔符来分割。说它类似Excel的读取是因为它们的每行内容都是由若干字段或者列组成。这里你可以直接用BufferedReader读取行,然后再分割,但我们希望可以直接从读取的行数据中得到各个字段,而不用使用者在获取行之后再去分割解析行内容。

先定义下读取的行为和行数据的形式:

 public interface IReader extends Closeable {

     /**
      * 读取下一行
      * @return ReaderRow
      * @throws Exception Exception
      */
     IRow readRow() throws Exception;

 }
 public interface IRow {

     /**
      * 获取当前行索引
      * @return
      */
     int getRowIndex();

     /**
      * 行内容是否为空
      * @return
      */
     boolean isEmpty();

     /**
      * 获取行列数据
      * @return
      */
     List<String> getColumnList();
 }

对文本读取的实现很简单:直接将行读取的操作委托给BufferedReader,然后用给定的分隔符对行内容进行分割

 public class TextReader implements IReader {

     private BufferedReader reader;

     private int rowIndex;

     private String regex;

     public TextReader(InputStream inputStream, String regex) throws Exception {
         this.regex = regex;
         reader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
     }

     @Override
     public TextRow readRow() throws Exception {
         String line = reader.readLine();
         rowIndex++;
         if(line == null){
             return null;
         }

         boolean isEmpty = StringUtils.isBlank(line);

         if(regex == null){
             TextRow row = new TextRow(rowIndex - 1, Arrays.asList(line));
             row.setEmpty(isEmpty);
             return row;
         }
         TextRow row = new TextRow(rowIndex - 1, Arrays.asList(line.split(regex)));
         row.setEmpty(isEmpty);
         return row;
     }

     @Override
     public void close() throws IOException {
         IoUtil.close(reader);
     }
 }

对于行内容,使用List来保存分割的所有字段

 public class TextRow implements IRow {

     private int rowIndex;

     private List<String> columnList;

     private boolean isEmpty;

     public TextRow(int rowIndex, List<String> rowData){
         this.rowIndex = rowIndex;
         this.columnList = rowData;
     }

     @Override
     public int getRowIndex() {
         return rowIndex;
     }

     @Override
     public boolean isEmpty() {
         return isEmpty;
     }

     @Override
     public List<String> getColumnList() {
         return columnList;
     }

     void setEmpty(boolean isEmpty) {
         this.isEmpty = isEmpty;
     }
 }

2、再来看下Excel读取时要解决的问题

2.1、 由于excel是由多个独立的sheet构成,所以对于读取的行结果,除了记录当前行索引之外,还需要记录当前sheet的名称和索引,以及当前行是否是当前sheet的最后一行。这些都是有必要的,因为使用者可能要根据这些信息去对数据进行识别处理。

 public class ExcelRow implements IRow{

     private int rowIndex;

     private List<String> columnList;

     private int sheetIndex;

     private String sheetName;

     private boolean isLastRow;

     private boolean isEmpty;

     public ExcelRow(int rowIndex, List<String> rowData){
         this.rowIndex = rowIndex;
         this.columnList = rowData;
     }

     @Override
     public int getRowIndex() {
         return rowIndex;
     }

     @Override
     public boolean isEmpty() {
         return isEmpty;
     }

     @Override
     public List<String> getColumnList() {
         return columnList;
     }

     public boolean isLastRow() {
         return isLastRow;
     }

     public int getSheetIndex() {
         return sheetIndex;
     }

     public String getSheetName() {
         return sheetName;
     }

     void setEmpty(boolean isEmpty) {
         this.isEmpty = isEmpty;
     }

     void setLastRow(boolean isLastRow) {
         this.isLastRow = isLastRow;
     }

     void setSheetIndex(int sheetIndex) {
         this.sheetIndex = sheetIndex;
     }

     void setSheetName(String sheetName) {
         this.sheetName = sheetName;
     }
 }

2.2、同样由于excel是由多个独立的sheet构成,使用者可能希望能够对Excel中要读取的sheet进行过滤,或者重排序。

简单的办法是在读取流程中抽出两个方法交给子类继承实现,但这对使用者不太舒服,想过滤还得先继承实现你的Reader。更好的做法是利用组合,先定义一个过滤器,如果使用者需要,就实现一个过滤器并交给读取器即可。

 public interface ExcelSheetFilter {

     /**
      * 根据sheet索引和sheet名称过滤需要处理的sheet
      * @param sheetIndex 从1开始
      * @param sheetName
      * @return
      */
     boolean filter(int sheetIndex, String sheetName);

     /**
      * 重排sheet的读取顺序,或者清除不需处理的sheet
      * @param nameList
      */
     void resetSheetListForRead(List<String> nameList);
 }

2.2、Excel文件分为xls和xlsx两种类型,但我们解析时可能直接的是文件流,无法通过哪种特征来判断文件流类型。所以只能将类型的判断交给使用者自己解决,我们可以预定义两种处理类型。

2.3、同样由于excel是由多个独立的sheet构成,使用者可能希望能够像读取下一行数据一样读取下一个sheet的数据。

由于我们不能假设使用者的调用行为,有可能他一会readRow()一会又readSheet(),所以必须先规定下readSheet()的语义:如果当前sheet已经读取过一些行并且还有剩余,那么直接返回当前sheet剩余的行,否则直接返回下一个sheet的所有行。这样的定义也更符合流的概念。

 public interface IExcelReader extends IReader {

     public static final String EXCEL_XLS = "xls";

     public static final String EXCEL_XLSX = "xlsx";

     /**
      * read next row
      * @return
      * @throws Exception Exception
      */
     ExcelRow readRow() throws Exception;

     /**
      * read next sheet
      * @return if the current sheet has remaining then return the rest, otherwise return the data of next sheet
      * @throws Exception
      */
     List<ExcelRow> readSheet() throws Exception;

     /**
      * set sheet filter
      * @param sheetFilter
      */
     void setSheetFilter(ExcelSheetFilter sheetFilter);
 }

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。

具体实现可以这样:

 public class ExcelReader implements IExcelReader {

     private static final Logger LOG = Logger.getLogger(ExcelReader.class);

     private InputStream inputStream;

     private String type;

     private Workbook workbook = null;

     private Map<String, Sheet> sheetMap = new HashMap<>();

     private List<String> sheetNameList = new ArrayList<>();

     private List<String> sheetNameGivenList = new ArrayList<>();

     private int sheetIndexReading = 0;

     private Sheet sheet; 

     private int sheetIndex = 0;

     private String sheetName;

     private int rowIndex = 0;

     private int rowCount;

     private int cellIndex = 0;

     private ExcelSheetFilter sheetFilter;

     private boolean inited = false;

     public ExcelReader(InputStream inputStream, String type) throws IOException {
         this.type = type;
         this.inputStream = inputStream;
         init();
     }

     @Override
     public void setSheetFilter(ExcelSheetFilter sheetFilter) {
         this.sheetFilter = sheetFilter;
     }

     private void init() throws IOException{
         if(EXCEL_XLS.equalsIgnoreCase(type)){
             workbook = new HSSFWorkbook(inputStream);
         }else if(EXCEL_XLSX.equalsIgnoreCase(type)){
             workbook = new XSSFWorkbook(inputStream);
         }else{
             throw new UnsupportedOperationException("Excel file name must end with .xls or .xlsx");
         }
         int sheetCount = workbook.getNumberOfSheets();
         for(int i = 0;i < sheetCount;i++){
             Sheet shee = workbook.getSheetAt(i);
             sheetNameList.add(shee.getSheetName());
             sheetMap.put(shee.getSheetName(), shee);
         }
         //cann't let the customer code to directly modify sheetNameList
         sheetNameGivenList.addAll(sheetNameList);
     }

     @Override
     public List<ExcelRow> readSheet() throws Exception {
         List<ExcelRow> list = new ArrayList<>();
         ExcelRow row = null;
         while((row = readRow()) != null){
             if(!row.isLastRow()){
                 list.add(row);
             }else{
                 return list;
             }
         }
         return null;
     }

     @Override
     public ExcelRow readRow() {
         if(!inited){
             inited = true;
             if(sheetFilter != null){
                 sheetFilter.resetSheetListForRead(sheetNameGivenList);
             }
             initSheet();
         }
         while(true){
             if(sheet == null){
                 return null;
             }
             if(sheetFilter != null && !sheetFilter.filter(sheetIndex, sheetName)){
                 if(++sheetIndexReading >= sheetNameGivenList.size()){
                     return null;
                 }
                 initSheet();
             }else{
                 if(rowIndex >= rowCount){
                     if(sheetIndexReading >= sheetNameGivenList.size() - 1){
                         return null;
                     }else{
                         sheetIndexReading++;
                         initSheet();
                         continue;
                     }
                 }else{
                     Row row = sheet.getRow(rowIndex);
                     rowIndex++;

                     //row not exist, don't know why
                     if(row == null){
                         ExcelRow data = new ExcelRow(rowIndex, new ArrayList<String>(0));
                         data.setSheetIndex(sheetIndex);
                         data.setSheetName(sheetName);
                         data.setEmpty(true);
                         data.setLastRow(rowIndex == rowCount);
                         return data;
                     }

                     int cellCount = row.getLastCellNum();
                     //Illegal Capacity: -1
                     if(cellCount <= 0){
                         ExcelRow data = new ExcelRow(rowIndex, new ArrayList<String>(0));
                         data.setSheetIndex(sheetIndex);
                         data.setSheetName(sheetName);
                         data.setEmpty(true);
                         data.setLastRow(rowIndex == rowCount);
                         return data;
                     }
                     List<String> list = new ArrayList<>(cellCount);

                     boolean isEmpty = true;
                     for(cellIndex = 0; cellIndex < cellCount; cellIndex++){
                         String value = getCellValue(row.getCell(cellIndex));
                         if(isEmpty && !StringUtils.isBlank(value)){
                             isEmpty = false;
                         }
                         list.add(value);
                     }
                     ExcelRow rowData = new ExcelRow(rowIndex, list);
                     rowData.setSheetIndex(sheetIndex);
                     rowData.setSheetName(sheetName);
                     rowData.setEmpty(isEmpty);
                     rowData.setLastRow(rowIndex == rowCount);
                     return rowData;
                 }
             }
         }
     }

     private void initSheet(){
         rowIndex = 0;
         sheetName = sheetNameGivenList.get(sheetIndexReading);
         sheetIndex = sheetNameList.indexOf(sheetName) + 1;
         while((sheet = sheetMap.get(sheetName)) == null){
             sheetIndexReading++;
             if(sheetIndexReading >= sheetNameGivenList.size()){
                 sheet = null;
                 return;
             }else{
                 sheetName = sheetNameGivenList.get(sheetIndexReading);
                 sheetIndex = sheetNameList.indexOf(sheetName);
             }
         }
         rowCount = sheet.getLastRowNum() + 1;//poi row num start with 0
     }

     private String getCellValue(Cell cell) {
         if (cell == null) {
             return "";
         }

         switch (cell.getCellType()) {
         case NUMERIC:
             double value = cell.getNumericCellValue();
             if(DateUtil.isCellDateFormatted(cell)){
                 Date date = DateUtil.getJavaDate(value);
                 return String.valueOf(date.getTime());
             }else{
                 try{
                     return double2String(value);
                 }catch(Exception e){
                     LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex, e);
                     return String.valueOf(value);
                 }
             }
         case STRING:
             return cell.getStringCellValue();
         case BOOLEAN:
             return String.valueOf(cell.getBooleanCellValue());
         case FORMULA:
             try {
                 return double2String(cell.getNumericCellValue());
             } catch (IllegalStateException e) {
                 try {
                     return cell.getRichStringCellValue().toString();
                 } catch (IllegalStateException e2) {
                     LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex, e2);
                     return "";
                 }
             } catch (Exception e) {
                 LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex, e);
                 return "";
             }
         case BLANK:
             return "";
         case ERROR:
             LOG.error("Excel format error: sheet=" + sheetName + ",row=" + rowIndex + ",column=" + cellIndex);
             return "";
         default:
             return "";
         }
     }

     static String double2String(Double d) {
         return formatDouble(d.toString());
     }

     static String formatDouble(String doubleStr) {
         boolean b = doubleStr.contains("E");
         int indexOfPoint = doubleStr.indexOf('.');
         if (b) {
             int indexOfE = doubleStr.indexOf('E');
             BigInteger xs = new BigInteger(doubleStr.substring(indexOfPoint + BigInteger.ONE.intValue(), indexOfE));
             int pow = Integer.parseInt(doubleStr.substring(indexOfE + BigInteger.ONE.intValue()));
             int xsLen = xs.toByteArray().length;
             int scale = xsLen - pow > 0 ? xsLen - pow : 0;
             doubleStr = String.format("%." + scale + "f", doubleStr);
         } else {
             Pattern p = Pattern.compile(".0$");
             Matcher m = p.matcher(doubleStr);
             if (m.find()) {
                 doubleStr = doubleStr.replace(".0", "");
             }
         }
         return doubleStr;
     }

     @Override
     public void close() throws IOException {
         IoUtil.close(workbook);
         IoUtil.close(inputStream);
     }
 }

4、使用方式

 public static void test() throws Exception{
     IExcelReader reader = new ExcelReader("D:/1.xlsx");
     reader.setSheetFilter(new ExcelSheetFilter(){
         @Override
         public boolean filter(int sheetIndex, String sheetName) {
             return true;
         }

         @Override
         public void resetSheetListForRead(List<String> nameList) {
             nameList.remove(0);
         }
     });

     ExcelRow row = null;
     while((row = reader.readRow()) != null){
         System.out.println(row);
     }
     reader.close();
20 }

readSheet()的使用方式与readRow()相同。通过将Workbook交给ExcelReader维护,我们可以直接面向数据,而不用去处理Excel的结构,也不用维护读取的状态。

5、存在问题

内存占用问题:上面提到Workbook在初始化的时候就解析了Excel,它将整个流全部读取并解析完成后维护在内存中,我们对它的读取其实就是对它结果的遍历,这种方式对内存的消耗大得超乎想象!

引用代码详见:https://github.com/shanhm1991/fom

利用poi包装一个简单的Excel读取器.一(适配一个Reader并提供readLine方法)的更多相关文章

  1. 【自动化测试】搭建一个简单从Excel读取用例内容并输出结果的脚本

    # -*- coding:utf-8 -*- from selenium import webdriver import xlrd import xlwt from xlutils.copy impo ...

  2. 用java从0生成一个简单的excel

    用java从0生成一个简单的excel 目标 用代码实现对一个excel的基础操作,包括创建,插入文字,(好像就这些了),生成的excel可以用wps打开,如果直接用c++的文件流会生成假的xls表格 ...

  3. 自己动手实现一个简单的JSON解析器

    1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...

  4. 一个简单的json解析器

    实现一个简单地json解析器. 两部分组成,词法分析.语法分析 词法分析 package com.mahuan.json; import java.util.LinkedList; import ja ...

  5. 用c#自己实现一个简单的JSON解析器

    一.JSON格式介绍 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着很多优点.例如易读性更好,占用空间更 ...

  6. 使用lua实现一个简单的事件派发器

    设计一个简单的事件派发器,个人觉得最重要的一点就是如何保证事件派发过程中,添加或删除同类事件,不影响事件迭代顺序和结果,只要解决这一点,其它都好办. 为了使用pairs遍历函数,重写了pairs(lu ...

  7. Arachnid包含一个简单的HTML剖析器能够分析包含HTML内容的输入流

    Arachnid是一个基于Java的web spider框架.它包含一个简单的HTML剖析器能够分析包含HTML内容的输入流.通过实现Arachnid的子类就能够开发一个简单的Web spiders并 ...

  8. 使用Python制作一个简单的刷博器

    呵呵,不得不佩服Python的强大,寥寥几句代码就能做一个简单的刷博器. import webbrowser as web import time import os count=0 while co ...

  9. 一个简单的excel文件上传到数据库方法

    因为以前项目中有用到过Excel导入,所以整理了一下,这是一个导入Excel数据到数据库的方法 注意:需要导入poi jar包 代码清单 /** * Excel 导入 * @param mapping ...

随机推荐

  1. 【iOS】UITableViewDelegate 方法没有调用

    可能原因:没有调用 reloadData 方法. [self.tableView reloadData];

  2. Could not load NIB in bundle: 'NSBundle.....

    学习NSNotification时遇到了这个问题,错误日志如下: 2015-08-28 17:47:24.617 NSNotificationDemo[7158:786614] *** Termina ...

  3. Thinkphp 5.1.7 parseData缺陷导致insert/update注入 分析

    目录 环境搭建 分析 参考 环境搭建 $ composer create-project topthink/think thinkphp-5.1.7 修改composer.json 5.1.* =&g ...

  4. Where is the clone one and how to extract it?

    One cannot be in two places at once. Do you know what's "Dual Apps"? Manufactures like Xia ...

  5. Mobile game forensics

    My friend Carrie'd like to know "Garena 传说对决" violates any mobile risks such as insecure d ...

  6. DesignPattern系列__04里氏替换原则

    1.内容引入--继承体系的思考 在继承中,凡是在父类已经实现的方法,其实算是一种契约或者规范,子类不应该在进行更改(重写):但是,由于这一点不是强制要求,所以当子类进行重写的时候,就会对继承体系产生破 ...

  7. Vue+Typescript中在Vue上挂载axios使用时报错

    Vue+Typescript中在Vue上挂载axios使用时报错 在vue项目开发过程中,为了方便在各个组件中调用axios,我们通常会在入口文件将axios挂载到vue原型身上,如下: main.t ...

  8. GDOI#348大陆争霸[SDOI2010]最短路有限制条件

    在一个遥远的世界里有两个国家:位于大陆西端的杰森国和位于大陆东端的 克里斯国.两个国家的人民分别信仰两个对立的神:杰森国信仰象征黑暗和毁灭 的神曾·布拉泽,而克里斯国信仰象征光明和永恒的神斯普林·布拉 ...

  9. android ——Tablayout

    Tabs make it easy to explore and switch between different views. 通过TabLayout可以在一个活动中通过滑动或者点击切换到不同的页面 ...

  10. CODING 告诉你如何建立一个 Scrum 团队

    原文地址:https://www.atlassian.com/agile/scrum/roles 翻译君:CODING 敏杰小王子 Scrum 当中有三个角色:PO(product owner),敏捷 ...