导出csv文件【多文件导出zip包】

问题

之前在项目中导出数据都是用的excel,但是随着生产数据量的增大,excel已经无法满足实际业务的需要。主要问题是如果一次性导出的数据过于庞大,excel格式导出速度慢,且存在内存被撑爆的可能。综合考虑采用csv格式导出,动态设定每个csv文件的数据条数,多个csv文件打成zip包。

/** 采用apache commons cvs
*导出csv文件,设定每个文件的最大数量,多个csv文件打成zip包
* 类说明 
*/
public class CsvUtils {
	
	private static Logger logger = LoggerFactory.getLogger(CsvUtils.class);
	private static final int BUFFER_SIZE = 8192;//缓冲区大小
	
	/**
	 * 将内容写入csv文件
	 * @param downloadTempPath 参考:request.getServletContext().getRealPath("download")
	 * @param relativePath 举例:${sessionId}/${productId}
	 * @param fileName 不带后缀名,举例:多个文件返回文件的后缀是zip,单个是csv
	 * @param csvCharset 读写csv文件的编码字符集
	 * @param header 表头
	 * @param records 内容
	 * @param csvMaxRowNum 写csv文件时单文件的最大行数(超过的分多个文件并压缩成zip包)
	 * @return 服务器磁盘上的文件
	 * @throws IOException 写入失败
	 */
	protected File writeToCsv(String downloadTempPath, String relativePath, String fileName, String csvCharset, List<String> header, List<List<String>> records, int csvMaxRowNum) throws IOException {
		if(StringUtils.isBlank(fileName)) {
			throw new IOException("文件名为空,生成csv文件失败");
		}
		if(header == null || header.isEmpty()) {
			throw new IOException("表头为空,生成csv文件[" + fileName + "]失败");
		}
		
		String tempPath = downloadTempPath;
		if(StringUtils.isBlank(tempPath)) {
			tempPath = "/salemanagement/temp/";
		}
		if(!StringUtils.endsWith(tempPath, "/")) {
			tempPath += "/";
		}
		if(relativePath == null) {
			relativePath = "";
		}
		relativePath = StringUtils.removeStart(relativePath, "/");//如果以"/"开头,则去掉"/"字符
		String absolutePath = tempPath + relativePath;
		
		int recordsNum = records == null ? 0 : records.size();
		if(recordsNum <= csvMaxRowNum) {//如果数据没有超过csvMaxRowNum,则返回csv文件
			List<List<String>> csvData = new ArrayList<List<String>>();
			csvData.add(header);
			if(recordsNum > 0) {//如果有数据则添加,否则将返回一个只有表头的csv文件
				csvData.addAll(records);
			}
			return writeRecords(csvData, absolutePath, fileName + ".csv", false, csvCharset);
		} else {
			List<File> files = new ArrayList<File>();
			int fullFileNum = recordsNum / csvMaxRowNum;//能够写满(数据正好等于csvMaxRowNum)的文件的个数
			for (int i = 0; i < fullFileNum; i++) {
				List<List<String>> csvData = new ArrayList<List<String>>();
				csvData.add(header);
				csvData.addAll(records.subList(csvMaxRowNum * i, csvMaxRowNum * (i + 1)));
				files.add(writeRecords(csvData, absolutePath, fileName + "_" + (i + 1) + ".csv", false, csvCharset));//文件序号从1开始
			}
			int leftNum = recordsNum % csvMaxRowNum;//不能写满一个文件时,剩余记录数。如果大于0,则需要单独写一个文件
			if(leftNum > 0) {
				List<List<String>> csvData = new ArrayList<List<String>>();
				csvData.add(header);
				csvData.addAll(records.subList(csvMaxRowNum * fullFileNum, recordsNum));
				files.add(writeRecords(csvData, absolutePath, fileName + "_" + (fullFileNum + 1) + ".csv", false, csvCharset));//文件序号从1开始
			}
			return ZipFileUtil.zip(fileName + ".zip", absolutePath, files.toArray(new File[files.size()]));
		}
	}
	
	/**
	 * 将字符串List存到文件中,每个List<String>一行
	 * @param records 字符串List
	 * @param filePath 目标全路径
	 * @param fileName 目标文件名
	 * @param append true追加,false覆盖
	 * @param charset 编码字符集
	 * @return 目标文件
	 * @throws IOException 写文件出错(包括字符集错误)
	 */
	public static File writeRecords(List<List<String>> records, String filePath, String fileName, boolean append, String charset) throws IOException {
		File parentFile = new File(filePath);
		if(!parentFile.exists()) {
			parentFile.mkdirs();
		}
		OutputStreamWriter osw = null;
		CSVPrinter csvPrinter = null;
		try {
			File file = new File(parentFile, fileName);
			if(StringUtils.isBlank(charset)) {
				osw = new OutputStreamWriter(new FileOutputStream(file, append));
			} else {
				osw = new OutputStreamWriter(new FileOutputStream(file, append), charset);
			}
			csvPrinter = new CSVPrinter(osw, CSVFormat.DEFAULT);
			for (int i = 0; i < records.size(); i++) {
	            csvPrinter.printRecord(records.get(i));
	        }
			return file;
		} finally {
			if(csvPrinter!=null) {
				try {
					csvPrinter.close();
				} catch (Exception e) {
					logger.warn("关闭CSVPrinter失败", e);
				}
			}
			if(osw!=null) {
				try {
					osw.close();
				} catch (Exception e) {
					logger.warn("关闭csv文件[" + fileName + "]失败", e);
				}
			}
		}
	}
	
	/**
	 * 压缩文件(保留文件的修改时间,不保留文件夹的)
	 * @param targetFileName 不包含路径,以zip结尾
	 * @param absRootPath 源文件必须在这个目录或子目录下,目标文件生成在这个目录下
	 */
	public static File zip(String targetFileName, String absRootPath, File... sourceFiles) throws IOException {
		File targetFile = new File(absRootPath, targetFileName);
		ZipOutputStream zos = null;
		try {
			zos = new ZipOutputStream(new FileOutputStream(targetFile));
			compress(zos, absRootPath, sourceFiles);
		} finally {
			if(zos != null) {
				try {
					zos.close();
				} catch (IOException e) {
					logger.warn("关闭目标文件[" + targetFile + "]的输入流失败", e);
				}
			}
		}
		return targetFile;
	}
	private static void compress(ZipOutputStream zipOutputStream, String absRootPath, File... sourceFiles) throws IOException {
		absRootPath = StringUtils.replace(absRootPath, "\\", "/");
		if(!absRootPath.endsWith("/")) {
			absRootPath += "/";
		}
		for (File sourceFile : sourceFiles) {
			if (sourceFile.isFile()) {
				String absPath = sourceFile.getAbsolutePath();
				absPath = StringUtils.replace(absPath, "\\", "/");
				String zipEntryName = StringUtils.removeStart(absPath, absRootPath);
				if(absPath.equals(zipEntryName)) {
					throw new RuntimeException("压缩出错,absRootPath["+absRootPath+"]与absPath["+absPath+"]不匹配");
				}
				ZipEntry zipEntry = new ZipEntry(zipEntryName);
//				zipEntry.setSize(sourceFile.length());
				zipEntry.setTime(sourceFile.lastModified());
				zipOutputStream.putNextEntry(zipEntry);
				
				InputStream bis = null;
				try {
					bis = new BufferedInputStream(new FileInputStream(sourceFile));
					int readLength = -1;
					byte[] buffer = new byte[BUFFER_SIZE];
					while ((readLength = bis.read(buffer, 0, BUFFER_SIZE)) != -1) {
						zipOutputStream.write(buffer, 0, readLength);
					}
				} finally {
					if(bis != null) {
						try {
							bis.close();
						} catch (IOException e) {
							logger.warn("关闭文件[" + sourceFile + "]的输入流失败", e);
						}
					}
					try {
						zipOutputStream.closeEntry();
					} catch (Exception e) {
						logger.warn("关闭ZipEntry[" + zipEntryName + "]失败", e);
					}
				}
			} else {
				compress(zipOutputStream, absRootPath, sourceFile.listFiles());
			}
		}
	}
	
	/**
	 * 解压文件(保留文件的修改时间,不保留文件夹的)
	 * @param targetDir 目标目录的绝对路径
	 */
	public static void unzip(File zipFile, String targetDir) throws IOException {
		targetDir = StringUtils.replace(targetDir, "\\", "/");
		if(!targetDir.endsWith("/")) {
			targetDir += "/";
		}
		ZipFile zip = null;
		try {
			zip = new ZipFile(zipFile);
			Enumeration<? extends ZipEntry> entries = zip.entries();
			while(entries.hasMoreElements()) {
				ZipEntry entry = entries.nextElement();
				if(!entry.isDirectory()) {//加压文件会自动创建父目录,文件夹全部忽略
					File entryFile = new File(targetDir + entry.getName());
					if (entryFile.exists()) {
						entryFile.delete();
					}
					File parent = entryFile.getParentFile();
					if(!parent.exists()) {
						parent.mkdirs();
					}
					InputStream bis = null;
					OutputStream bos = null;
					try {
						bis = new BufferedInputStream(zip.getInputStream(entry));
						bos = new BufferedOutputStream(new FileOutputStream(entryFile));
						int readLength = -1;
						byte[] buffer = new byte[BUFFER_SIZE];
						while ((readLength = bis.read(buffer, 0, BUFFER_SIZE)) != -1) {
							bos.write(buffer, 0, readLength);
						}
						bos.flush();
					} finally {
						if(bis != null) {
							try {
								bis.close();
							} catch (IOException e) {
								logger.warn("关闭entry[" + entry.getName() + "]的输入流失败", e);
							}
						}
						if(bos != null) {
							try {
								bos.close();
							} catch (IOException e) {
								logger.warn("关闭文件[" + entryFile.getPath() + "]的输出流失败", e);
							}
						}
					}
					entryFile.setLastModified(entry.getTime());
				}
			}
		} finally {
			if(zip != null) {
				try {
					zip.close();
				} catch (IOException e) {
					logger.warn("关闭压缩文件[" + zipFile.getPath() + "]失败", e);
				}
			}
		}
	}
}
测试代码
/**
 * 导出CSV文件
 */
@RequestMapping("/downloadFile")
public ResponseEntity<byte[]> downloadFile(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
    List<String> header = new ArrayList<>();
    header.add("编号");
    header.add("姓名");
    header.add("手机号");

    List<List<String>> lists = new ArrayList<>();
    for (int i = 0;i < 100;i++) {
          List<String> dataList = new ArrayList<>();
          dataList.add(i + "");
          dataList.add("Jack" + i);
          dataList.add("18815764728");
          lists.add(dataList);
      }
 
    //导出CSV文件
    File file = writeToCsv(request.getServletContext().getRealPath("download"), session.getId(), "note","GBK", header, lists,20);

    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Disposition", "attachment;filename=" + file.getName());
    return new ResponseEntity<>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK);
}

小结:导出csv格式的文件效率比excel大大提高,但导出的csv文件如果用excel打开存在一定的问题:如导出的身份证,手机号和时间显示结果并不是自己想要的结果【手机号会转变成科学记数法,时间会用###代替】,需要手动去调整。目前网上有2种解决方案,一种是加上制表符“\t”,一种是“=数据”,(具体使用自行上网查阅)。但这两种解决方法都存在问题,即打开时没问题,但复制文本时会带上空格或“=”,两种效果不一样。
如果谁有完美的解决方法,希望留言告知,感谢!


版权声明:本文为gufenghua0222原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。