一、背景
开发中,我们经常需要导入csv文件到数据库中,但是如果csv文件太大了,可能会报错,这时候可以对csv文件进行拆分,分批导入。本节就以spring boot项目为例实现csv大文件拆分并输出拆分后的zip包。
二、后端实现
1、controller层,我们传下面几个参数:
(1)file参数: 前端传的大csv文件
(2)size参数:要拆分的小文件最大行数
(3)request参数:请求体
(4)response参数 :响应体
2、controller主要代码如下:
(1)比较容易理解,前半部分目的是获取前端传的文件的基本信息
(2)SplitUtils.getCsvZipPath(inputStream, fileName, splitSize);方法对csv文件进行拆分并返回拆分后的文件夹路径。
(3)exportZipUtils.zipExport(zipPath, request, response);方法将拆分后的csv文件夹打包输出到前端。
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public String uploadAndGetSplitZip(@RequestParam("file") MultipartFile file, @RequestParam("size") String size,
HttpServletRequest request, HttpServletResponse response) {
try {
InputStream inputStream = file.getInputStream();
String originalFilename = file.getOriginalFilename();
String[] split = originalFilename.split("\\.");
String type = split[split.length - 1];
int index = originalFilename.lastIndexOf(".");
String fileName = originalFilename.substring(0, index);
int splitSize = Integer.valueOf(size);
logger.info("文件信息:文件名:{} 文件类型: {}" +
" 文件大小:{}MB 要求拆分文件最大行数: {}", fileName, type,
file.getSize() / 1024 / 1024, splitSize);
String zipPath = SplitUtils.getCsvZipPath(inputStream, fileName, splitSize);
exportZipUtils.zipExport(zipPath, request, response);
forceDeleteFilesUtils.deleteAllFilesOfDir(new File(zipPath));
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
3、拆分csv文件方法主要代码如下:
(1)参数inputStream:为大csv文件流。
(2)参数fileName :为前端所传文件名。
(3)参数 splitSize: 为拆分后小文件的最大行数。
(4)这个方法主要思路将大文件流放到BufferedReader里面,然后获取总行数,根据参数splitSize计算需要拆分成几个小文件,需要几个文件,我们就创建几个,放到list集合里,一行一行遍历源文件,第一行的内容所以文件都写入,除第一行外的内容,随机写入创建的小文件里面。最后把所有的小文件关流。
@Component
public class SplitUtils {
private static Logger logger = LoggerFactory.getLogger(SplitUtils.class);
private static String defaultDir = System.getProperty("java.io.tmpdir") + File.separator;
/**
* 拆分csv文件并返回文件夹路径
*
* @param inputStream
* @param filename
* @param splitSize
* @return
*/
public static String getCsvZipPath(InputStream inputStream, String filename, int splitSize) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
InputStreamReader reader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(reader);
Stream<String> lines = bufferedReader.lines();
List<String> contents = lines.collect(Collectors.toList());
long fileCount = contents.size();
int splitNumber = (int) ((fileCount % splitSize == 0) ? (fileCount / splitSize) : (fileCount / splitSize + 1));
logger.info("csv文件总行数: {}行 拆分文件个数:{}个", fileCount, splitNumber);
//将创建的拆分文件写入流放入集合中
List<BufferedWriter> listWriters = new ArrayList<>();
//创建存放拆分文件的目录
File dir = new File(defaultDir + filename);
//文件夹存在,可能里面有内容,删除所有内容
if (dir.exists()) {
delAllFile(dir.getAbsolutePath());
}
dir.mkdirs();
for (int i = 0; i < splitNumber; i++) {
String splitFilePath = defaultDir + filename + File.separator + filename + i + ".csv";
File splitFileName = new File(splitFilePath);
splitFileName.createNewFile();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(splitFileName)));
listWriters.add(bufferedWriter);
}
for (int i = 0; i < fileCount; i++) {
if (i == 0) {
for (int count = 0; count < splitNumber; count++) {
listWriters.get(count).write(contents.get(i));
listWriters.get(count).newLine();
}
} else {
listWriters.get(i % splitNumber).write(contents.get(i));
listWriters.get(i % splitNumber).newLine();
}
}
//关流
listWriters.forEach(it -> {
try {
it.flush();
it.close();
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
logger.info("csv拆分文件失败 :" + e);
e.printStackTrace();
}
stopWatch.stop();
logger.info("csv文件拆分共花费: " + stopWatch.getTotalTimeMillis() + " ms");
return defaultDir + filename + File.separator;
}
(5)拆分文件时,存放临时文件的地方可能已存在同名的文件,需要删除。意思就是我们拆分文件时,肯定需要把拆分的文件放到一个地方,可能这个地方不干净,有其他文件,所以我们放之前先删除一下这里的文件。方法如下:这个方法在上面拆分文件方法里用到了。在这里补充一下。
/***
* 删除文件夹
*
*/
public static void delFolder(String folderPath) {
try {
delAllFile(folderPath); // 删除完里面所有内容
String filePath = folderPath;
filePath = filePath.toString();
File myFilePath = new File(filePath);
myFilePath.delete(); // 删除空文件夹
} catch (Exception e) {
e.printStackTrace();
}
}
/***
* 删除指定文件夹下所有文件
*
* @param path 文件夹完整绝对路径
* @return
*/
public static boolean delAllFile(String path) {
boolean flag = false;
File file = new File(path);
if (!file.exists()) {
return flag;
}
if (!file.isDirectory()) {
return flag;
}
String[] tempList = file.list();
File temp = null;
for (int i = 0; i < tempList.length; i++) {
if (path.endsWith(File.separator)) {
temp = new File(path + tempList[i]);
} else {
temp = new File(path + File.separator + tempList[i]);
}
if (temp.isFile()) {
temp.delete();
}
if (temp.isDirectory()) {
delAllFile(path + "/" + tempList[i]);// 先删除文件夹里面的文件
delFolder(path + "/" + tempList[i]);// 再删除空文件夹
flag = true;
}
}
return flag;
}
4、拆分为小文件后,我们需要打包传到前端,exportZipUtils.zipExport(zipPath, request, response);这个方法就是干这个事的,代码如下,就是个打包组件,复制使用就可以了。
(1)filePath为存放拆分后的小文件路径
(2)request和response分别为请求体和响应体。
@Component
public class ExportZipUtils {
private static Logger logger = LoggerFactory.getLogger(ExportZipUtils.class);
public void zipExport(String filePath, HttpServletRequest request, HttpServletResponse response) {
//zip包的名称
StopWatch stopWatch = new StopWatch();
stopWatch.start();
String zipName = "package.zip";
//要打包的文件夹路径
String packagePath = filePath;
response.setContentType("octets/stream");
response.setCharacterEncoding("UTF-8");
String agent = request.getHeader("USER-AGENT");
try {
if (agent.contains("MSIE") || agent.contains("Trident")) {
zipName = URLEncoder.encode(zipName, "UTF-8");
} else {
zipName = new String(zipName.getBytes("UTF-8"), "ISO-8859-1");
}
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage(), e);
}
response.setHeader("Content-Disposition", "attachment;fileName=\"" + zipName + "\"");
ZipOutputStream zipos = null;
try {
zipos = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()));
zipos.setMethod(ZipOutputStream.DEFLATED);
zipos.setLevel(0);
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
DataOutputStream os = null;
InputStream is = null;
try {
String[] fileNameList = new File(packagePath).list();
for (int i = 0; i < fileNameList.length; i++) {
File file = new File(packagePath + File.separator + fileNameList[i]);
zipos.putNextEntry(new ZipEntry(fileNameList[i]));
os = new DataOutputStream(zipos);
is = new FileInputStream(file);
//输入流转换为输出流
IOUtils.copy(is, os);
is.close();
zipos.closeEntry();
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
} finally {
stopWatch.stop();
logger.info("输出zip包共耗时: " + stopWatch.getTotalTimeMillis() + " ms");
// 推荐使用try-with-resource
try {
if (is != null) {
is.close();
}
if (os != null) {
os.flush();
os.close();
}
if (zipos != null) {
zipos.flush();
zipos.close();
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
}
5、打包的文件传到前端后,如果想删除临时文件,可以使用这个方法,传进去要删除的文件路径,该路径下的所有文件就被删除了,工具代码如下:
/**
* 删除文件夹(强制删除)
*
* @param path
*/
public void deleteAllFilesOfDir(File path) {
if (null != path) {
if (!path.exists()){
return;
}
if (path.isFile()) {
boolean result = path.delete();
int tryCount = 0;
while (!result && tryCount++ < 10) {
System.gc(); // 回收资源
result = path.delete();
}
}
File[] files = path.listFiles();
if (null != files) {
for (int i = 0; i < files.length; i++) {
deleteAllFilesOfDir(files[i]);
}
}
path.delete();
}
}
/**
* 删除文件
*/
public boolean deleteFile(String pathname) {
boolean result = false;
File file = new File(pathname);
if (file.exists()) {
file.delete();
result = true;
System.out.println("文件已经被成功删除");
}
return result;
}
三、测试效果
1、我们通过Postman进行请求,视图如下:
2、返回结果如下:
(1)日志输出
(2)文件效果如下:
四、总结
以上就是我的csv大文件拆分的一些思路,希望帮到大家,更多精彩关注java基础笔记,有帮助可以点个赞,