一次将数据导出为 CSV 格式文件时遇到的坑

I0ncxV

CSV格式简介

CSVComma Separate Values,这种文件格式经常用来作为不同程序之间的数据交互的格式。

格式特点

  1. 每条记录占一行,且记录中字段间以逗号 , 分割,行与行之间用换行符分割;
  2. 逗号 , 前后的空格会被忽略;
  3. 若记录的字段中含有逗号 , ,那么久必须用双引号包括起来;
  4. 字段中包含有换行符,该字段必须用双引号括起来;
  5. 字段前后包含有空格,该字段必须用双引号括起来;
  6. 字段中的双引号两个双引号表示;
  7. 字段中如果有双引号,该字段必须用双引号括起来;
  8. 第一条记录,可以是字段名,相当于表头的位置数据。

普通拼接格式导出

浏览器导出

/**
 * 导出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);
}

参考链接

使用PHP生成并导出CSV文件

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

导出的,第一行和最后一行有空白。

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!