利用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 ...
随机推荐
- bitset的简单用法
1.头文件 #include<bitset> 2.基本操作 bitset<n> b; b有n位,每位都为0. 参数n可以为一个表达式.如bitset<5> b, 则 ...
- StringBuffer类的delete()方法和deleteCharAt()方法的区别
引言 StringBuffer类的delete()方法和deleteCharAt()方法都是用来删除StringBuffer字符串中的字符 区别 1.对于delete(int start,int en ...
- 编写自定义 .NET Core 主机以从本机代码控制 .NET 运行时
自定义 .Net Core 主机运行.Net Core代码,以及控制运行时运行状态,是在.Net Core 高级运行环境以及定制.Net Host ,CLR 等必不可少的. 这些设置包括为 1 ...
- 关于STM32GPIO按键上下拉配置的认识
说真的,后知后觉这个问题还是有点值得研究的,一开始学习我用的板子在按键模块电路中GPIO输入脚是有外部上下拉电阻的,如下图所示:当KEY1接V3.3,在其后为它接一个下拉电阻,可以保证按下按键输入高电 ...
- Kubernetes容器集群管理环境 - 完整部署(中篇)
接着Kubernetes容器集群管理环境 - 完整部署(上篇)继续往下部署: 八.部署master节点master节点的kube-apiserver.kube-scheduler 和 kube-con ...
- 如何在Vue项目中使用vw实现移动端适配
有关于移动端的适配布局一直以来都是众说纷纭,对应的解决方案也是有很多种.在< 使用Flexible实现手淘H5页面的终端适配>提出了Flexible的布局方案,随着 viewport 单位 ...
- WinForm开发中通用附件管理控件设计开发参考
1.引言 在WinForm开发中,文件附件的管理几乎在任何一个应用上都会存在,是一个非常通用集中的公共模块.我们日常记录会伴随着有图片.文档等附件形式来展现,如果为每个业务对象都做一个附件管理,或者每 ...
- 49.Qt-网络编程之QTCPSocket和QTCPServer(实现简易网络调试助手)
在上章 48.QT-网络通信讲解1,我们学习了网络通信基础后,本章便来实战一篇.源码正在上传中,等下贴地址. PS:支持客户端和服务器,提供源码,并且服务器支持多客户端连入,并且可以指定与个别客户端发 ...
- 初识代理——Proxy
无处不在的模式——Proxy 最近在看<设计模式之禅>,看到代理模式这一章的时候,发现自己在写spring项目的时候其实很多时候都用到了代理,无论是依赖注入.AOP还是其他,可以说是无处不 ...
- Java 8原生API也可以开发响应式代码?
前段时间工作上比较忙,这篇文章一直没来得及写,本文是阅读<Java8实战>的时候,了解到Java 8里已经提供了一个异步非阻塞的接口(CompletableFuture),可以实现简单的响 ...