一、背景

开发中,我们经常需要导入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进行请求,视图如下:

java 拆分集合 java拆分文件_java 拆分集合

java 拆分集合 java拆分文件_i++_02

2、返回结果如下:

(1)日志输出

java 拆分集合 java拆分文件_java 拆分集合_03

(2)文件效果如下:

java 拆分集合 java拆分文件_web_04

四、总结

以上就是我的csv大文件拆分的一些思路,希望帮到大家,更多精彩关注java基础笔记,有帮助可以点个赞,