经过一个下午的折腾,上周五还是发现出现该异常情况的原因。概括来说还是中文转编码的问题,即utf-8转gbk出现的小部分行缺少空格或者双引号的问题。
出现问题的原因:
在进行导出的过程中,接收其他程序传过来的数组参数,均为utf-8编码,我这边在调用fwrite或者fputcvs写入文件的过程中并没有进行 iconv("UTF-8", "GB2312//IGNORE", $header),而是将文件保存在了 export目录下,然后由vuejs做的后台调用位于同application下的一个共用类的 export方法,该方法是这样的:
public function export()
{
$file = \Request::get('file');
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename=export.csv');
header('Cache-Control: max-age=0');
$contents = file_get_contents(\Env::get('runtime_path') . '/export/' . $file);
echo iconv("UTF-8", "GB2312//IGNORE", $contents);
@unlink(RUNTIME_PATH . '/export/' . $filename);
exit;
}
问题就出在 echo iconv("UTF-8", "GB2312//IGNORE", $contents); 这句代码上面,这里看到是对传入的整个文件进行转编码的,这里未进行测试到底多少行的数据会出现缺少逗号引号的问题。
最终的解决思路是,把共用类中对整个文件的转编码放到写入方法中,逐条转编码就解决了该问题。
更改后的代码:
public static function createCsv($data, $header = [], $filename = '')
{
// 参数判断
$data = is_object($data) ? $data->toArray() : $data;
$header = is_array($header) ? $header : [];
$filename = (1 > strlen(trim($filename))) ? 'csv-' : trim($filename);
if (empty($data)) {
return false;
}
// 文件名/目录
$filename = $filename . date("YmdHis", time()) . rand(1000, 9999) . ".csv";
$dir = Env::get('runtime_path') . '/export/';
if (!is_dir($dir) && !mkdir($dir)) {
return false;
}
// 打开文件指针资源
$handle = fopen($dir . $filename, 'w+');
if (!$handle) {
return false;
}
// 写入文件header头
if (!empty($header)) {
foreach ($header as $key => $item) {
$header[$key] = iconv("UTF-8", "GB2312//IGNORE", $item);
}
$res = fputcsv($handle, $header);
if (!$res) {
return false;
}
}
// 判断header是索引数组还是关联数组
$is_assoc = array_keys($header) !== range(0, count($header) - 1);
// 写入文件内容
$frequency = 0; // 频率
$limit = 100000;
foreach ($data as $datum) {
$frequency++;
if ($limit == $frequency) {
// 刷新输出buffer
ob_flush();
flush();
$frequency = 0;
}
// 如果是关联数组,则获取内容中和头部key相对应的值
if ($is_assoc) {
$csv = "";
foreach(array_keys($header) as $item) {
$datum[$item] = iconv("UTF-8", "GB2312//IGNORE", $item);
$val = str_replace('"', '""', $datum[$item]); // 将单个双引号替换为两个双引号
$csv .= '"' . $val . '",'; // 为每个字符增加双引号,并添加逗号分割符
}
$csv = substr($csv, 0, -1); // 去掉每行最后一个逗号
$csv .= "\n"; // 添加换行符
$res = @fwrite($handle, $csv);
} else {
$res = fputcsv($handle, $datum);
}
if (!$res) {
return false;
}
}
// 关闭指针资源
fclose($handle);
return Url::build('erp/Common/export', 'file=' . $filename);
}
public function export()
{
$file = \Request::get('file');
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename=export.csv');
header('Cache-Control: max-age=0');
$contents = file_get_contents(\Env::get('runtime_path') . '/export/' . $file);
echo $contents;
exit;
}
调用步骤:
先调用createCsv生成文件,再调用export下载。
———————————————————————分割线——————————————————————
另外一种解决思路:
csv文件直接存储 UTF-8 编码:
即不需要进行 iconv 转编码,这样会有个小问题就是MacOS中的Excel无法自动识utf-8编码,中文会出现乱码的情况。
原因:
Excel在读取csv的时候是通过读取文件头上的bom来识别编码的,如果文件头无bom信息,则默认按照unicode编码读取。(这个bom是微软自己定义的一种文件头部协定,顾名思义存储在文件头部,存储内容就是标识文件编码的信息。)而我们生成csv的平台不一定遵循微软的bom协议,导致如果输出非unicode编码的csv文件(例如utf-8),并且没有生成bom信息的话,Excel自动按照unicode编码读取,就会出现乱码问题了。
作者:李蛟 链接:https://www.zhihu.com/questio...
来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
根据知乎查到的答案,原来是这里未遵循微软的bom协议。原因找到了,解决方案就能出了。
解决方法:
fopen 方法下写入bom头,这里简单写一下步骤
function createCsv($filename, $header, $data) {
$handle = fopen($filename, 'w+');
// 添加BOM,标识为UTF-8格式
fwrite($handle, chr(0xEF).chr(0xBB).chr(0xBF));
// 写入头部
fputcsv($handle, $header);
// 逐行写入内容
foreach ($data as $datum) {
fputcsv($handle, $datum);
}
// 关闭指针资源
fclose($handle);
}
另外再补充一下 Bom 简介
在UCS 编码中有一个叫做”ZERO WIDTH NO-BREAKSPACE”的字符,它的编码是FEFF。
FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。
UCS规范建议我们在传输字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。
这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;
如果收到FFFE,就表明这个字节流是Little-Endian的。
因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。
字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF。
所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
Windows就是使用BOM来标记文本文件的编码方式的。