fgetcsv 備忘録

CSVファイルから行ごとに配列へ変換してくれるfgetcsvですが、PHP5になってロケール設定を参照するようになり、googleでの検索では他のキーワードとして「fgetcsv 文字化け」「fgetcsv バグ」などが提示されるなどかなり不遇な扱いです。

が、オンラインの関数リファレンスにもこの関数はロケール設定を考慮します。としっかり注意書きが明記されてるので、裏を返せばサーバのロケール設定でShift_JISを扱えるようにすればExcelで作成したCSVファイルでも問題なく扱えるってことでもあり、多分PHPでCSVファイルを処理する最もスマートな方法ってことでもあります。

ということでロケールにShift_JISの定義を追加してみます(fedoraの場合)。

まずはロケールの確認。

locale -a | grep ja
ja_JP
ja_JP.eucjp
ja_JP.ujis
ja_JP.utf8
japanese
japanese.euc

ということでロケールにShift_JISは含まれてません。この状態ではPHPでfgetcsvを使う前に

setlocale('LC_ALL', 'ja-JP.sjis');

としてもロケールが設定できないので、Shift_JISの定義をロケールに追加する必要があります。

sudo localedef -f SHIFT_JIS -i ja_JP ja_JP.SJIS
キャラクタマップ `SHIFT_JIS' は ASCII 互換ではありません, ロケールは ISO C に従っていません

なんか怒られますが、実行後もう一度ロケールを確認すると

locale -a | grep ja
ja_JP
ja_JP.eucjp
ja_JP.sjis
ja_JP.ujis
ja_JP.utf8
japanese
japanese.euc

と、ja_JP.sjisが追加されてます。ここまで終えるとサーバの再起動後にShift_JISをロケールに追加完了です。ちなみにPHPでは標準のsetlocaleを使うことでプログラム実行中にロケール情報を設定できるので、Shift_JISの定義をロケールに追加するだけでOKで、サーバのロケールを変更するとかは必要ありません。

とりあえずサンプルとして日本郵便郵便番号データダウンロードから適当にCSVを貰ってきてテストしてみます。なお、ここにあるCSVファイルは郵便番号や住所が入っているカラムはダブルクォートで括られているのですが、内容がダブルクォートで括られていると問題が起きにくいので、今回のテストでは一旦ダブルクォートは全部消してしまいます。

で、

<?php
$file = dirname(__FILE__) .'/26KYOUTO.CSV';

echo "<h1>fgetcsv() TEST</h1>\n";
echo sprintf( "<p>LC_ALL=%s</p>\n", setlocale(LC_ALL, 'ja_JP.sjis') );

if ( file_exists($file) ) {
   $fh  = fopen($file, "r");
   $row = 0;

   while ( ( $data = fgetcsv($fh, 1024, ',') ) !== FALSE ) {
       $rows++;

       echo sprintf( "<h2>LINE %d</h2>\n", $rows, $cols         );
       echo sprintf( "<pre>%s</pre>\n",    print_r($data, true) );
   }

   fclose($fh);
}
else {
   echo "File not found, $file";
}

これでfgetcsv利用時に文字化け(全部/一部とも)が起こらなくなるはずです。fgetcsvもちゃんと設定さえしてやれば巷の評判よりは(きっと)できる子です。

とはいえ、なんでPHP5でfgetcsvだけがロケール設定を参照するようになったんでしょうね。fgetcsvのためだけにサーバの設定をおいほれと変えられない状況もありえると思うんですけど。

そういう場合はCSVファイルの内容をロケール設定可能な任意の文字コードに変換した上で一時ファイルへ書込み、その一時ファイルをfgetcsvに通してやることで対処可能。この方法はPHP5 の fgetcsv() で読み込み内容が腐る現象でも書かれてます。

あるいはPHP5でfgetcsvが正常に動作しないで紹介されている fgetcsv_reg も試してみた限り大丈夫そうです。