あずきみるくのあずきはニガテ - for Engineer

ひよっこプログラマがやってみたことやハマっちゃったことなど、備忘録的な感じで書いていきます。

cakePHPでCSV取込 - 困ったこといろいろ

PHPにはCSVを取り込む便利な関数がいろいろあるようで。
最初は定番と言われるものを使ってたのですが・・・

いろいろ問題が発生してきました。
その流れとともに、解決方法をメモメモします。
<環境>
PHP 5.4
cakePHP 2.5.8

1)そもそも、どの関数を使うのがいいの?

あずきは最初、何も考えないまま定番?の「fopen して fgetcsv」を使ってました。
これ → PHP: fgetcsv - Manual

だって、検索したら最初に出てくるんだもん。

が、しかし、、、
CSVの区切り文字をスペース区切りやタブ区切りにも対応したい」
となったとき、fgetcsvの第3引数、「delimiter」には1文字しか指定できないのです。

fgetcsv(ファイルポインタ, length, delimiter(ここ、ね));

「\s」とか「\t」とか、書けないんですよ。
これを指定すると、
「1文字だけって言ってるでしょ!!」っておこられる。

じゃあ、どうやって区切り文字を実現するか。
仕方ないんで、ごりごり切りましたよ。
・・・「preg_split」で。。。
残念ですね。


なんか他にやりようがないか、しらべていたら、こんな記事を見つけました。
【PHP】その CSV 変換、本当に「fgetcsv」でいいの? (フェンリル | デベロッパーズブログ)
ふむ。とても興味深い。見てるとワクワクしますねw
CSV取込だけでもこんなに方法があるんですね。
オススメは「SplFileObject::READ_CSVとな。
調べてみます。

2)SplFileObject

どうやらファイル全般、扱えるようですが、ことCSVに関しては、
「SplFileObject::READ_CSV
という、すてきな物が用意されているようです。

ただ、同時に気になる記事も見つけました。
zBlog
なんと、

SplFileObjectではsjisCSVが処理できないからUTF-8にするしかない

だそうです。

ついでに言うと、SplFileObjectを使って実装したところ、

  • 最終行の改行も登録される
  • 空行も登録される

という問題が発覚しました。

fgetcsvなら上手いことやってくれたのに。

意外と問題が多いです。
が、当初の目的の区切り文字は簡単に変更できそうです。
乗りかけた船なので、乗り切っちゃいます。

3)区切り文字を変更する

案外、簡単にできました。

 $file = new SplFileObject($filepath); 
 $file->setFlags(SplFileObject::READ_CSV);
 $file->setCsvControl($delimiter, $enclosure);  // ←ここです。

マニュアルはこちら
PHP: SplFileObject::setCsvControl - Manual

区切り文字(delimiter)だけでなく、囲み文字(enclosure)も楽々設定できます。

ここはさくさく解決したので、SplFileObjectの問題を解決しに行きます。

4)SJISのファイルも読み込みたい

Excelで作られたCSVは基本的にエンコードSJIS(Shift−JIS)になるようです。
とりあえず、これも読み込みできるように。

こうなりました。

$str = mb_convert_encoding($str, "UTF-8", "auto");

・・・ええ、「mb_convert_encoding」使っただけです。

ただ、ここで注意!!
このまま実行すると・・・
「Unable to detect character encoding」ってエラーが発生する可能性があります。
ってか、発生しましたorz

mb_convert_encoding関数でUnable to detect character encodingというエラーが出る件 - 大人になったら肺呼吸
こちらの先生の教えに従い、「mb_language("Japanese");」を足します。

mb_language("Japanese");
$str = mb_convert_encoding($str, "UTF-8", "auto");

こうなりました。これで、’auto’の呪縛から逃れることができます。

5)空行や改行も読まれる

めんどくさいです。。。
SplFileObjectのマニュアルのページに
「SplFileObject::SKIP_EMPTY」なるものを見つけ、
ちょっとだけ小躍りしましたが、、、つかえませんでした。
使えないんですよ!!

ここは、力技で押し切ります。

後で、全部まとめたソース書いてますので、そちらをご覧ください。

できた!!

以上のもやもやを取り込んでみたソースがこちらです。

$file = new SplFileObject($filepath); 
$file->setFlags(SplFileObject::READ_CSV);
$file->setCsvControl($delimiter, $enclosure);

foreach ($file as $key => $line) {
      // 空行をチェックするための配列とフラグを初期化
      $tmp_array = array();
      $set_flg = false;

      foreach( $line as $str ){
          // エンコードをすべてUTF-8に変換
          mb_language("Japanese");
          $str = mb_convert_encoding($str, "UTF-8", "auto")
          $tmp_array[] = Sanitize::clean($str);
      }
      // 空行かどうかチェック
      foreach($tmp_array as $tmp){
          if(!empty($tmp)){
              $set_flg = true;
          }
      }
      // 空行でなければ登録する
      if($set_flg){
           $records[] = $tmp_array;
      }    
}
 return $records;  // よみこんだやつ

うーん。。。やっぱり力技な所が気になります。

いい方法をご存知の方がおられたら、ぜひご教授くださいm(_ _ )m


おまけ:ファイルサイズチェックしたい

ファイルサイズに上限を設けておくと、もんのすごい重いファイルを読み込まされるリスクが減ります。
そんなわけで、こんなチェックもできるよ、ってことで。

// ファイルサイズチェック(50KBまで)
        $size = filesize($file_path);
        if(!$size || $size > 100000){
           echo 'ファイルサイズが1MBを超えています';
        }

なんて簡単なんでしょ。


取込の次は、きっと出力です。
こちらも問題まみれだと思うので、頑張ります!!

ご覧になった皆様からのありがたいコードレビュー?!もよろしくお願いいたしますw