一次将数据导出为 CSV 格式文件时遇到的坑
CSV格式简介
CSV 即 Comma Separate Values
,这种文件格式经常用来作为不同程序之间的数据交互的格式。
格式特点
- 每条记录占一行,且记录中字段间以逗号
,
分割,行与行之间用换行符分割; - 逗号
,
前后的空格会被忽略; - 若记录的字段中含有逗号
,
,那么久必须用双引号包括起来; - 字段中包含有换行符,该字段必须用双引号括起来;
- 字段前后包含有空格,该字段必须用双引号括起来;
- 字段中的双引号用两个双引号表示;
- 字段中如果有双引号,该字段必须用双引号括起来;
- 第一条记录,可以是字段名,相当于表头的位置数据。
普通拼接格式导出
浏览器导出
/**
* 导出CSV文件
*/
function exportCsv()
{
// 需要导出的内容
$data = [
['name' => '张三', 'score' => '80'],
['name' => '李四', 'score' => '90'],
['name' => '王五', 'score' => '60'],
];
// 文件名,这里都要将utf-8编码转为gbk,要不可能出现乱码现象
$filename = $this->utfToGbk('导出csv文件.csv');
// 拼接文件信息,这里注意两点
// 1、字段与字段之间用逗号分隔开
// 2、行与行之间需要换行符
$fileData = $this->utfToGbk('姓名, 分数') . "\n";
foreach ($data as $value) {
$temp = $value['name'] . ',' .
$value['score'];
$fileData .= $this->utfToGbk($temp) . "\n";
}
// 头信息设置
header("Content-type:text/csv");
header("Content-Disposition:attachment;filename=" . $filename);
header('Cache-Control:must-revalidate,post-check=0,pre-check=0');
header('Expires:0');
header('Pragma:public');
echo $fileData;
exit;
}
/**
* 字符转换(utf-8 => GBK)
*/
function utfToGbk($data)
{
return iconv('utf-8', 'GBK', $data);
}
通过命令行方式导出数据
/**
* 下载CSV文件
*/
public function downLoadCsv()
{
// 需要导出的内容
$data = [
['name' => '张三', 'score' => '80'],
['name' => '李四', 'score' => '90'],
['name' => '王五', 'score' => '60'],
];
// 文件名,这里都要将utf-8编码转为gbk,要不可能出现乱码现象
$filename = $this->utfToGbk('生成csv文件.csv');
// 拼接文件信息,这里注意两点
// 1、字段与字段之间用逗号分隔开
// 2、行与行之间需要换行符
$fileData = $this->utfToGbk('姓名, 分数') . "\n";
foreach ($data as $value) {
$temp = $value['name'] . ',' .
$value['score'];
$fileData .= $this->utfToGbk($temp) . "\n";
}
$filePath = __DIR__ . '/' . $filename;
// 将一个字符串写入文件
file_put_contents($filePath, $fileData);
return $filePath;
}
/**
* 字符转换(utf-8 => GBK)
*/
public function utfToGbk($data)
{
return iconv('utf-8', 'GBK', $data);
}
注意:
上述导出方式中,导出的数据格式均是采用代码拼接而成。
在实践中,发现这种导出方式在遇到一些长形文本尤其是其中含有特殊字符的,就会导致一个字段中的文本在导出的 CSV
文件中占据好几行的空间,显示错乱。
因此,更推荐下面的 fputcsv 函数导出。
fputcsv 函数导出
fputcsv ( resource
$handle
, array$fields
[, string$delimiter
= ‘,’ [, string$enclosure
= ‘“‘ ]] ) : int
fputcsv — 将行格式化为 CSV 并写入文件指针。
fputcsv 将一行(用 fields
数组传递)格式化为 CSV 格式并写入由 handle
指定的文件。
参数
handle
必选参数,文件指针必须是有效的,必须指向由 fopen() 或 fsockopen() 成功打开的文件(并还未由 fclose() 关闭)。
fields
必选参数,存储一行各字段数据的数组。
delimiter
可选参数,
delimiter
参数设定字段分界符(只允许一个字符),也就是每行字段之间分界符,如逗号,
。enclosure
可选参数,
enclosure
参数设定字段字段环绕符(只允许一个字符),如双引号""
。
返回值
成功时,返回写入字符串的长度, 或者在失败时返回 FALSE
。
示例
$list = array (
array('aaa', 'bbb', 'ccc', 'dddd'),
array('123', '456', '789'),
array('"aaa"', '"bbb"')
);
$fp = fopen('file.csv', 'w');
foreach ($list as $fields) {
fputcsv($fp, $fields);
}
fclose($fp);
输出:
aaa,bbb,ccc,dddd
123,456,789
"""aaa""","""bbb"""
通过命令行方式导出数据
// 按教师作文号码 导出大赛数据
function exportDataByRid($dir,$ridArr){
$arr = [];
foreach($ridArr as $k => $rid) {
$essayList = $this->db()->getAll("select user_id,essay_id,title,essay,score,stu_number,stu_class from eng_essay where request_id = $rid and type >= 0");
$user_ids = array_column($essayList, 'user_id');
$user_ids = array_unique($user_ids);
$userInfos = $this->member()->getMembersByIds($user_ids, ['name','school']);
// 存放标准数据的数组
$dataList = [];
// 遍历原数组,重组顺序
foreach($essayList as $key => $essay) {
$dataList[$key]['user_id'] = $essay['user_id'];
$dataList[$key]['name'] = $userInfos[$essay['user_id']]['name'];
$dataList[$key]['school'] = $userInfos[$essay['user_id']]['school'];
$dataList[$key]['stu_class'] = $essay['stu_class'];
$dataList[$key]['stu_number'] = $essay['student_number'];
$dataList[$key]['essay_id'] = $essay['essay_id'];
$dataList[$key]['title'] = $essay['title'];
$dataList[$key]['essay'] = $essay['essay'];
$dataList[$key]['score'] = $this->score($essay['score']);
}
$filename = $this->utfToGbk($rid . '.csv');
// csv 文件的头部
$fileData = $this->utfToGbk('用户ID,姓名,学校,班级,学号,作文号,题目,内容, 分数') . "\n";
if( !is_dir( $dir) ){
if( !mkdir($dir )) return array('error'=>'目录不不可写!');
}
$filePath = $dir.$filename;
// 生成 文件 handler
$file = fopen($filePath,"w");
fputcsv($file, explode(',', $fileData));
//计数器
$num = 0;
//每隔$limit行,刷新一下输出buffer,不要太大,也不要太小
$limit = 100000;
foreach ($dataList as $value) {
$num++;
//刷新一下输出buffer,防止由于数据过多造成问题
if ($limit == $num) {
ob_flush();
flush();
$num = 0;
}
array_walk($value, function (&$val, $key){
$val = $this->utfToGbk($val);
});
fputcsv($file, $value);
}
// 记得关闭
fclose($file);
$arr[] = $filePath;
}
print_r($arr);
}
/**
* 字符转换(utf-8 => GBK)
*/
public function utfToGbk($data)
{
return iconv('utf-8', 'GBK', $data);
}
注意
优点
这种情况下,即使长文本字段中也不会出现占行错乱的问题,如 上述代码中的 essay
字段。
缺陷
但是还有一个问题,就是当字段内容在使用 iconv
函数进行 GBK 转换(如上述代码中的 iconv('utf-8', 'GBK', $data);
)时,存在无法转换的内容或者特殊字符,那么转换过程中就会报错,从而导致在导出的 CSV 中 整个字段内容的丢失。
解决方案
解决上述整个字段内容的丢失的问题,我们需要在使用 iconv
转化字段内容字符集时,在目的字符集的后面添加上 //IGNORE
标示,使得在转换过程中遇到错误时,忽略错误,接着运行,最终获得字段完整内容,从而顺利导出。
/**
* 字符转换(utf-8 => GBK)
*/
public function utfToGbk($data)
{
return iconv('utf-8', 'GBK//IGNORE', $data);
}
参考链接
本作品采用《CC 协议》,转载必须注明作者和本文链接
导出的,第一行和最后一行有空白。