利用poi包装一个简单的Excel读取器.一(适配一个Reader并提供readLine方法)
通常,读文本我们会使用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方法)的更多相关文章
- 【自动化测试】搭建一个简单从Excel读取用例内容并输出结果的脚本
# -*- coding:utf-8 -*- from selenium import webdriver import xlrd import xlwt from xlutils.copy impo ...
- 用java从0生成一个简单的excel
用java从0生成一个简单的excel 目标 用代码实现对一个excel的基础操作,包括创建,插入文字,(好像就这些了),生成的excel可以用wps打开,如果直接用c++的文件流会生成假的xls表格 ...
- 自己动手实现一个简单的JSON解析器
1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...
- 一个简单的json解析器
实现一个简单地json解析器. 两部分组成,词法分析.语法分析 词法分析 package com.mahuan.json; import java.util.LinkedList; import ja ...
- 用c#自己实现一个简单的JSON解析器
一.JSON格式介绍 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着很多优点.例如易读性更好,占用空间更 ...
- 使用lua实现一个简单的事件派发器
设计一个简单的事件派发器,个人觉得最重要的一点就是如何保证事件派发过程中,添加或删除同类事件,不影响事件迭代顺序和结果,只要解决这一点,其它都好办. 为了使用pairs遍历函数,重写了pairs(lu ...
- Arachnid包含一个简单的HTML剖析器能够分析包含HTML内容的输入流
Arachnid是一个基于Java的web spider框架.它包含一个简单的HTML剖析器能够分析包含HTML内容的输入流.通过实现Arachnid的子类就能够开发一个简单的Web spiders并 ...
- 使用Python制作一个简单的刷博器
呵呵,不得不佩服Python的强大,寥寥几句代码就能做一个简单的刷博器. import webbrowser as web import time import os count=0 while co ...
- 一个简单的excel文件上传到数据库方法
因为以前项目中有用到过Excel导入,所以整理了一下,这是一个导入Excel数据到数据库的方法 注意:需要导入poi jar包 代码清单 /** * Excel 导入 * @param mapping ...
随机推荐
- 【iOS】打印方法名
为了便于追踪程序运行过程,可以在日志打印方法名,示例代码如下: NSLog(@"%@", NSStringFromSelector(_cmd)); 结果如图所示: 此外,在多个中, ...
- Mysql执行过程总结
总分三个阶段:Sql的解析,执行和结果获取阶段. 如下图,展开相熟.
- Codis与RedisCluster的原理详解
背景介绍 我们先来看一下为什么要做集群,如果我们要部署一个单节点Redis,很明显会遇到单点故障的问题. 首先能想到解决单点故障的方法,就是做主从,但是当有海量存储需求时,单一的主从结构就会出问题,说 ...
- HttpsUtils
package io.renren.modules.jqr.util; import java.io.BufferedReader; import java.io.InputStream; impor ...
- c#小灶——标识符和关键字
标识符 我们之前说,命名空间的名字是自己取的,类名也是自己取的,方法名也是自己取的,以后还有各种常量.变量.对象……这些名字是自己取的.这些名字,就是标识符. 标识符规则: 标识符可以包含大小写字母. ...
- table 表格 细边框 最简单样式
table 表格细边框的效果实现方法虽然不难,但网上简单有效的方法却很少,在此记录一下 css 样式 /** table 细边框 **/ table{border-collapse: collapse ...
- 一个基于TCP/IP的服务器与客户端通讯的小项目(超详细版)
1.目的:实现客户端向服务器发送数据 原理: 2.建立两个控制台应用,一个为服务器,用于接收数据.一个为客户端,用于发送数据. 关键类与对应方法: 1)类IPEndPoint: 1.是抽象类EndPo ...
- java的八种数据类型
1)四种整数类型(byte.short.int.long): byte:8 位,用于表示最小数据单位,如文件中数据,-128~127 short:16 位,很少用,-32768 ~ 327 ...
- 『开发技术』GPU训练加速原理(附KerasGPU训练技巧)
0.深入理解GPU训练加速原理 我们都知道用GPU可以加速神经神经网络训练(相较于CPU),具体的速度对比可以参看我之前写的速度对比博文: [深度应用]·主流深度学习硬件速度对比(CPU,GPU,TP ...
- 最新 Flutter 团队工程师中文演讲 | Flutter 的性能测试和理论
本视频为 Google Flutter 团队的软件工程师 Xiao Yu 在 2018 谷歌开发者大会做的演讲,演讲题目是<Flutter 的性能测试和理论>. 这个视频里将会通过近半个小 ...