java 导出CSV文件踩坑记

CSV简介

CSV全称是:Comma Separated Values (逗号分隔值)或者 Character Separated Values(字符分隔值)。其文件以纯文本形式存储表格数据(数字和文本)。CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符。每一行记录位于一个单独的行上,用回车换行符CRLF(也就是\r\n)分割。

  • 对于excel来说默认使用 ,进行分割数据。
  • 每一行记录最后一个字段后不能跟逗号
  • 每一行一条记录
  • 列为空需要指定 ""
  • 用回车换行符CRLF(\r\n)分割每条记录
  • 纯文本,使用某个字符集,比如ASCII、Unicode、EBCDIC或GB2312;

上面的内容从百科上总结(抄袭)的。

我的踩坑经历

之前做了一个CSV导出的小功能,然后踩了很多坑。最后蓦然回首,发现真的好简单,自己给自己挖了很多坑,简单的功能被自己弄的异常复杂。

作为一个聪明的程序员,我肯定不会重复造轮子,就在网上随便找了一份代码。然后噩梦从此开始。

我比较懒,所以使用反射来减轻我的工作量
我的最初版本

public class ExportCSVUtil<T>{
	private Class<T> cls;//数据类型
	private List<T> list; //存放需要填充的数据
	private List<Method> methods;//存储get方法
	
	/**
	 * 构造方法
	 * @param list 填充数据
	 * @param cls 数据类型
	 * @throws Exception
	 */
	public ExportCSVUtil(List<T> list,Class<T> cls) throws Exception{
		this.list=list;
		this.cls=cls;
		parse();
	}
	
		
	public  void doExport(OutputStream out) throws IOException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
			OutputStreamWriter outputStreamWriter =new OutputStreamWriter(out,"utf8");
			BufferedWriter writer = new BufferedWriter(outputStreamWriter);//使用缓冲流,比较适合写文本操作,效率可能也会高些
			writeBody(writer,list); 
			outputStreamWriter.flush();
			outputStreamWriter.close();
	}
	
	/**
	*向输出流中写数据
	* @param list 填充数据
	* @param writer 缓冲流
	*/
	public void writeBody(BufferedWriter writer,List<T> list){
		
		int i=0;
		int length = methods.length;
		for(T obj: list){
			for(Method method: methods){
				Object result= method.invoke(obj,new Object[]);// 反射获取get方法的值
				String str=null;
				if(result==null) //处理空值
					str="";
				else
					str=result.toString();
				if(i++ <=length-1)	
					writer.write("\""+str+"\","); //文本用双引号包裹
				else
					writer.write("\""+str+"\""); //最后的元素需要使用换行符而不是“,” 需要特别注意
			}
			writer.newLine();
		}
	
	}

	/**
	*根据数据类型,解析元素
	*/
	private void parse() throws SecurityException {
		Field[] fields = cls.getDeclaredFields();
		for(Field field : fields) {
			String fieldName = field.getName();
			try{
				Method method =cls.getDeclaredMethod("get"+StringUtils.capitalize(fieldName));//根据名称方法
			}catch(NoSuchMethodException e){
			}
			if(method==null)
				throw new IllegalStateException(cls+"  未获取属性为\t"+fieldName+"的get方法");
			methods.add(method);//获取所有定义在实体类的get方法
		}
	}
}

1. 中文乱码

问题就是那么不期而遇,首先 中文乱码啦。在网上又是一通找,发现只需要开始时向流中写入BOM(0xEF0xBB0xBF),该方式告诉excel打开该文件时以utf8编码打开

public  void doExport(OutputStream out) throws IOException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
			byte[] bytes =new byte[] {(byte)0xEF,(byte)0xBB,(byte)0xBF}; //utf8编码
			out.write(bytes);
			
			//其他逻辑
	}
2. 数据错行

以为大功告成了,结果问题悄悄的来却不肯走。数据错行了,效果图大致如下在这里插入图片描述
排查了一下原因发现是包裹的数据有换行。然后就使用正则去掉了\n

int i=0;
int length = methods.length;
for(Method method: methods){
				Object result= method.invoke(obj,new Object[]);
				String str=null;
				if(result==null)
					str="";
				else
					str=result.toString().replaceAll("[/r/n]"," ");
				if(i++ <=length-1)	
					writer.write("\""+str+"\","); //文本用双引号包裹
				else
					writer.write("\""+str+"\""); //最后的元素需要使用换行符而不是“,” 需要特别注意
			}
3. 数据错列

忙了很多时间,总以为现在改好了,没想到生活不如意。数据又错列了,数据从英文逗号处被分成两份。
在这里插入图片描述是谁告诉我,逗号使用"“包裹就没事的。可能我的搜索方式有问题,在网上也没找到合适的答案。然后去查了一下百科,发现出错的原因,竟然是它!它!它!。是它就是它,我烦恼的根源双引号(”)。因为有一个数据库字段存储的是爬取的html元素信息,里面包含了很多双引号,所以就出问题了。

原来如果文本中有双引号,需要做处理(转义?)就像这样 " 变成 “” ,一个双引号变成两个双引号。

例如下面的这段内容

<p class="aa" > asdsd"</p>

我们需要转化为下面的格式

<p class=""aa""> asdsd""</p>

使用String内置方法replaceAll,使用简单的正则替换就可以了

content.replaceAll("\"","\"\"");

这样分割字符逗号和换行都不会捣乱了,然后就没有然后了,这种方式治疗了我多年的腰间盘,使用之后极度舒适。

int i=0;
int length = methods.length;
for(Method method: methods){
				Object result= method.invoke(obj,new Object[]);
				String str=null;
				if(result==null)
					str=" ";
				else
					str=result.toString().replaceAll("\"","\"\"");//它会保证逗号和分隔符保证不会在捣乱了
				if(i++ <=length-1)	
					writer.write("\""+str+"\","); //文本用双引号包裹
				else
					writer.write("\""+str+"\""); //最后的元素需要使用换行符而不是“,” 需要特别注意
}
4. 日期格式显示不正确(#####)

大概这是最后一个问题了吧。希望是

解决方式比较简单,前面添加制表符,如果还是不显示那就加两个。原因可能跟excel差不多,默认只能显示指定长度的数字,超过指定长度或者列不够宽所以就显示####

writer.write("\t"+str);
5. 我疯了,又特曼出问题了

原以为万事大吉,结果在测试的时候又抛出一个致命的问题。又错列了,折腾了半天,在同事的帮助下才定位到了问题。原来excel的单元格可以存放的字符是有数量限制的,而出问题的那个数据,字符数量特别多。那也没什么好的建议,要么截取数据,要么将数据分段存放在多个字段中。

另外Microsoft 和wps的处理方式不一样,Microsoft 中 excel会显示超出的文字(这就是显示出错的那部分数据),而wps会截取数据显示(只是显示差异,原始数据没有修改)。

经过测试发现Microsoft excel(2016)中每个单元格可以存放32767个字符(汉字也可以存放32767个)。网上都说只可以显示1024个字符,但是实际测试的时候excel 2016 可以显示超过了5000个字符

在这里插入图片描述
当字符数大于32767时不可输入

踩坑完的代码

最终版本

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

import org.apache.commons.lang.StringUtils;

public class ExportCSVUtil<T>{
	private Class<T> cls;//数据类型
	private List<T> list;//填充数据
	private List<Method> methods;//存储get方法
	
	/**
	 * 该方式导出csv文件会显示标题
	 * @param list 填充数据
	 * @param cls 数据类型
	 * @throws Exception
	 */
	public ExportCSVUtil(List<T> list,Class<T> cls) throws Exception{
		this.list=list;
		this.cls=cls;
		parse();
	}

	/**
	 * 调用此方法,将数据写出到指定的文件流中
	 * @param OutputStream 输出流
	 * @throws Exception
	 */	
	public  void doExport(OutputStream out) throws Exception{
			byte[] bytes =new byte[] {(byte)0xEF,(byte)0xBB,(byte)0xBF}; //utf8编码
			out.write(bytes);//写入BOM 
			OutputStreamWriter outputStreamWriter =new OutputStreamWriter(out,"utf8");
			BufferedWriter writer = new BufferedWriter(outputStreamWriter);
			writeBody(writer,list);//写内容
			outputStreamWriter.flush();
			outputStreamWriter.close();
	}
	
	/**
	*写数据
	 * @throws InvocationTargetException 
	 * @throws IllegalArgumentException 
	 * @throws IllegalAccessException 
	 * @throws IOException 
	*/
	public void writeBody(BufferedWriter writer,List<T> list) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException{
		//数据遍历
		for(T obj: list){
			int i=0;
			int length = methods.size();
			for(Method method: methods){
					Object result= method.invoke(obj,new Object[] {});
					String str=null;
					if(result==null)//处理空值
						str="";
					else
						str=result.toString().replaceAll("\"","\"\""); //处理文本中的"
						
					if(i++ <=length-1)	
						writer.write("\""+str+"\","); //文本用双引号包裹
					else
						writer.write("\""+str+"\""); //最后的元素需要使用换行符而不是“,” 需要特别注意
				}
				writer.newLine();//换行
		}
	}

	/**
	* 解析实体类,获取get或者 is方法
	*/
	private void parse() throws NoSuchMethodException{
		Field[] fields = cls.getDeclaredFields();//java文档上其实说过,该方法获取到的列表并不能保证其顺序。严格的来说应该对变量上添加排序的注解,然后对获取到的元素进行重新排序。实际使用的时候,获取到的数组就是声明属性的顺序,可能在某些情况下会有问题。
		//根据属性获取方法,当然也可以直接获取方法
		for(Field field : fields) {
			String fieldName = field.getName();
			Method method =null;
			try {
				//普通get方法   getXnnn
				cls.getDeclaredMethod("get"+StringUtils.capitalize(fieldName));
			}catch (Exception e) {
			}
			if(method==null) {
				try {
					//bool属性对应的方法  isXnn
					cls.getDeclaredMethod("is"+StringUtils.capitalize(fieldName));
				}catch (Exception e) {
				}
			}
			//方法不存在时抛出异常
			if(method==null)
				throw new NoSuchMethodException(cls+"的属性"+fieldName+"对象没有对应的getter 方法");
			
			methods.add(method);//获取getter方法
		}
		
	}
}

注意:

Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this Class object. This includes public, protected, default (package) access, and private fields, but excludes inherited fields.

getDeclaredFields 方法会返回类或者接口上定义的所有的属性(无论修饰符是public, protected, default (package) , private,包括静态的属性),但是不包含继承的属性。

所以parse方法需要根据场景修改,要么规范好实体类不包含不必要的属性,要么使用自定义注解去跳过某个属性,要么直接获取方法(非必要属性不生成get方法)

try {
    //输出路径
    FileOutputStream outputStream = new FileOutputStream("D:/out/1.csv");
	// 从数据库获取数据集
	List<Person> list= service.getPersonList();
	ExportCSVUtil<Person> utils=new ExportCSVUtil<>(list,Person.class);
	//将数据集写入到 输出流中
	utils.doExport(outputStream);
} catch (FileNotFoundException e) {
   e.printStackTrace();
}

最后总结一下:

  • 数据从第一行解析
  • 用逗号(或者其他字符)分割每个元素,最后一个元素不需要使用逗号分隔
  • 一行代表一条数据
  • 用回车换行符CRLF(\r\n)分割每条记录。测试发现使用\n每行最后也是显示CRLF
  • 英文逗号必须存放在双引号字符。 “12,aaa” √
  • 双引号字符(")必须是封闭的双引号字符(""),每一个嵌入式双引号字符必须用一对双引号字符。“q"w” --> “q""w”
  • 换行符必须封闭在双引号字符。 “q/n12”

综上: 文本内容应当存放在""中,并对文本中的"做处理。

参考:百度百科-CSV

后记 我理解的excel解析csv文件的规则

如果想自己解析csv文件,我有一些拙见。虽然不清楚excel具体的解析方式,但掉坑那么久也是有些心得。

如果文本被双引号包裹,那么:

  • 首先如果文本外存在双引号,那么它总是试图与离自己最近的"匹配,当然这个双引号不包括转义后的双引号("")
  • 如果遇到两个相邻的双引号那么它的优先级比较高,会优先将"“会优先转化为”。对应1
  • 与文本头部双引号匹配成功的双引号不显示,对于没有匹配成功的双引号,会正常显示。对应5,6
  • 当" 匹配成功后,如果其后还有数据,则按正常情况解析。遇到逗号就拆分数据 。对应2
  • 如果文本头部的双引号在换行前没有匹配到双引号,它会一直找下去直到找到它。在此过程中它不会理会逗号或者回车小姐姐抛来的眉眼。所以文本中的逗号(或者其他分隔符)要放在 双引号中。对应 3

上面说了那么多,先不说有很多错别字,可能语句读起来都不通顺。所以祭出杀器
在这里插入图片描述

现在图已经有了,真相还会远吗。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值