转自:Php导入导出csv文件时的生僻字处理和格式控制 - 班登 - 博客
应用场景
Web应用中导入导出报表是一个非常常见的需求,而普通用户使用最多的表格文件都是用Excel保存的。
一般来说,对于数据量较小的导入导出,使用xls/xlsx文件和用户交互是最佳选择,即有相应函数库作为支撑,用户的学习成本也是最低的。但是,对于数据量较大的业务来说就不适用了。因为,大批量数据在导入时,函数库会将表中所有数据载入到内存中,由于xls/xlsx的文件本身在原始数据基础上就包含了一些冗余信息(如格式),加上数据量本身的扩充,导致内存消耗会非常大,在php环境中使用phpExcel这个最热门的库,读取一个5M左右的xls数据就有可能导致512M的php可用内容消耗殆尽。
因此,考虑到以下的应用场景:
大数据量交互:导入数据行数在5-10万,列数在10-100,文件大小5M-20M之间
用户依赖:用户只习惯用Excel处理数据
无需表格格式:无论导入导出,用户均对数据表格格式没有要求(边框、底纹、对齐等),仅对数据格式有基本要求(数字、文本、浮点数等)
后端对xls/xlsx大文件的导入性能较差
在这种情况下,考虑csv文件作为数据导入导出的载体,是我认为较为合适的一个选择。相对于xls/xlsx,csv文件的优势如下:
体积小,上传速度快
Mysql原生支持Load data infile from csv,可以快速dump数据
用户学习成本不高,只需要告诉用户保存文件时选择csv格式即可
由于文件本身是纯文本,可以在导入前对数据做验证和其他处理,既降低了内存消耗,又提高了读取速度
所以,最近一段时间系统都采用csv进行数据导入导出。但是,最近发现一些导入导出时的问题,主要是出在字符处理上。
问题起因
最开始发现的问题是这样的:首先我在网站上导出一个数据表,其中有一列是“名单”,也就是一些姓名。然后,我再将这个表原样导入,发现其中有些记录中的姓名居然和我导出之前不一样了。比如说,王玥变成了王h;还有堃这个字不见了等等。很快,我就发现这几个问题主要是出现在一些生僻字上,google一番之后得到了答案:字符集不正确。
问题排查
最开始的搜索结果把我引入了数据导入阶段的问题排查环节。一种说法是数据库的字符集不正确,那么无论你的原始数据是怎样的肯定都不会支持这些生僻字。我们知道,中文系统常见字符集有以下几种:
utf-8:国标字符集,目前应用最广泛,中文字库也是较全的,不存在生僻字问题。
gb2312:中文字符集,很多人都很熟,然而也是这次背锅的主角,生僻字显示错误或者不显示就是因为字符集是他。
gbk/gb18030:还是中文字符集,对上面的生僻字也支持,因此不会出现问题。
上面看似是三种字符集,其实可以分为两类。utf-8和gbk系列的根本区别在于他们的编码方式不同,即两套完全不一样的16进制编码表示双字节字符(汉字);而gbk和gb2312基本相当于超集和子集的关系,只是字库大小有别,编码方式是一样的。
于是,我检查了数据库编码,以及php的编码,很显然,都是utf-8,因此不存在不支持生僻字的问题。那么问题出在哪儿了呢?
回顾一下现有的业务逻辑,我的导出和导入流程基本是这样的:
php读取mysql数据,循环写入csv文件,在写入时用iconv函数从utf-8转换为gbk编码;
打开文件,填入其他需要的数据,保存;
导入数据,其中load data命令已经设置字符集为gbk。
这个时候我就开始百思不得其解了:既然utf-8和gbk都不存在生僻字问题,为什么我就是导入不成功呢?难道是我的转换/导入命令有问题?为了解决这个疑问,我觉得放弃gbk转换,直接使用utf-8进行全程数据交换。于是,重新导出数据后,我先用文本编辑器查看了一下,文件内容正常!但是当我用Excel打开之后……
明明是正常的文件,为什么Excel里变成乱码了呢?
CSV与UTF-8,微软
很显然,Excel对于我的文件编码识别是错误的。第二次google一番之后了解了,csv本身只是纯文本,微软在打开csv文件时,默认都将文件的格式判定为Windows平台的标准格式,即ANSI,所以自然识别不了utf-8的字符。那么怎样才能识别呢?答案很简单,加BOM。
BOM简单的来说就是一个文件头,在文件头部加入0xEF 0xBB 0xBF之后,utf-8就变成了带BOM的文件,再次用Excel打开,果然正常显示了。至此,是否问题已经解决了呢?数据库支持生僻字,导出utf-8文件,加BOM后打开……并没有。因为之后还需要做数据导入,由于额外的文件头存在,导入时会直接报错(因为已经不是原来的纯文本了)。那么,很自然的想到在导入之前去除BOM头。好了,这下导入也可以了。然而,现在又出现几个新的问题:
用户在Excel中直接新建一个文件,保存为csv时的格式为ANSI,因此导入时需要判断文件是否是utf-8,确定字符集
在已经是utf-8时,还需要判断是否是带BOM的,那么就需要读取文件做一下检测,并去除BOM。
带BOM文件仅仅是显示正常,如果直接保存,文件中的列分隔符从默认的逗号变成了空格,直接导致文件内容变得完全不可控了。
因此,在当前的技术方案下,需要考虑编码格式、是否有BOM、分隔符判断等……感觉就像是为了补锅砸了一个更大的窟窿,需要花费更多的精力去填。那么,这条路行不通的话,就回到上一节的问题,如果还是将文件转换为gbk,为何还会有生僻字的问题呢?按照研究得到的资料,gbk应该是完全支持这些字符的啊?
原来是Mac的锅
回到最开始我们导出的那个文件。我将文件放到Windows平台打开,发现里面显示的字符是正确的!这时我才想起来,是最近开始在Mac上做导入导出,才发现这个生僻字的问题,难道是Mac系统的字符集有问题?经过第三次google之后,终于找到了本次问题的最终答案:
Excel在打开一个csv文件时,实际上使用了系统默认的字符集和默认分隔符进行了一次数据导入,而过程对用户是隐藏的。而Mac中的默认字符集的中文字库不支持生僻字。
我们可以做一个实验:打开Excel,选择导入数据(不是直接打开csv文件),选择之前的文件,可以看到第一步选择字符集的界面:
可以看到,默认字符集是Mactonish,这是Mac的默认字符集。这个字符集具体是什么,我没有搜索到,但是我把字符集改为第二项Windows(ANSI)之后,生僻字立刻显示正常了。随后,我又使用简体中文的三个字符集(如上图所示)进行测试,发现Simplified Chinese(GB 2312)类型确实是不支持生僻字的,而Simplified Chinese(Mac OS)同样如此,只有Simplified Chinese(Windows, DOS)格式可以支持。
所以,其实不是我的系统不支持生僻字,而是Mac中的Excel在打开csv文件时的文件导入过程改变了字符集,缩小了字库,导致我的生僻字直接丢掉了。既然这样,问题解决就变得比较简单了:对于Windows用户,不用担心本问题的发生,因为默认字符集就支持生僻字;而对于Mac用户,需要注意下载的数据表不要直接打开,选择用导入数据的方式打开,并选择正确的字符集即可。
生僻字问题的总结
综上,虽然我的问题原因相对比较小众,但问题排查过程中对整个导入导出环节的字符集理解过程我觉得还是很有意义的。我的流程中其他环节其实已经避免了生僻字的问题,但其他开发者在做类似流程时可能会忽略或者错误处理这些环节。因此,总结一下,为了避免导入导出时的生僻字问题,需要注意以下几点:
检查数据库和后端文件的编码,即数据来源的编码,必须为utf-8或gbk等支持中文生僻字的编码。
导出时,如果为utf-8,则转换成gbk编码。
导入时设定字符集,load data infile加上charset关键字。
非Windows系统打开文件使用数据导入功能,而非直接打开,以便确定字符集,防止错误导入。
至此,生僻字的问题得到完整解决。
导出时的格式问题
这次花了一些时间解决生僻字问题后,我也顺便处理了一些导出数据时的格式控制问题。当然,这些问题都是直接能搜到解决方案的,我在这里只是整理归纳分享一下,希望对有同样问题的人能提供快速帮助。主要问题如下:
科学计数法问题:导出数据中存在超过12位的数字时,在Excel中将以科学计数法的形式显示,而这往往不是用户需要的。
前导0问题:导出数据中存在第一位为0的数字时,Excel会忽略掉这个0,将它直接抹去。
逗号问题:默认分隔符是逗号,假如原始数据里也有逗号,则会导致列分隔出错。
为什么会存在这些问题呢?逗号问题的原因显而易见,只要想办法和作为分隔符的逗号区分开就可以了。关于科学计数法和前导0的问题,原因是这样的:csv是纯文本文件,相对于xls来说没有数据类型的信息,所以Excel在打开csv时根据数据的内容自动进行了类型判断。对于一串数字,Excel认为是数字类型,超出12位就会以科学计数法表示;而既然是纯数字,很显然没有以0开头的表示法,所以这个0也就被抹掉了。
解决问题的方法也很简单:加引号。即将文本用=""包裹起来,=在最前,会被Excel识别为一个函数,而双引号包裹表示一个字符串,那么Excel不会对字符串里的内容做任何改动,于是就可以原样输出了。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23此转换函数可以判断字符或数字形式的数字、浮点数、逗号,将需要包裹的文本用=""包裹起来,并对其中的逗号和已经存在的双引号做转义处理。测试数据如下:
1转换后的文本内容如下:
1Excel中显示正常。
转自:Posted by 班登 Dec 6th, 2016 11:32 amphp
网友评论