文章目录
-
- 1、Excel百万数据导出
- 2、Excel百万数据的导入
- 3、opencsv操作CSV文件
-
- [3.1 CSV文件简介](#3.1 CSV文件简介)
- [2.2 opencsv常用API](#2.2 opencsv常用API)
- [3.3 导出CSV文件](#3.3 导出CSV文件)
-
- [3.3.1 需求](#3.3.1 需求)
- [3.3.2 代码实现](#3.3.2 代码实现)
- [3.4 读取CSV文件](#3.4 读取CSV文件)
1、Excel百万数据导出
1.1、概述
Excel可以分为早期的Excel2003版本(使用POI的HSSF对象操作)和Excel2007版本(使用POI的XSSF操作),两者对百万数据的支持如下:
-
Excel 2003:在POI中使用HSSF对象时,excel 2003最多只允许存储65536条数据,一般用来处理较少的数据量。这时对于百万级别数据,Excel肯定容纳不了。
-
Excel 2007:当POI升级到XSSF对象时,它可以直接支持excel2007以上版本,因为它采用ooxml格式。这时excel可以支持1048576条数据,单个sheet表就支持近百万条数据。但实际运行时还可能存在问题,原因是执行POI报表所产生的行对象,单元格对象,字体对象,他们都不会销毁,这就导致OOM的风险。
1.2、解决方案分析
对于百万数据量的Excel导入导出,只讨论基于Excel2007的解决方法。在ApachePoi 官方提供了对操作大数据量的导入导出的工具和解决办法,操作Excel2007使用XSSF对象,可以分为三种模式:
-
java代码解析xml
-
dom4j:一次性加载xml文件再解析
-
SAX:逐行加载,逐行解析
-
用户模式:用户模式有许多封装好的方法操作简单,但创建太多的对象,非常耗内存(之前使用的方法)
-
事件模式:基于SAX方式解析XML,SAX全称Simple API for XML,它是一个接口,也是一个软件包。它是一种XML解析的替代方法,不同于DOM解析XML文档时把所有内容一次性加载到内存中的方式,它逐行扫描文档,一边扫描,一边解析。
-
SXSSF对象:是用来生成海量excel数据文件,主要原理是借助临时存储空间生成excel

1.3、原理分析
在实例化SXSSFWorkBook这个对象时,可以指定在内存中所产生的POI导出相关对象的数量(默认100),一旦内存中的对象的个数达到这个指定值时,就将内存中的这些对象的内容写入到磁盘中(XML的文件格式),就可以将这些对象从内存中销毁,以后只要达到这个值,就会以类似的处理方式处理,直至Excel导出完成。
1.4、百万数据的导出
1.4.1、模拟数据
- 创建表:
sql
CREATE TABLE `tb_user2` (
`id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(100) DEFAULT NULL COMMENT '姓名',
`phone` varchar(15) DEFAULT NULL COMMENT '手机号',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`salary` int(10) DEFAULT NULL,
`hire_date` datetime DEFAULT NULL COMMENT '入职日期',
`dept_id` bigint(20) DEFAULT NULL COMMENT '部门编号',
`birthday` datetime DEFAULT NULL COMMENT '出生日期',
`photo` varchar(200) DEFAULT NULL COMMENT '照片路径',
`address` varchar(300) DEFAULT NULL COMMENT '现在住址'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 创建存储过程
sql
CREATE DEFINER=`root`@`localhost` PROCEDURE `test_insert`()
BEGIN
DECLARE n int DEFAULT 1; -- 定义变量n=1
SET AUTOCOMMIT=0; -- 取消自动提交
while n <= 5000000 do
INSERT INTO `tb_user2` VALUES ( n, CONCAT('测试', n), '13800000001', '北京市', '北京市', '11000', '2001-03-01 21:18:29', '1', '1981-03-02 00:00:00', '\\static\\user_photos\\1.jpg', '北京市西城区宣武大街1号院');
SET n=n+1;
END while;
COMMIT;
END
- 开始执行
sql
CALL test_insert();
插入500W数据大概需要200至300秒左右
sql
SELECT COUNT(0) FROM tb_user2;

1.4.2、思路分析
导出时使用的是SXSSFWorkBook这个类,一个工作表sheet最多只能放1048576行数据, 当我们的业务数据已超过100万了,一个sheet就不够用了,必须拆分到多个工作表。
导出百万数据时有两个弊端:
-
不能使用模板
-
不能使用太多的样式
也就是说导出的数据太多时必须要放弃一些。
1.4.3、代码实现
UserController代码:
java
@GetMapping(value = "/downLoadMillion",name = "导出用户百万数据的导出")
public void downLoadMillion(Long id,HttpServletRequest request,HttpServletResponse response) throws Exception{
userService.downLoadMillion(request,response);
}
Service方法:
java
void downLoadMillion(HttpServletRequest request, HttpServletResponse response) throws Exception;
实现类方法:
java
// 百万数据的导出 1、肯定使用高版本的excel 2、使用sax方式解析Excel(XML)
// 限制:1、不能使用模板 2、不能使用太多的样式
@Override
public void downLoadMillion(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 指定使用的是sax方式解析
Workbook workbook = new SXSSFWorkbook(); //sax方式就是逐行解析
// Workbook workbook = new XSSFWorkbook(); //dom4j的方式
// 导出500W条数据 不可能放到同一个sheet中 规定:每个sheet不能超过100W条数据
int page = 1;
int pageSize=200000;
int rowIndex = 1; ///记录的是每个sheet的行索引
int num = 0; // 记录了处理数据的个数
Row row = null;
Cell cell = null;
Sheet sheet = null;
while (true){ //不停地查询
List<User> userList = this.findPage(page,pageSize);
if(CollectionUtils.isEmpty(userList)){ //如果查询不到就不再查询了
break;
}
if(num%1000000==0){ //每100W个就重新创建新的sheet和标题
rowIndex = 1;
// 在工作薄中创建一个工作表
sheet = workbook.createSheet("第"+((num/1000000)+1)+"个工作表");
// 设置列宽
sheet.setColumnWidth(0,8*256);
sheet.setColumnWidth(1,12*256);
sheet.setColumnWidth(2,15*256);
sheet.setColumnWidth(3,15*256);
sheet.setColumnWidth(4,30*256);
// 处理标题
String[] titles = new String[]{"编号","姓名","手机号","入职日期","现住址"};
// 创建标题行
Row titleRow = sheet.createRow(0);
for (int i = 0; i < titles.length; i++) {
cell = titleRow.createCell(i);
cell.setCellValue(titles[i]);
}
}
// 处理内容
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (User user : userList) {
row = sheet.createRow(rowIndex);
cell = row.createCell(0);
cell.setCellValue(user.getId());
cell = row.createCell(1);
cell.setCellValue(user.getUserName());
cell = row.createCell(2);
cell.setCellValue(user.getPhone());
cell = row.createCell(3);
cell.setCellValue(sdf.format(user.getHireDate()));
cell = row.createCell(4);
cell.setCellValue(user.getAddress());
rowIndex++;
num++;
}
page++;// 继续查询下一页
}
// 导出的文件名称
String filename="百万数据.xlsx";
// 设置文件的打开方式和mime类型
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader( "Content-Disposition", "attachment;filename=" + new String(filename.getBytes(),"ISO8859-1"));
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
workbook.write(outputStream);
}
1.4.4、测试结果
导出的这个文档大概需要3-5分钟的时间,有105 MB,内容如下

1.5、两种解析excel方式的比较
- Workbook workbook = new SXSSFWorkbook(); //sax方式就是逐行解析
- Workbook workbook = new XSSFWorkbook(); //dom4j的方式
使用jvm内存监视:

如果使用xssfworkboot,会出现内存溢出异常

使用SXSSFWorkbook不会出现内存溢出,
java
Workbook workbook = new SXSSFWorkbook(); //sax方式就是逐行解析
- 事件模式:它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。
2、Excel百万数据的导入
2.1、需求分析
使用POI基于事件模式解析案例提供的Excel文件
2.2、思路分析
用户模式:加载并读取Excel时,是通过一次性的将所有数据加载到内存中再去解析每个单元格内容。当Excel数据量较大时,由于不同的运行环境可能会造成内存不足甚至OOM异常。
例如读取我们刚刚导出的百万数据:
java
//测试百万数据的导入
public class POIDemo5 {
public static void main(String[] args) throws Exception {
XSSFWorkbook workbook = new XSSFWorkbook("C:\\Users\\syl\\Desktop\\百万用户数据的导出.xlsx");
XSSFSheet sheetAt = workbook.getSheetAt(0);
String stringCellValue = sheetAt.getRow(0).getCell(0).getStringCellValue();
System.out.println(stringCellValue);
}
}
会直接报内存溢出的错误:

事件模式:它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。
2.3、代码实现
2.3.1、步骤分析
-
设置POI的事件模式
根据Excel获取文件流
根据文件流创建OPCPackage 用来组合读取到的xml 组合出来的数据占用的空间更小
创建XSSFReader对象
-
Sax解析
自定义Sheet处理器
创建Sax的XmlReader对象
设置Sheet的事件处理器
逐行读取
2.3.2、自定义处理器
java
package com.tigerhhzz.handler;
import com.tigerhhzz.pojo.User;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.usermodel.XSSFComment;
public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
// 编号 用户名 手机号 入职日期 现住址
private User user;
@Override
public void startRow(int rowIndex) { //每一行的开始 rowIndex代表的是每一个sheet的行索引
if(rowIndex==0){
user = null;
}else{
user = new User();
}
}
@Override //处理每一行的所有单元格
public void cell(String cellName, String cellValue, XSSFComment comment) {
if(user!=null){
String letter = cellName.substring(0, 1); //每个单元名称的首字母 A B C
switch (letter){
case "A":{
user.setId(Long.parseLong(cellValue));
break;
}
case "B":{
user.setUserName(cellValue);
break;
}
}
}
}
@Override
public void endRow(int rowIndex) { //每一行的结束
if(rowIndex!=0){
System.out.println(user);
}
}
}
2.3.3、自定义解析
通过自定义解析类进行解析:
java
package com.tigerhhzz.utils;
import com.tigerhhzz.handler.SheetHandler;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
/**
* 自定义Excel解析器
*/
public class ExcelParser {
public void parse (String path) throws Exception {
//1.根据Excel获取OPCPackage对象
OPCPackage pkg = OPCPackage.open(path, PackageAccess.READ);
try {
//2.创建XSSFReader对象
XSSFReader reader = new XSSFReader(pkg);
//3.获取SharedStringsTable对象
SharedStringsTable sst = reader.getSharedStringsTable();
//4.获取StylesTable对象
StylesTable styles = reader.getStylesTable();
XMLReader parser = XMLReaderFactory.createXMLReader();
// 处理公共属性:Sheet名,Sheet合并单元格
parser.setContentHandler(new XSSFSheetXMLHandler(styles,sst, new SheetHandler(), false));
XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
while (sheets.hasNext()) {
InputStream sheetstream = sheets.next();
InputSource sheetSource = new InputSource(sheetstream);
try {
parser.parse(sheetSource);
} finally {
sheetstream.close();
}
}
} finally {
pkg.close();
}
}
}
2.4、测试
- 用户模式下读取测试Excel文件直接内存溢出,测试Excel文件映射到内存中还是占用了不少内存;
- 事件模式下可以流畅的运行。
使用事件模型解析:
java
package com.tigerhhzz;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
//测试百万数据的导入
public class POIDemo5 {
public static void main(String[] args) throws Exception {
XSSFWorkbook workbook = new XSSFWorkbook("C:\\Users\\syl\\Desktop\\百万用户数据的导出.xlsx");
XSSFSheet sheetAt = workbook.getSheetAt(0);
String stringCellValue = sheetAt.getRow(0).getCell(0).getStringCellValue();
System.out.println(stringCellValue);
}
}
3、opencsv操作CSV文件
3.1 CSV文件简介
现在好多的网站中导出的文件会出现一种csv文件,我们接下来学习一下csv文件的导出方式。
CSV文件:Comma-Separated Values,中文叫逗号分隔值或者字符分割值,其文件以纯文本的形式存储表格数据。该文件是一个字符序列,可以由任意数目的记录组成,记录间以某种换行符分割。每条记录由字段组成,字段间的分隔符是其他字符或者字符串。所有的记录都有完全相同的字段序列,相当于一个结构化表的纯文本形式。
用文本文件、excel或者类似与文本文件的编辑器都可以打开CSV文件。
为了简化开发,我们可以使用opencsv类库来导出csv文件
需要的依赖:
xml
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>4.5</version>
</dependency>
2.2 opencsv常用API
-
写入到csv文件会用到CSVWriter对象,创建此对象常见API如下

-
使用CSVWriter对象写入数据常用的方法如下:

-
读取csv文件会用到CSVReader对象,创建此对象常见API如下

构造器涉及到的三个参数:
- reader:读取文件的流对象,常有的是BufferedReader,InputStreamReader。
- separator:用于定义前面提到的分割符,默认为逗号CSVWriter.DEFAULT_SEPARATOR用于分割各列。
- quotechar:用于定义各个列的引号,有时候csv文件中会用引号或者其它符号将一个列引起来,例如一行可能是:"1","2","3",如果想读出的字符不包含引号,就可以把参数设为:"CSVWriter.NO_QUOTE_CHARACTER "
read方法:

3.3 导出CSV文件
3.3.1 需求
我们还是以需求作为学习的驱动:把用户的列表数据导出到csv文件中
3.3.2 代码实现
UserController代码
java
@GetMapping(value = "/downLoadCSV",name = "导出用户数据到CSV文件中")
public void downLoadCSV(HttpServletResponse response){
userService.downLoadCSV(response);
}
Service层
java
void downLoadCSV(HttpServletResponse response);
实现类
java
@Override
public void downLoadCSV(HttpServletResponse response) {
try {
// 准备输出流
ServletOutputStream outputStream = response.getOutputStream();
// 文件名
String filename="百万数据.csv";
// 设置两个头 一个是文件的打开方式 一个是mime类型
response.setHeader( "Content-Disposition", "attachment;filename=" + new String(filename.getBytes(),"ISO8859-1"));
response.setContentType("text/csv");
OutputStreamWriter osw = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
osw.write(0xFEFF);
// 创建一个用来写入到csv文件中的writer
CSVWriter writer = new CSVWriter(osw);
// 先写头信息
writer.writeNext(new String[]{"编号","姓名","手机号","入职日期","现住址"});
// 如果文件数据量非常大的时候,我们可以循环查询写入
int page = 1;
int pageSize=200000;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
while (true) { //不停地查询
List<User> userList = this.findPage(page, pageSize);
if (CollectionUtils.isEmpty(userList)) { //如果查询不到就不再查询了
break;
}
// 把查询到的数据转成数组放入到csv文件中
for (User user : userList) {
writer.writeNext(new String[]{user.getId().toString(),user.getUserName(),user.getPhone(),simpleDateFormat.format(user.getHireDate()),user.getAddress()});
}
writer.flush();
page++;
}
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
3.4 读取CSV文件
读取刚才导出的CSV文件
java
package com.tigerhhzz;
import com.opencsv.CSVReader;
import com.tigerhhzz.pojo.User;
import java.io.FileReader;
import java.text.SimpleDateFormat;
//读取百万级数据的csv文件
public class CsvDemo {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws Exception {
CSVReader csvReader = new CSVReader(new FileReader("C:\\Users\\Administrator\\Downloads\\百万数据.csv"));
String[] titles = csvReader.readNext(); //读取到第一行 是小标题
// "编号","姓名","手机号","入职日期","现住址"
User user = null;
while (true){
user = new User();
String[] content = csvReader.readNext();
if(content==null){
break;
}
user.setId(Long.parseLong(content[0]));
user.setUserName(content[1]);
user.setPhone(content[2]);
user.setHireDate(simpleDateFormat.parse(content[3]));
user.setAddress(content[4]);
System.out.println(user);
}
}
}

"人的一生会经历很多痛苦,但回头想想,都是传奇"。