[PHP]fputcsv()でCSVを出力する際に各フィールドにダブルクォートを付けるには

Read More

最近CSVファイルを触ることが多くなってきたシラサヤです。
でも毎回微妙に要件が違うのでなんか色々めんどくさい。



さて今回は、タイトルに書いたものが要件としてあったわけだけど、
PHP関数のfputcsv()を使ってCSVを出力する際に各フィールドにダブルクォートを付けるってのは、


できない






はい、できません。
PHPマニュアル見たけど無理ですな。



え~~~~じゃあどうすんの?


というわけで自作関数の出番。


■全フィールドを""(ダブルクォート)で括る自作関数mb_fputcsv()
<?php
/**
 * 配列の値をCSV形式にフォーマットする
 * ****************************************40
 * fputcsv()の改良版。
 * 基本的な動作はPHP関数のfputcsv()と同じ。
 * 
 * 相違点:
 *   数値型文字型を考慮せず全てのフィールドに対し""(ダブルクォート)で括るという点。
 *   Shift-JISではなくUTF-8にしか対応していない点。
 *   
 * 注意点:
 *   第5引数をtrueにすると「\"」(バックスラッシュ ダブルクォート)という方言エスケープのかかった文字列に対しても、RFC4180に準拠したエスケープ処理を行う。
 *   PHPのfgetcsv()、str_getcsv()とRFC4180はエスケープの解釈がそれぞれ異なるため、これを行ったCSVファイルにはこれらのPHP関数は利用できなくなる。
 *   ただし一般的にCSVファイルを取り扱うことの多いEcxelの動作はRFC4180のものである。
 * 
 * @param resource $fp=null
 * @param array $fields=null
 * @param string $delimiter=',' // 将来的な予約
 * @param string $enclosure='"' // 将来的な予約
 * @param boolean $rfc=false
 * @return int // 書き込んだ文字列の長さ。失敗したらFALSE 
 */
function mb_fputcsv($fp=null, $fields=null, $delimiter=',', $enclosure='"', $rfc=false) {
  $str=null;
  $chk=true;
  
  if($chk) {
    $cnt=0;
    $last=count($fields);
    foreach($fields as $val) {
      $cnt++;
      if(!$rfc) {  // fputcsv()の挙動
        $val = preg_replace('/(?<!\\\\\\\\)\"/u', '""', $val);
      }else {      // RFC4180に準拠
        $val = preg_replace('/\"/u', '""', $val);
      }// end if
        $str.= '"'. $val. '"';
      if($cnt!=$last) $str.= ',';
    }// end foreach
  }// end if
  if($chk) $chk = fwrite($fp, $str);
  if($chk) $chk = fwrite($fp, "\r\n");
  
  return $chk;
}// end function
?>



はい、なんかごちゃごちゃやってます。


まあ関数のコメントにも書いていることの繰り返しになるんだけど、
基本的な動作はPHP関数のfputcsv()と同じ


違う点は数値型文字型を考慮せず全てのフィールドに対し""(ダブルクォート)で括るというとこ。
あと、PHPファイルをUTF-8で書くパターンが自分は多いので、出力はShift-JISではなくUTF-8のままとなっているのも違う点。


そしてfputcsv()の最大の注意点とも言える「\"」(バックスラッシュ ダブルクォート)の問題。
以後これを「方言」と表現する。


CSVにはいくつかの方言と呼ばれるものが存在する。
ただ方言という言い方をしたもののCSVの実装の歴史をみればこの方言の方がRFC4180より先に存在しているという。


一応こういったことにも対応できるようにしておいた。

それがこのコメントの部分
 *   第5引数をtrueにすると「\"」(バックスラッシュ ダブルクォート)という方言エスケープのかかった文字列に対しても、RFC4180に準拠したエスケープ処理を行う。
 *   PHPのfgetcsv()、str_getcsv()とRFC4180はエスケープの解釈がそれぞれ異なるため、これを行ったCSVファイルにはこれらのPHP関数は利用できなくなる。
 *   ただし一般的にCSVファイルを取り扱うことの多いEcxelの動作はRFC4180のものである。


第5引数trueを渡せばRFC4180に準拠したエスケープに切り替わる。
け、ど、これを使うなら使うで今度はfgetcsv()str_getcsv()RFC4180対応バージョンを作らんといけんね。

まあ今回はCSVファイルを出力して別のサーバに渡すだけなの要件なのでfgetcsv()もstr_getcsv()も使わない。
なので公開はしません
第5引数はfalseのままお使い下さい。



ちなみに関数内で使っているちょっと変わった正規表現は肯定的後読みと言う。
ここがわかりやすい↓
ttp://d.hatena.ne.jp/a_bicky/20100530/1275195072





--
さて、と。
mb_fputcsv()のテスト用に適当な配列を用意してCSVファイルを出力してみる。


<?php
// 1: 文字列、2: ダブルクォート含み、3: 円マーク含み、4: 数値
// 5: カンマ含み、6: 改行含み、7: ダブルクォートを方言でエスケープ
$csv = array(1=>'a', 2=>'"b', 3=>'\\\\c', 4=>4, 5=>'e,e', 6=>'f
', 7=>'\\\\"g');

$sFilePath = 'C:/test.csv';

$chk=true;
if($chk)$chk = $fp = fopen($sFilePath, 'w+');
if($chk)$chk = mb_fputcsv($fp, $csv);
if($chk)$chk = fclose($fp);
print $chk;
?>



ちなみに用意した$csv配列はPHPのエスケープが働くのでバッククォートが倍の数になっている。
PHPのエスケープを外した状態だとこんな感じの値が入っている。
# シングルクォートの中だけどこれはあくまでイメージ。
array (
  1 => 'a',
  2 => '"b',
  3 => '\c',
  4 => 4,
  5 => 'e,e',
  6 => 'f
',
  7 => '\"g',
);




さっき出力したCSVファイルをnotepadで開いてみる。


こちらはmb_fputcsv()のもの。


こちらはfputcsv()のもの。



notepadで開いたのはダブルクォートの付き方を確認するため。

mb_fputcsv()の方はフィールドのすべてがダブルクォートで括られていることがわかる。
その他の動作はいっしょ。





ついでにmb_fputcsv()第5引数trueのもの。


こちらは例の「\"」(バックスラッシュ ダブルクォート)の部分もPFC4180的な解釈をしている。





それぞれ要件にあった使い方をしたい。




・関連
[PHP]str_getcsv()があるんならstr_putcsv()があってもいいじゃない
http://xirasaya.com/?m=detail&hid=407







-- 2014/04/09 一部修正:
バックスラッシュの表記でその数が少なく表示されていたのを修正。

-- 2014/04/11 文字修正: 関数名をmy_fputcsv()からmb_fputcsv()に変更