关于excel:聊聊Excel解析如何处理百万行EXCEL文件-京东云技术团队

10次阅读

共计 17108 个字符,预计需要花费 43 分钟才能阅读完成。

一、引言

Excel 表格在后盾管理系统中应用十分宽泛,多用来进行批量配置、数据导出工作。在日常开发中,咱们也免不了进行 Excel 数据处理。

那么,如何失当地解决数据量宏大的 Excel 文件,防止内存溢出问题?本文将比照剖析业界支流的 Excel 解析技术,并给出解决方案。

如果这是您第一次接触 Excel 解析,建议您从第二章理解本文根底概念;如果您曾经对 POI 有所理解,请跳转第三章浏览本文重点内容。

二、根底篇 -POI

说到 Excel 读写,就离不开这个圈子的的老大哥——POI。

Apache POI 是一款 Apache 软件基金会用 Java 编写的收费开源的跨平台的 Java API,全称 Poor Obfuscation Implementation,“简洁版的含糊实现”。它反对咱们用 Java 语言和包含 Word、Excel、PowerPoint、Visio 在内的所有 Microsoft Office 文档交互,进行数据读写和批改操作。

(1)“蹩脚”的电子表格

在 POI 中,每种文档都有一个与之对应的文档格局,如 97-2003 版本的 Excel 文件(.xls),文档格局为 HSSF——Horrible SpreadSheet Format,意为“蹩脚的电子表格格局”。尽管 Apache 风趣而虚心地将本人的 API 冠以“蹩脚”之名,不过这的确是一款全面而弱小的 API。

以下是局部“蹩脚”的 POI 文档格局,包含 Excel、Word 等:

Office 文档 对应 POI 格局
Excel (.xls) HSSF (Horrible SpreadSheet Format)
Word (.doc) HWPF (Horrible Word Processor Format)
Visio (.vsd) HDGF (Horrible DiaGram Format)
PowerPoint(.ppt) HSLF(Horrible Slide Layout Format)

(2)OOXML 简介

微软在 Office 2007 版本推出了基于 XML 的技术规范:Office Open XML,简称 OOXML。不同于老版本的二进制存储,在新标准下,所有 Office 文档都应用了 XML 格局书写,并应用 ZIP 格局进行压缩存储,大大晋升了规范性,也进步了压缩率,放大了文件体积,同时反对向后兼容。简略来说,OOXML 定义了如何用一系列的 XML 文件来示意 Office 文档。

Xlsx 文件的实质是 XML

让咱们看看一个采纳 OOML 规范的 Xlsx 文件的形成。咱们右键点击一个 Xlsx 文件,能够发现它能够被 ZIP 解压工具解压(或间接批改扩大名为.zip 后解压),这阐明:Xlsx 文件是用 ZIP 格局压缩的。解压后,能够看到如下目录格局:

关上其中的“/xl”目录,这是这个 Excel 的次要构造信息:

其中 workbook.xml 存储了整个 Excel 工作簿的构造,蕴含了几张 sheet 表单,而每张表单构造存储在 /wooksheets 文件夹中。styles.xml 寄存单元格的格局信息,/theme 文件夹寄存一些预约义的字体、色彩等数据。为了缩小压缩体积,表单中所有的字符数据被对立寄存在 sharedStrings.xml 中。通过剖析不难发现,Xlsx 文件的主体数据都以 XML 格局书写。

XSSF 格局

为了反对新规范的 Office 文档,POI 也推出了一套兼容 OOXML 规范的 API,称作 poi-ooxml。如 Excel 2007 文件(.xlsx)对应的 POI 文档格局为 XSSF(XML SpreadSheet Format)。

以下是局部 OOXML 文档格局:

Office 文档 对应 POI 格局
Excel (.xlsx) XSSF (XML SpreadSheet Format)
Word (.docx) XWPF (XML Word Processor Format)
Visio (.vsdx) XDGF (XML DiaGram Format)
PowerPoint (.pptx) XSLF (XML Slide Layout Format)

(3)UserModel

在 POI 中为咱们提供了两种解析 Excel 的模型,UserModel(用户模型)和 EventModel(事件模型)。两种解析模式都能够解决 Excel 文件,但解析形式、解决效率、内存占用量都不尽相同。最简略和实用的当属 UserModel。

UserModel & DOM 解析

用户模型定义了如下接口:

  1. Workbook- 工作簿,对应一个 Excel 文档。依据版本不同,有 HSSFWorkbook、XSSFWorkbook 等类。
  2. Sheet- 表单,一个 Excel 中的若干个表单,同样有 HSSFSheet、XSSFSheet 等类。
  3. Row- 行,一个表单由若干行组成,同样有 HSSFRow、XSSFRow 等类。
  4. Cell- 单元格,一个行由若干单元格组成,同样有 HSSFCell、XSSFCell 等类。

能够看到,用户模型非常贴合 Excel 用户的习惯,易于了解,就像咱们关上一个 Excel 表格一样。同时用户模型提供了丰盛的 API,能够反对咱们实现和 Excel 中一样的操作,如创立表单、创立行、获取表的行数、获取行的列数、读写单元格的值等。

为什么 UserModel 反对咱们进行如此丰盛的操作?因为在 UserModel 中,Excel 中的所有 XML 节点都被解析成了一棵 DOM 树,整棵 DOM 树都被加载进内存,因而能够进行不便地对每个 XML 节点进行 随机拜访

UserModel 数据转换

理解了用户模型,咱们就能够间接应用其 API 进行各种 Excel 操作。当然,更不便的方法是应用用户模型将一个 Excel 文件转化成咱们想要的 Java 数据结构,更好地进行数据处理。

咱们很容易想到关系型数据库——因为二者的本质是一样的。类比数据库的数据表,咱们的思路就有了:

  1. 将一个 Sheet 看作表头和数据两局部,这二者别离蕴含表的构造和表的数据。
  2. 对表头(第一行),校验表头信息是否和实体类的定义的属性匹配。
  3. 对数据(残余行),从上向下遍历每一个 Row,将每一行转化为一个对象,每一列作为该对象的一个属性,从而失去一个对象列表,该列表蕴含 Excel 中的所有数据。

接下来咱们就能够依照咱们的需要解决咱们的数据了,如果想把操作后的数据写回 Excel,也是一样的逻辑。

应用 UserModel

让咱们看看如何应用 UserModel 读取 Excel 文件。此处应用 POI 4.0.0 版本,首先引入 poi 和 poi-ooxml 依赖:

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>4.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>4.0.0</version>
    </dependency>

咱们要读取一个简略的 Sku 信息表,内容如下:

如何将 UserModel 的信息转化为数据列表?

咱们能够通过实现反射 + 注解的形式定义表头到数据的映射关系,帮忙咱们实现 UserModel 到数据对象的转换。实现基本思路是:① 自定义注解,在注解中定义列号,用来标注实体类的每个属性对应在 Excel 表头的第几列。② 在实体类定义中,依据表构造,为每个实体类的属性加上注解。③ 通过反射,获取实体类的每个属性对应在 Excel 的列号,从而到相应的列中获得该属性的值。

以下是简略的实现,首先筹备自定义注解 ExcelCol,其中蕴含列号和表头:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelCol {

    /**
     * 当前列数
     */
    int index() default 0;

    /**
     * 当前列的表头名称
     */
    String header() default "";}

接下来,依据 Sku 字段定义 Sku 对象,并增加注解,列号别离为 0,1,2,并指定表头名称:

import lombok.Data;
import org.shy.xlsx.annotation.ExcelCol;

@Data
public class Sku {@ExcelCol(index = 0, header = "sku")
    private Long id;

    @ExcelCol(index = 1, header = "名称")
    private String name;

    @ExcelCol(index = 2, header = "价格")
    private Double price;
}

而后,用反射获取表头的每一个 Field,并以列号为索引,存入 Map 中。从 Excel 的第二行开始(第一行是表头),遍历前面的每一行,对每一行的每个属性,依据列号拿到对应 Cell 的值,并为数据对象赋值。依据单元格中值类型的不同,如文本 / 数字等,进行不同的解决。以下为了简化逻辑,只对表头呈现的类型进行了解决,其余状况的解决逻辑相似。全副代码如下:

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.shy.domain.pojo.Sku;
import org.shy.xlsx.annotation.ExcelCol;

import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MyUserModel {public static void main(String[] args) throws Exception {List<Sku> skus = parseSkus("D:\sunhaoyu8\Documents\Files\skus.xlsx");
        System.out.println(JSON.toJSONString(skus));
    }

    public static List<Sku> parseSkus(String filePath) throws Exception {FileInputStream in = new FileInputStream(filePath);
        Workbook wk = new XSSFWorkbook(in);
        Sheet sheet = wk.getSheetAt(0);
        // 转换成的数据列表
        List<Sku> skus = new ArrayList<>();

        // 获取 Sku 的注解信息
        Map<Integer, Field> fieldMap = new HashMap<>(16);
        for (Field field : Sku.class.getDeclaredFields()) {ExcelCol col = field.getAnnotation(ExcelCol.class);
            if (col == null) {continue;}
            field.setAccessible(true);
            fieldMap.put(col.index(), field);
        }

        for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {Row r = sheet.getRow(rowNum);
            Sku sku = new Sku();
            for (int cellNum = 0; cellNum < fieldMap.size(); cellNum++) {Cell c = r.getCell(cellNum);
                if (c != null) {setFieldValue(fieldMap.get(cellNum), getCellValue(c), sku);
                }
            }
            skus.add(sku);
        }
        return skus;
    }

    public static void setFieldValue(Field field, String value, Sku sku) throws Exception {if (field == null) {return;}
        // 失去此属性的类型
        String type = field.getType().toString();
        if (StringUtils.isBlank(value)) {field.set(sku, null);
        } else if (type.endsWith("String")) {field.set(sku, value);
        } else if (type.endsWith("long") || type.endsWith("Long")) {field.set(sku, Long.parseLong(value));
        } else if (type.endsWith("double") || type.endsWith("Double")) {field.set(sku, Double.parseDouble(value));
        } else {field.set(sku, value);
        }
    }

    public static String getCellValue(Cell cell) {DecimalFormat df = new DecimalFormat("#.##");
        if (cell == null) {return "";}
        switch (cell.getCellType()) {
            case NUMERIC:
                return df.format(cell.getNumericCellValue());
            case STRING:
                    return cell.getStringCellValue().trim();
            case BLANK:
                return null;
        }
        return "";
    }

最初,将转换实现的数据列表打印进去。运行后果如下:

[{"id":345000,"name":"电脑 A","price":5999.0},{"id":345001,"name":"手机 C","price":4599.0}]

Tips:如果您的程序呈现“NoClassDefFoundError”,请引入 ooxml-schemas 依赖:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>ooxml-schemas</artifactId>
    <version>1.4</version>
</dependency>

版本抉择见下表,如 POI 4.0.0 对应 ooxml-schemas 1.4 版本:

UserModel 的局限

以上解决逻辑对于大部分的 Excel 文件都很实用,但最大的毛病是内存开销大,因为所有的数据都被加载入内存。实测,以上 3 列的 Excel 文件在 7 万行左右就会呈现 OOM,而 XLS 文件最大行数为 65535 行,XLSX 更是达到了 1048576 行,如果将几万甚至百万级别的数据全副读入内存,内存溢出危险极高。

那么,该如何解决传统 UserModel 无奈解决大批量 Excel 的问题呢?开发者们给出了许多精彩的解决方案,请看下一章。

三、进阶篇 - 内存优化的摸索

接下来介绍本文重点内容,同时解决本文所提出的问题:如何进行 Excel 解析的内存优化,从而解决百万行 Excel 文件?

(1)EventModel

后面咱们提到,除了 UserModel 外,POI 还提供了另一种解析 Excel 的模型:EventModel 事件模型。不同于用户模型的 DOM 解析,事件模型采纳了 SAX 的形式去解析 Excel。

EventModel & SAX 解析

SAX 的全称是 Simple API for XML,是一种基于事件驱动的 XML 解析办法。不同于 DOM 一次性读入 XML,SAX 会采纳边读取边解决的形式进行 XML 操作。简略来讲,SAX 解析器会逐行地去扫描 XML 文档,当遇到标签时会触发解析处理器,从而触发相应的事件 Handler。咱们要做的就是继承 DefaultHandler 类,重写一系列事件处理办法,即可对 Excel 文件进行相应的解决。

上面是一个简略的 SAX 解析的示例,这是要解析的 XML 文件:一个 sku 表,其中蕴含两个 sku 节点,每个节点有一个 id 属性和三个子节点。

<?xml version="1.0" encoding="UTF-8"?>
<skus>
    <sku id="345000">
        <name> 电脑 A </name>
        <price>5999.0</price>
   </sku>
    <sku id="345001">
        <name> 手机 C </name>
        <price>4599.0</price>
   </sku>
</skus>

对照 XML 构造,创立 Java 实体类:

import lombok.Data;

@Data
public class Sku {
    private Long id;
    private String name;
    private Double price;
}

自定义事件处理类 SkuHandler:

import com.alibaba.fastjson.JSON;
import org.shy.domain.pojo.Sku;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class SkuHandler extends DefaultHandler {
    /**
     * 以后正在解决的 sku
     */
    private Sku sku;
    /**
     * 以后正在解决的节点名称
     */
    private String tagName;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {if ("sku".equals(qName)) {sku = new Sku();
            sku.setId(Long.valueOf((attributes.getValue("id"))));
        }
        tagName = qName;
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {if ("sku".equals(qName)) {System.out.println(JSON.toJSONString(sku));
            // 解决业务逻辑
            // ...
        }
        tagName = null;
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {if ("name".equals(tagName)) {sku.setName(new String(ch, start, length));
        }
        if ("price".equals(tagName)) {sku.setPrice(Double.valueOf(new String(ch, start, length)));
        }
    }
}

其中,SkuHandler 重写了三个事件响应办法:

startElement()——每当扫描到新 XML 元素时,调用此办法,传入 XML 标签名称 qName,XML 属性列表 attributes;

characters()——每当扫描到未在 XML 标签中的字符串时,调用此办法,传入字符数组、起始下标和长度;

endElement()——每当扫描到 XML 元素的完结标签时,调用此办法,传入 XML 标签名称 qName。

咱们用一个变量 tagName 存储以后扫描到的节点信息,每次扫描节点发送变动时,更新 tagName;

用一个 Sku 实例保护以后读入内存的 Sku 信息,每当该 Sku 读取实现时,咱们打印该 Sku 信息,并执行相应业务逻辑。这样,就能够做到一次读取一条 Sku 信息,边解析边解决。因为每行 Sku 构造雷同,因而,只须要在内存保护一条 Sku 信息即可,防止了一次性把所有信息读入内存。

调用 SAX 解析器时,应用 SAXParserFactory 创立解析器实例,解析输出流即可,Main 办法如下:

import org.shy.xlsx.sax.handler.SkuHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.InputStream;

public class MySax {public static void main(String[] args) throws Exception {parseSku();
    }

    public static void parseSku() throws Exception {SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        SAXParser saxParser = saxParserFactory.newSAXParser();
        InputStream inputStream = ClassLoader.getSystemResourceAsStream("skus.xml");
        saxParser.parse(inputStream, new SkuHandler());
    }
}

输入后果如下:

{"id":345000,"name":"电脑 A","price":5999.0}
{"id":345001,"name":"手机 C","price":4599.0}

以上演示了 SAX 解析的根底原理。EventModel 的 API 更简单,同样通过重写 Event handler,实现 SAX 解析。有趣味的读者,请参见 POI 官网的示例代码:https://poi.apache.org/components/spreadsheet/how-to.html

EventModel 的局限

POI 官网提供的 EventModel API 尽管应用 SAX 形式解决了 DOM 解析的问题,然而存在一些局限性:

① 属于 low level API,形象级别低,绝对比较复杂,学习应用老本高。

② 对于 HSSF 和 XSSF 类型的解决形式不同,代码须要依据不同类型别离做兼容。

③ 未能完满解决内存溢出问题,内存开销仍有优化空间。

④ 仅用于 Excel 解析,不反对 Excel 写入。

因而,笔者 不倡议应用 POI 原生的 EventModel,至于有哪些更举荐的工具,请看下文。

(2)SXSSF

SXSSF 简介

SXSSF,全称 Streaming XML SpreadSheet Format,是 POI 3.8-beta3 版本后推出的低内存占用的流式 Excel API,旨在解决 Excel 写入时的内存问题。它是 XSSF 的扩大,当须要将大批量数据写入 Excel 中时,只须要用 SXSSF 替换 XSSF 即可。SXSSF 的原理是滑动窗口——在内存中保留肯定数量的行,其余行存储在磁盘。这么做的益处是内存优化,代价是失去了随机拜访的能力。SXSSF 能够兼容 XSSF 的绝大多数 API,非常适合理解 UserModel 的开发者。

内存优化会难以避免地带来肯定限度:

① 在某个工夫点只能拜访无限数量的行,因为其余行并未被加载入内存。

② 不反对须要随机拜访的 XSSF API,如删除 / 挪动行、克隆 sheet、公式计算等。

③ 不反对 Excel 读取操作。

④ 正因为它是 XSSF 的扩大,所以不反对写入 Xls 文件。

UserModel、EventModel、SXSSF 比照

到这里就介绍完了所有的 POI Excel API,下表是所有这些 API 的性能比照,来自 POI 官网:

能够看到,UserModel 基于 DOM 解析,性能是最齐全的,反对随机拜访,惟一毛病是 CPU 和内存效率不稳固;

EventModel 是 POI 提供的流式读取计划,基于 SAX 解析,仅反对向前拜访,其余 API 不反对;

SXSSF 是 POI 提供的流式写入计划,同样仅能向前拜访,反对局部 XSSF API。

(3)EasyExcel

EasyExcel 简介

为了解决 POI 原生的 SAX 解析的问题,阿里基于 POI 二次开发了 EasyExcel。上面是援用自 EasyExcel 官网的介绍:

Java 解析、生成 Excel 比拟有名的框架有 Apache poi、jxl。但他们都存在一个重大的问题就是十分的耗内存,poi 有一套 SAX 模式的 API 能够肯定水平的解决一些内存溢出的问题,但 POI 还是有一些缺点,比方 07 版 Excel 解压缩以及解压后存储都是在内存中实现的,内存耗费仍然很大。easyexcel 重写了 poi 对 07 版 Excel 的解析,一个 3M 的 excel 用 POI sax 解析仍然须要 100M 左右内存,改用 easyexcel 能够升高到几 M,并且再大的 excel 也不会呈现内存溢出;03 版依赖 POI 的 sax 模式,在下层做了模型转换的封装,让使用者更加简略不便。

如介绍所言,EasyExcel 同样采纳 SAX 形式解析,但因为重写了 xlsx 的 SAX 解析,优化了内存开销;对 xls 文件,在下层进一步进行了封装,升高了应用老本。API 上,采纳注解的形式去定义 Excel 实体类,使用方便;通过事件监听器的形式做 Excel 读取,相比于原生 EventModel,API 大大简化;写入数据时,EasyExcel 对少量数据,通过反复屡次写入的形式从而升高内存开销。

EasyExcel 最大的劣势是应用简便,十分钟能够上手。因为对 POI 的 API 都做了高级封装,所以适宜不想理解 POI 根底 API 的开发者。总之,EasyExcel 是一款值得一试的 API。

应用 EasyExcel

引入 easyexcel 依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.3</version>
</dependency>

首先,用注解定义 Excel 实体类:

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class Sku {@ExcelProperty(index = 0)
    private Long id;

    @ExcelProperty(index = 1)
    private String name;

    @ExcelProperty(index = 2)
    private Double price;
}

接下来,重写 AnalysisEventListener 中的 invoke 和 doAfterAllAnalysed 办法,这两个办法别离在监听到单行解析实现的事件时和全副解析实现的事件时调用。每次单行解析实现时,咱们打印解析后果,代码如下:

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import org.shy.domain.pojo.easyexcel.Sku;

public class MyEasyExcel {public static void main(String[] args) {parseSku();
    }

    public static void parseSku() {
        // 读取文件门路
        String fileName = "D:\sunhaoyu8\Documents\Files\excel.xlsx";
        // 读取 excel
        EasyExcel.read(fileName, Sku.class, new AnalysisEventListener<Sku>() {
            @Override
            public void invoke(Sku sku, AnalysisContext analysisContext) {System.out.println("第" + analysisContext.getCurrentRowNum() + "行:" + JSON.toJSONString(sku));
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {System.out.println("全副解析实现");
            }
        }).sheet().doRead();
    }
}

测验一下,用它解析一个十万行的 excel,该文件用 UserModel 读取会 OOM,如下:

运行后果:

(4)Xlsx-streamer

Xlsx-streamer 简介

Xlsx-streamer 是一款用于流式读取 Excel 的工具,同样基于 POI 二次开发。尽管 EasyExcel 能够很好地解决 Excel 读取的问题,但解析形式为 SAX,须要通过实现监听器以事件驱动的形式进行解析。有没有其余的解析形式呢?Xlsx-streamer 给出了答案。

译自官网文档的形容:

如果您过来曾应用 Apache POI 读取 Excel 文件,您可能会留神到它的内存效率不是很高。浏览整个工作簿会导致重大的内存应用顶峰,这会对服务器造成严重破坏。Apache 必须读取整个工作簿的起因有很多,但其中大部分与该库容许您应用随机地址进行读写无关。如果(且仅当)您只想以疾速且内存高效的形式读取 Excel 文件的内容,您可能不须要此性能。可怜的是,POI 库中惟一用于读取流式工作簿的货色要求您的代码应用相似 SAX 的解析器。该 API 中短少所有敌对的类,如 Row 和 Cell。该库充当该流式 API 的包装器,同时保留规范 POI API 的语法。持续浏览,看看它是否适宜您。留神:这个库只反对读取 XLSX 文件。

如介绍所言,Xlsx-streamer 最大的便当之处是兼容了用户应用 POI UserModel 的习惯,它对所有的 UserModel 接口都给出了本人的流式实现,如 StreamingSheet、StreamingRow 等,对于相熟 UserModel 的开发者来说,简直没有学习门槛,能够间接应用 UserModel 拜访 Excel。

Xlsx-streamer 的实现原理和 SXSSF 雷同,都是滑动窗口——限定读入内存中的数据大小,将正在解析的数据读到内存缓冲区中,造成一个临时文件,以避免大量应用内存。缓冲区的内容会随着解析的过程一直变动,当流敞开后,临时文件也将被删除。因为内存缓冲区的存在,整个流不会被残缺地读入内存,从而避免了内存溢出。

与 SXSSF 一样,因为内存中仅加载入局部行,故就义了随机拜访的能力,仅能通过遍历程序拜访整表,这是不可避免的局限。换言之,如果调用 StreamingSheet.getRow(int rownum)办法,该办法会获取 sheet 的指定行,会抛出“不反对该操作”的异样。

Xlsx-streamer 最大的劣势是兼容 UserModel,尤其适宜那些相熟 UserModel 又不想应用繁琐的 EventModel 的开发者。它和 SXSSF 一样,都通过实现 UserModel 接口的形式给出解决内存问题的计划,很好地填补了 SXSSF 不反对读取的空白,能够说它是“读取版”的 SXSSF。

应用 Xlsx-streamer

引入 pom 依赖:

    <dependency>
        <groupId>com.monitorjbl</groupId>
        <artifactId>xlsx-streamer</artifactId>
        <version>2.1.0</version>
    </dependency>

上面是一个应用 xlsx-streamer 的 demo:

import com.monitorjbl.xlsx.StreamingReader;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;

import java.io.FileInputStream;

public class MyXlsxStreamer {public static void main(String[] args) throws Exception {parseSku();
    }

    public static void parseSku() throws Exception {FileInputStream in = new FileInputStream("D:\sunhaoyu8\Documents\Files\excel.xlsx");
        Workbook wk = StreamingReader.builder()
                // 缓存到内存中的行数,默认是 10
                .rowCacheSize(100)
                // 读取资源时,缓存到内存的字节大小,默认是 1024
                .bufferSize(4096)
                // 关上资源,必须,能够是 InputStream 或者是 File
                .open(in);
        Sheet sheet = wk.getSheetAt(0);

        for (Row r : sheet) {System.out.print("第" + r.getRowNum() + "行:");
            for (Cell c : r) {if (c != null) {System.out.print(c.getStringCellValue() + " ");
                }
            }
            System.out.println();}
    }
}

如代码所示,Xlsx-streamer 的应用办法为:应用 StreamingReader 进行参数配置和流式读取,咱们能够手动配置固定的滑动窗口大小,有两个指标,别离是缓存在内存中的最大行数和缓存在内存的最大字节数,这两个指标会同时限度该滑动窗口的下限。接下来,咱们能够应用 UserModel 的 API 去遍历拜访读到的表格。

应用十万行量级的 excel 文件实测一下,运行后果:

StAX 解析

Xlsx-streamer 底层采纳的解析形式,被称作 StAX 解析。StAX 于 2004 年 3 月在 JSR 173 标准中引入,是 JDK 6.0 推出的新个性。它的全称是 Streaming API for XML,流式 XML 解析。更精确地讲,称作“流式拉剖析”。之所以称作拉剖析,是因为它和“流式推剖析”——SAX 解析绝对。

之前咱们提到,SAX 解析是一种事件驱动的解析模型,每当解析到标签时都会触发相应的事件 Handler,将事件“推”给响应器。在这样的推模型中,解析器是被动,响应器是被动,咱们不能抉择想要响应哪些事件,因而这样的解析比拟不灵便。

为了解决 SAX 解析的问题,StAX 解析采纳了“拉”的形式——由解析器遍历流时,原来的响应器变成了驱动者,被动遍历事件解析器(迭代器),从中拉取一个个事件并解决。在解析过程中,StAX 反对应用 peek()办法来 ” 偷看 ” 下一个事件,从而决定是否有必要剖析下一个事件,而不用从流中读取事件。这样能够无效进步灵活性和效率。

上面用 StAX 的形式再解析一下雷同的 XML:

<?xml version="1.0" encoding="UTF-8"?>
<skus>
    <sku id="345000">
        <name> 电脑 A </name>
        <price>5999.0</price>
   </sku>
    <sku id="345001">
        <name> 手机 C </name>
        <price>4599.0</price>
   </sku>
</skus>

这次咱们不须要监听器,把所有解决的逻辑集成在一个办法中:

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.shy.domain.pojo.Sku;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.io.InputStream;
import java.util.Iterator;


public class MyStax {

    /**
     * 以后正在解决的 sku
     */
    private static Sku sku;
    /**
     * 以后正在解决的节点名称
     */
    private static String tagName;

    public static void main(String[] args) throws Exception {parseSku();
    }
    
    public static void parseSku() throws Exception {XMLInputFactory inputFactory = XMLInputFactory.newInstance();
        InputStream inputStream = ClassLoader.getSystemResourceAsStream("skus.xml");
        XMLEventReader xmlEventReader = inputFactory.createXMLEventReader(inputStream);
        while (xmlEventReader.hasNext()) {XMLEvent event = xmlEventReader.nextEvent();
            // 开始节点
            if (event.isStartElement()) {StartElement startElement = event.asStartElement();
                String name = startElement.getName().toString();
                if ("sku".equals(name)) {sku = new Sku();
                    Iterator iterator = startElement.getAttributes();
                    while (iterator.hasNext()) {Attribute attribute = (Attribute) iterator.next();
                        if ("id".equals(attribute.getName().toString())) {sku.setId(Long.valueOf(attribute.getValue()));
                        }
                    }
                }
                tagName = name;
            }
            // 字符
            if (event.isCharacters()) {String data = event.asCharacters().getData().trim();
                if (StringUtils.isNotEmpty(data)) {if ("name".equals(tagName)) {sku.setName(data);
                    }
                    if ("price".equals(tagName)) {sku.setPrice(Double.valueOf(data));
                    }
                }
            }
            // 完结节点
            if (event.isEndElement()) {String name = event.asEndElement().getName().toString();
                if ("sku".equals(name)) {System.out.println(JSON.toJSONString(sku));
                    // 解决业务逻辑
                    // ...
                }
            }
        }
    }
}

以上代码与 SAX 解析的逻辑是等价的,用 XMLEventReader 作为迭代器从流中读取事件,循环遍历事件迭代器,再依据事件类型做分类解决。有趣味的小伙伴能够本人入手尝试一下,摸索更多 StAX 解析的细节。

四、论断

EventModel、SXSSF、EasyExcel 和 Xlsx-streamer 别离针对 UserModel 的内存占用问题给出了各自的解决方案,上面是对所有本文提到的 Excel API 的比照:

  UserModel EventModel SXSSF EasyExcel Xlsx-streamer
内存占用量 较低
全表随机拜访
读 Excel
读取形式 DOM SAX SAX StAX
写 Excel

建议您依据本人的应用场景抉择适宜的 API:

  1. 解决大批量 Excel 文件的需要,举荐抉择 POI UserModel、EasyExcel;
  2. 读取大批量 Excel 文件,举荐抉择 EasyExcel、Xlsx-streamer;
  3. 写入大批量 Excel 文件,举荐抉择 SXSSF、EasyExcel。

应用以上 API,肯定能够满足对于 Excel 开发的需要。当然 Excel API 不止这些,还有许多同类型的 API,欢送大家多多摸索和翻新。

页面链接:

POI 官网:https://poi.apache.org/

EasyExcel 官网:https://easyexcel.opensource.alibaba.com

Xlsx-streamer Github:https://github.com/monitorjbl/excel-streaming-reader

作者:京东保险 孙昊宇

起源:京东云开发者社区

正文完
 0