Excel深度解析历程
故事背景
最近公司分配了一个任务,里面就有各种各样的文件解析,由于公司之前也找过外包公司开发过同样的代码,
但是xlsx格式的文件接口响应速度惨不忍睹,下面用的全部都是同一个文件测试,文件大小7.8M,行数2.1w行,
之前的版本,执行当前文件的计算耗时12秒!!!
于是乎,我就成了重新开发那个模块的“大冤种”;
技术选型方案
一、 直接采用现成的框架
1. 可以使用easypoi框架,简单易用,详情可以查看我的另一篇
博客[链接] https://blog.csdn.net/q82682810X/article/details/114314333/ ,
不出意外的话,应该旧版本就采用的是这种懒人模式,代码敲起来是快了,系统得垮了。
2. 使用阿里提供的easyExcel工具类,sax模式读取,性能倍感提升!
3. 使用apache提供的excel解析基础类,进行解析
二、 自己处理解析
这里最大的困难就是得去了解excel解析的各种api,操作excel的方式等。还得自己去摸轮子。
遇到的困难
看了上面的选型,肯定很多人第一反应就是使用阿里的框架,靠谱,性能又好。没错我一开始也是这么决定的,
但是接着就有了另外一个问题,阿里提供的easyExcel工具类不支持解析csv格式的文件解析。很不凑巧的是easypoi
他支持,(正可谓鱼和熊掌不可兼得);
那这时候肯定就有人说了,解析xlsx的时候用阿里的,csv用easypoi的不久ok了么?
后来实操的时候依赖冲突严重,两则都引入了apache的excel解析模块...而且在单元测试的时候,发现easyExcel(阿里)
的也渐渐不能满足我的“野心”了。(解析一个7.8M的xlsx文件,纯处理行读取(不做任何解析操作),2.1w行数据,耗时3.2s)
升级的思路
一、大胆的创想
会不会是因为Java语言天生的特性,根本就发挥性能达不到极致呢?顿时脑光一乍!C语言解析excel!!!
然后想着用java通过调用jni的方式去获取文件的解析值!!!
于是乎,我就在网上查各种C语言解析excel的例子,能跑通就可以做一个直接的对比。然后我东找西找,发现
一篇颇为神奇的文章:链接: [link](https://blog.csdn.net/skv00d00/article/details/123419791) ,于是乎
奇怪的只是增加了——这篇文章是解压xlsx格式的文件,然后根据里面的xml文件去做数据解析,然后一步步进入文件探究
(这里一定要注意,仅适用于xlsx格式的文档)
好家伙,原来他就是一个压缩包文件,剩下的就是怎么从解析好的文件里面去拿到自己想要的数据信息了。
经过一番简单的摸索,终于定位到了两个文件:分别是 xl/sharedStrings.xml 和 xl/worksheets/sheet1.xml 这两
个文件, 第一个文件一看名字就很有来头,共享字符串?后面经过验证,发现,所有表格里面的字符数据,
全部都可以在这个文件里面找得到,剩下的谜题就在sheet1.xml 文件中了,不出意外的话,肯定是利用的数据索引
的方式去进行字符串关联。结果果然不出我的意料!
sheet.xml 中 row标签代表的是行信息,c标签代表是新的一列,指的是column,c标签中有两个属性,第一个属性代表他属于第几列 例如:A1、BA1列这种(BA1代表BA列,第1行),还有一个s值,这个s就标识着他是不是字符串,如果是,则需要从共享字符串文件中的索引下标去获取目标值,否则他可能是数字类型,直接读取。
二、深度解析实践
第一步
先创建一个解析Excel的工具类(DeepExcelParse.class) ,如果只需要excel文件File对象,
NodeList是用作于接收sharedStrings的管理,继承的DefaultHnadler主要是用作于sheet.xml文件的sax模式读取
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.XmlUtil;
import cn.hutool.core.util.ZipUtil;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.helpers.DefaultHandler;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class DeepExcelParse extends DefaultHandler implements Closeable {
private final File tempFile;
private final List<Entity> result;
/**
* stringSharing
*/
private final NodeList nodeList;
public DeepExcelParse(File excelFile) {
tempFile = new File(excelFile.getParent(), IdUtil.fastSimpleUUID());
ZipUtil.unzip(excelFile, tempFile);
Document document = XmlUtil.readXML(
new File(tempFile, "xl" + File.separator + "sharedStrings.xml"));
this.nodeList = document.getElementsByTagName("t");
this.result = new LinkedList<>();
}
@Override
public void close() throws IOException {
FileUtil.del(tempFile);
}
}
利用到的实体对象,这里仅做DEMO展示:
@Data
private class Entity{
private String id;
private String name;
}
第二步
重写Handler解析的方法,暴露一个start开始解析的入口
public void start(){
File target = new File(tempFile, "xl" + File.separator
+ "worksheets" + File.separator + "sheet1.xml");
if (!FileUtil.exist(target)) {
throw new RuntimeException("系统异常");
}
XmlUtil.readBySax(target, this);
}
第三步
接下来就得处理真正的解析流程了,重写DefaultHandler的方法,核心的有:startElement,endElement,characters方法
这里就不具体展示解析流程(如果有需要的话,后续再更新这里的内容),
startElement 标签开始时会调用的方法,这里可以获取到标签类型 例如:<div id='id1'><span attr='abc'>test</span></div>,他会进
入两次这个方法,一次拿到的是div标签,第二次会来到span标签,都是指的是解析开始,这里只能够获取得到id和attr属性的值,
拿不到 span标签里面的 ‘test’值
endElement方法:标签结束的回调,这里也会进入两次,刚好和上面的函数以一对对的方式进行回调
characters 方法: 这里是负责获取span标签内部的内容,方法定义:characters(char[] ch, int start, int length)
如果是字符串的话,则直接通过 new String(ch,start,length);就可以解析到标签内部的内容块。
性能测试
捣腾了大半天之后,终于要迎来了我的性能测试对比时间了,也是同样的excel文档,2.1W行数据,解析耗时2.3秒!
小点:优化的点不单止是解析方式不一样,而且我在解析文件的过程中,会跳过很多我不关心的数据列,但是现成的框架
并不会知道调用者需要的是哪些字段,会逐个字段或者通过反射获取@Excel注解上面的值去决定获取哪些列的数据,但是
反射终究会有性能问题。目前优化暂告一段落。
性能提升空间
上面展示的例子还是有缺点的,最大的确定就是会直接加载整个共享字符串的内容进来,在某种情况下,他可能会很大,
进而占用了大部分的内存空间,再高并发的背景下,会频繁触发GC,并且会有OOM的风险,这时候就需要更加合适的GC
处理器了,再者就是将JDK的版本升至9或以上,java9中有个新特性是专门针对String.class做出优化,核心是减少了
一些单字节可以展示的字符串的占用空间(会有两种模式,一种是jdk8以前的,用两个字节代表一个字符,现在会考虑
动态切换占用一个字节/两个字节)
小结
在经历了这一波探索之后感觉收货良多,一是把xlsx的老底掀开了解了一遍,有发现新天地的小惊喜,有性能优化上面的提升的喜悦。由最开始的12秒,优化成6秒,到最后的2.8秒!还是有相当的大的收货的。
如果有更好的解析方式也欢迎来分享!