一次將資料匯出為 CSV 格式檔案時遇到的坑

alalala發表於2020-04-17

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 協議》,轉載必須註明作者和本文連結

相關文章