见死亡,见众生,见自在
上一章简单介绍了 SpringBoot上传和下载文件(二十七),如果没有看过,请观看上一章
一. CSV 文件
以下内容,来自百度百科
一.一 CSV文件简介
逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),
其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,
最常见的是逗号或制表符。通常,所有记录都有完全相同的字段序列。通常都是纯文本文件。
建议使用WORDPAD或是记事本来开启,再则先另存新档后用EXCEL开启,也是方法之一
一.二 规则
1 开头是不留空,以行为单位。
2 可含或不含列名,含列名则居文件第一行。
3 一行数据不跨行,无空行。
4 以半角逗号(即,)作分隔符,列为空也要表达其存在。
5列内容如存在半角引号(即"),替换成半角双引号("")转义,即用半角引号(即"")将该字段值包含起来。
6文件读写时引号,逗号操作规则互逆。
7内码格式不限,可为 ASCII、Unicode 或者其他。
8不支持数字
9不支持特殊字符
一.三 Csv文件举例
文件形式 形如:
id,name,sex,age,description
1,两个蝴蝶飞_1,男,21,我是第1个,我的名字是:两个蝴蝶飞_1
2,两个蝴蝶飞_2,女,22,我是第2个,我的名字是:两个蝴蝶飞_2
3,两个蝴蝶飞_3,男,23,我是第3个,我的名字是:两个蝴蝶飞_3
4,两个蝴蝶飞_4,女,24,我是第4个,我的名字是:两个蝴蝶飞_4
5,两个蝴蝶飞_5,男,25,我是第5个,我的名字是:两个蝴蝶飞_5
6,两个蝴蝶飞_6,女,26,我是第6个,我的名字是:两个蝴蝶飞_6
7,两个蝴蝶飞_7,男,27,我是第7个,我的名字是:两个蝴蝶飞_7
8,两个蝴蝶飞_8,女,28,我是第8个,我的名字是:两个蝴蝶飞_8
9,两个蝴蝶飞_9,男,29,我是第9个,我的名字是:两个蝴蝶飞_9
10,两个蝴蝶飞_10,女,30,我是第10个,我的名字是:两个蝴蝶飞_10
二. hutool 关于Csv处理的工具
官网地址是: CSV文件处理工具-CsvUtil
在 pom.xml 中添加依赖
<!--添加hutool-all, 处理csv文件解析-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.10</version>
</dependency>
在 cn.hutool.core.text.csv 包下
接下来,老蝴蝶简单讲解一下.
对应的官方文档地址是: https://apidoc.gitee.com/loolly/hutool/
二.一 读取
二.一 .一 CsvReadConfig
/** 是否首行做为标题行,默认false */
protected boolean containsHeader;
/** 是否跳过空白行,默认true */
protected boolean skipEmptyRows = true;
/** 每行字段个数不同时是否抛出异常,默认false */
protected boolean errorOnDifferentFieldCount;
/** 定义开始的行(包括),此处为原始文件行号 */
protected long beginLineNo;
/** 结束的行(包括),此处为原始文件行号 */
protected long endLineNo = Long.MAX_VALUE-1;
在读取文件时,可以进行相应的配置.
注意,是否将首行默认为标题行 containsHeader 为 false
二.一.二 CsvReader
二.二 写入
二.二.一 CsvWriteConfig
写入文件时的配置
/**
* 是否始终使用文本分隔符,文本包装符,默认false,按需添加
*/
protected boolean alwaysDelimitText;
/**
* 换行符
*/
protected char[] lineDelimiter = {CharUtil.CR, CharUtil.LF}; //对应的是 \r \n
二.二.二 CsvWriter
二.三 公共部分
二.三.一 总的封装数据 CsvData
public class CsvData implements Iterable<CsvRow>, Serializable {
private static final long serialVersionUID = 1L;
//标题行
private final List<String> header;
//行数据
private final List<CsvRow> rows;
}
二.三.二 每一行数据对象 CsvRow
public final class CsvRow implements List<String> {
/** 原始行号 */
private final long originalLineNumber;
//这一行的 标题和对应的索引位置 如 name 0 表示 name字段是在第0位,最开始的位置
final Map<String, Integer> headerMap;
// 这一行的 内容
final List<String> fields;
}
二.三.三 工具类 CsvUtil
基本就这些常用的信息,可以满足日常开发的相关需求.
写几个常见的例子,就明白怎么使用了.
三. Csv的操作使用
与上一章节的 Excel 操作例子相同
提取了一个工具类,用于获取数据
public class DataUtil {
/**
* 获取用户的数据
* @date 2021/11/8 13:51
* @author zk_yjl
* @param name
* @return java.util.List<top.yueshushu.learn.pojo.User>
*/
public static List<User> createDataList(String name) {
List<User> result=new ArrayList<>();
for(int i=1;i<=10;i++){
User user=new User();
user.setId(i);
user.setName(name+"_"+i);
user.setSex(i%2==0?"女":"男");
user.setAge(20+i);
user.setDescription("我是第"+i+"个,我的名字是:"+user.getName());
result.add(user);
}
return result;
}
}
不要忘记在 pom.xml 中添加依赖
<!--添加hutool-all, 处理csv文件解析-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.10</version>
</dependency>
三.一 Csv写入文件
放置在 WriteTest 测试类里面
三.一.一 writeBeans 方式写入数据
/**
* 简单的 csv文件写入
* @date 2021/11/8 13:51
* @author zk_yjl
* @param
* @return void
*/
@Test
public void simpleWriteTest() throws Exception{
//1. 大批量的业务处理,最后获取组装相应的数据信息。
List<User> userList= DataUtil.createDataList("两个蝴蝶飞");
//2. 存储的文件地址
String filePath="D:\\csv\\simple.csv";
//获取 CsvWriter
CsvWriter csvWriter = CsvUtil.getWriter(filePath, Charset.forName("UTF-8"));
// 写入注释
csvWriter.writeComment("一个简单的csv文件");
//写入新的一行
csvWriter.writeLine();
//写入内容
csvWriter.writeBeans(userList);
//关闭内容
csvWriter.close();
log.info("写入文件成功");
}
运行后,查看数据
标题行 是对应的 bean的属性值.
三.一.二 注解在实体上,自定义标题头输出
可以在实体类上,通过 @Alias 注解来自定义标题头
@Data
public class User {
@Alias("编号")
private Integer id;
@Alias("姓名")
private String name;
// @Alias("性别")
private String sex;
// @Alias("年龄")
private Integer age;
@Alias("描述信息")
private String description;
}
进行测试
/**
* 写入标题头文件
* @date 2021/11/8 14:06
* @author zk_yjl
* @param
* @return void
*/
@Test
public void headerTest() throws Exception{
//1. 大批量的业务处理,最后获取组装相应的数据信息。
List<User> userList= DataUtil.createDataList("两个蝴蝶飞");
//2. 存储的文件路径
String filePath="D:\\csv\\header.csv";
//3. 获取CsvWriter
CsvWriter csvWriter = CsvUtil.getWriter(filePath, Charset.forName("UTF-8"));
//4. 写入注入
csvWriter.writeComment("带着自定义标题头的文件");
//写入内容
csvWriter.writeBeans(userList);
//关闭
csvWriter.close();
log.info(">>写入标题头文件成功");
}
编号,姓名,描述信息, 通过 @Alias 注解 进行了自定义标题.
一般开发中,导出 .csv 文件,都是采用 导出 Java Bean 的方式.
复杂的,就需要通过 CsvData 实体进行构建了.
三.一.三 CsvData 构造实现导出数据
/**
* 写入CsvData数据
* @date 2021/11/8 14:06
* @author zk_yjl
* @param
* @return void
*/
@Test
public void csvDataTest() throws Exception{
//1. 大批量的业务处理,最后获取组装相应的数据信息。
List<User> userList= DataUtil.createDataList("两个蝴蝶飞");
//2. 存储的文件路径
String filePath="D:\\csv\\data.csv";
//3. 获取CsvWriter
CsvWriter csvWriter = CsvUtil.getWriter(filePath, Charset.forName("UTF-8"));
//4. 写入注入
csvWriter.writeComment("写入CsvData文件");
//写入标题头
List<String> header=Arrays.asList("编号","姓名","性别","年龄","描述");
List<CsvRow> csvRowList=new ArrayList<>();
int i=0;
//写入行
for(User user:userList){
//构建 headerMap
Map<String, Integer> headerMap=new HashMap<>();
headerMap.put("编号",0);
headerMap.put("姓名",1);
headerMap.put("性别",2);
headerMap.put("年龄",3);
headerMap.put("描述",4);
//放置该行的值
List<String> fieldList=new ArrayList<>();
fieldList.add(user.getId()+"");
fieldList.add(user.getName()+"");
fieldList.add(user.getSex()+"");
fieldList.add(user.getAge()+"");
fieldList.add(user.getDescription()+"");
//构建 CsvRow 对象
CsvRow csvRow=new CsvRow(i,headerMap,fieldList);
csvRowList.add(csvRow);
i++;
}
CsvData csvData=new CsvData(header,csvRowList);
csvWriter.write(csvData);
//关闭
csvWriter.close();
log.info(">>写入CsvData 文件成功");
}
运行程序
三.二 Csv读取文件
放置在 ReadTest 测试类里面,读取的文件用刚才生成的那些文件.
三.二.一 简单读取数据
/**
* 简单文件的读取
* @date 2021/11/8 14:15
* @author zk_yjl
* @param
* @return void
*/
@Test
public void simpleTest(){
//1. 读取的文件路径
String filePath="D:\\csv\\simple.csv";
//构建 CsvReader 对象
CsvReader csvReader = CsvUtil.getReader();
//进行读取
CsvData csvData = csvReader.read(new File(filePath), Charset.forName("UTF-8"));
//进行处理
//获取相应的内容
int rowCount = csvData.getRowCount();
log.info(">>>读取了{}行",rowCount);
//获取行数据
List<CsvRow> rows = csvData.getRows();
for(CsvRow csvRow:rows){
//打印出该行的内容信息
List<String> rawList = csvRow.getRawList();
log.info(">>获取内容信息:"+rawList);
}
}
读取了 11行, 将 id,name,sex 这些应该是标题行的信息,变成了正常的内容形式.
可以通过 CsvReadConfig 进行相关的配置.
三.二.二 CsvReadConfig 读取配置简单使用
/**
* 读取信息,读取成 CsvData 形式.
* @date 2021/11/8 14:39
* @author zk_yjl
* @param
* @return void
*/
@Test
public void config1Test(){
//1. 读取的文件路径
String filePath="D:\\csv\\simple.csv";
//2. 进行配置
CsvReadConfig csvReadConfig=new CsvReadConfig();
csvReadConfig.setSkipEmptyRows(true);
//包括标题行
csvReadConfig.setContainsHeader(true);
//进行了读取,设置
//构建 CsvReader 对象
CsvReader csvReader = CsvUtil.getReader(csvReadConfig);
//进行读取
CsvData csvData = csvReader.read(new File(filePath), Charset.forName("UTF-8"));
//进行处理
List<String> header = csvData.getHeader();
log.info(">>>读取的文件标题头为:"+header.toString());
//获取相应的内容
int rowCount = csvData.getRowCount();
log.info(">>>读取了{}行",rowCount);
//获取行数据
List<CsvRow> rows = csvData.getRows();
for(CsvRow csvRow:rows){
List<String> rawList = csvRow.getRawList();
log.info(">>获取内容信息:"+rawList);
}
}
读取时,会默认把一行变成标题头,
剩余的是内容, 读取是正常的.
三.二.三 CsvReadConfig 读取配置较复杂使用
可以进行动态的配置,从哪一行开始读取,读取到第几行.
/**
* 配置读取的信息
* @date 2021/11/8 14:39
* @author zk_yjl
* @param
* @return void
*/
@Test
public void config2Test(){
//1. 读取的文件路径
String filePath="D:\\csv\\simple.csv";
//2. 进行配置
CsvReadConfig csvReadConfig=new CsvReadConfig();
csvReadConfig.setSkipEmptyRows(true);
csvReadConfig.setContainsHeader(true);
//设置最开始读取的行号, 注意,这一行,会被当成标题行的.
csvReadConfig.setBeginLineNo(0);
//读取的行号
csvReadConfig.setEndLineNo(6);
//进行了读取,设置
//构建 CsvReader 对象
CsvReader csvReader = CsvUtil.getReader(csvReadConfig);
//进行读取
CsvData csvData = csvReader.read(new File(filePath), Charset.forName("UTF-8"));
//进行处理
List<String> header = csvData.getHeader();
log.info(">>>读取的文件标题头为:"+header.toString());
//获取相应的内容
int rowCount = csvData.getRowCount();
log.info(">>>读取了{}行",rowCount);
//获取行数据
List<CsvRow> rows = csvData.getRows();
for(CsvRow csvRow:rows){
/* List<String> rawList = csvRow.getRawList();
log.info(">>获取内容信息:"+rawList);*/
log.info(">>>>");
Map<String, String> fieldMap = csvRow.getFieldMap();
for(Map.Entry<String,String> fieldEntry:fieldMap.entrySet()){
log.info(">>>>>>>>:"+fieldEntry.getKey()+":"+fieldEntry.getValue());
}
}
}
如果修改
csvReadConfig.setBeginLineNo(4); // 由0 变成4
再次运行
三.二.四 读取数据,封装成 Java Bean
/**
* 读取信息,放置到bean 里面
* @date 2021/11/8 14:39
* @author zk_yjl
* @param
* @return void
*/
@Test
public void headerTest() throws Exception{
//1. 读取的文件路径
String filePath="D:\\csv\\header.csv";
//进行了读取,设置
//2. 进行配置
CsvReadConfig csvReadConfig=new CsvReadConfig();
csvReadConfig.setSkipEmptyRows(true);
csvReadConfig.setContainsHeader(true);
//构建 CsvReader 对象
CsvReader csvReader = CsvUtil.getReader(csvReadConfig);
//直接读取,封装成 Bean
List<User> userList = csvReader.read(new FileReader(new File(filePath)), User.class);
for(User user:userList){
log.info(">>"+user);
}
}
可以在 实体类上 通过 @Alias 注解 将文件中的标题与类中的属性进行关联起来.
这就是一些关于 Csv的基本操作.
四. SpringBoot 实现 Csv文件的导入和导出
按照上一章节的模式,进行修改.
四.一 前端页面
<!doctype html>
<!--注意:引入thymeleaf的名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type"content="text/html;charset=UTF-8">
<title>文件上传</title>
<link rel="StyleSheet" href="webjars/bootstrap/3.4.1/css/bootstrap.css" type="text/css">
</head>
<body class="container">
<p class="h1">导入文件</p>
<form action="uploadCsv" method="post" enctype="multipart/form-data">
<div class="form-group">
<div class="custom-file">
<input type="file" class="custom-file-input" id="file" name="file">
<label class="file-label" for="file">选择文件</label>
</div>
</div>
<button type="submit" class="btn btn-primary">上传</button>
</form>
<p class="h1">文件导出</p>
<a href="downloadCsv">导出Csv文件</a>
<script type="text/javascript" src="webjars/jquery/3.5.1/jquery.js"></script>
<script type="text/javascript" src="webjars/bootstrap/3.4.1/js/bootstrap.js"></script>
</body>
</html>
四.二 Csv文件导入
四.二.一 导入逻辑处理
/**
* 上传Excel文件,获取文件里面的内容.
* @date 2021/11/4 21:12
* @author zk_yjl
* @return
*/
@PostMapping("/uploadCsv")
@ResponseBody
public String uploadCsv(@RequestParam MultipartFile file) throws IOException {
String realPath =uploadFilePath;
File newFile = new File(realPath);
// 如果文件夹不存在、则新建
if (!newFile.exists()){
newFile.mkdirs();
}
// 上传
File uploadFile=new File(newFile, file.getOriginalFilename());
file.transferTo(uploadFile);
//读取文件信息,主要是这一行
List<User> userList = CsvUtil.getReader().read(new FileReader(uploadFile), User.class);
userList.forEach(n->{
log.info("输出内容:>>>"+n);
//可以进行具体的业务操作
});
String uploadPath=realPath+"/"+file.getOriginalFilename();
return "上传文件成功,地址为:"+uploadPath;
}
四.二.二 导入上传测试
将 header.csv 文件进行上传
后端控制台 将 文件中的内容打印出来
四.三 Csv文件导出
四.三.一 导出文件逻辑处理
/**
* 下载Excel文件
* @date 2021/11/4 21:12
* @author zk_yjl
* @return
*/
@GetMapping("/downloadCsv")
@ResponseBody
public void downloadCsv(HttpServletResponse response) throws IOException {
//1. 通过传入的参数,和相关的业务代码逻辑处理,获取相应的数据.
//2. 将数据进行转换,转换成 List<User> 的形式.
List<User> dataList= DataUtil.createDataList("两个蝴蝶飞");
//将数据进行下载.
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
try {
//响应类型
response.setContentType("multipart/form-data");
response.setCharacterEncoding("utf-8");
//进行下载
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("员工表", "UTF-8").replaceAll("\\+", "%20");
//响应的是 .csv 文件的后缀
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".csv");
// 这里需要设置不关闭流
String filePath=uploadFilePath+File.separator+fileName+".csv";
File file=new File(filePath);
if(!file.exists()){
file.createNewFile();
}
//将数据,写入到 文件里面。 主要是这一行代码逻辑
CsvUtil.getWriter(file, Charset.forName("UTF-8")).writeBeans(dataList).close();
downloadFile(response,file);
//将该文件删除
file.delete();
} catch (Exception e) {
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = new HashMap<String, String>();
map.put("status", "failure");
map.put("message", "下载文件失败" + e.getMessage());
JSONObject jsonObject=new JSONObject();;
response.getWriter().println(jsonObject.toString());
}
}
/**
* @return boolean
* @Description 下载文件
* @Param response,file
**/
public boolean downloadFile(HttpServletResponse response, File file) {
FileInputStream fileInputStream = null;
BufferedInputStream bufferedInputStream = null;
OutputStream os = null;
try {
fileInputStream = new FileInputStream(file);
bufferedInputStream = new BufferedInputStream(fileInputStream);
os = response.getOutputStream();
//MS产本头部需要插入BOM
//如果不写入这几个字节,会导致用Excel打开时,中文显示乱码
os.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});
byte[] buffer = new byte[1024];
int i = bufferedInputStream.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bufferedInputStream.read(buffer);
}
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭流
if (os != null) {
try {
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedInputStream != null) {
try {
bufferedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
file.delete();
}
return false;
}
四.三.二 导出测试
点击导出文件,可以进行相应的下载.
本章节的代码放置在 github 上:
https://github.com/yuejianli/springboot/tree/develop/SpringBoot_Csv
谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!