通常,读文本我们会使用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. 第三章 JavaScript操作Dom对象

    常用的方法: 1.访问节点: 通过Document.getElementByXXX()获得一个指定节点-->再通过以下属性节点访问节点:第一部分:节点属性a:parentNode 返回节点的父节 ...

  2. 比特币and区块链

    比特币简介 比特币(Bitcoin:比特金)最早是一种网络虚拟货币,可以购买现实生活当中的物品.它的特点是分散化.匿名.只能在数字世界使用,不属于任何国家和金融机构,并且不受地域的限制,可以在世界上的 ...

  3. Mybatis使用动态sql

    动态sql 常见的几种:trim.where.set.foreach.if.choose.when 下面通过案例一一演示 if语法 <select id="selectIfTest1& ...

  4. spring boot 学习笔记(二)之打包

    一.叙述 spring boot 在 pom 中可以配置成  packaging 为 jar ,这样打包出来的就是一个 jar 包,可以通过 Java 命令直接运行, Java 命令为: java - ...

  5. 新IT运维时代 | Docker运维之最佳实践-上篇

    容器技术的发展可以分为两个阶段,第一个阶段聚焦在IaaS层,仅仅把容器当做更轻量级虚拟机来使用,解决了应用运行时进程级资源隔离的问题:随着Docker的出现,容器虚拟化才有了统一的平台,由此容器技术发 ...

  6. dubbo异常处理

    dubbo异常处理 我们的项目使用了dubbo进行不同系统之间的调用. 每个项目都有一个全局的异常处理,对于业务异常,我们会抛出自定义的业务异常(继承RuntimeException). 全局的异常处 ...

  7. Linux基础管道管理

    一.I/O重定向 标准输入,标准输出,标准错误 file descriptors (FD, 文件描述符或Process I/O channels); 进程使用文件描述符来管理打开的文件 [root@l ...

  8. 转载 | float 清除浮动的7种方法

    什么叫浮动:浮动会使当前标签脱离文档流,产生上浮的效果,同时还会影响周边元素(前后标签)及父级元素的位置和width,height属性.下面用一个小例子来看一看浮动的全过程:1.首先我们新建一个网页, ...

  9. intellIJ IDEA学习笔记

    如果你初次用idea,毫无目的的度娘如何使用IDEA     浪费的将会是大量的时间.为以表诚意, 上一套IDEA教学视频,以表我诚意.(下载地址:https://pan.baidu.com/s/1g ...

  10. 算法之《图》Java实现

    数据结构之图 定义(百度百科) 图的术语表 无向图 深度优先搜索 广度优先遍历 有向图 路径问题 调度问题 强连通性 最小生成树(无向图) 最小生成树的贪心算法 加权无向图的数据结构 Kruskal算 ...