一、背景
开发中,我们经常需要导入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)文件效果如下: