何度となくハマったPHPでのCSV処理・・・もういやだ。
最初は、fgetcsv で setlocaleし忘れでハマり、改行を含むセルでSplFileObjectを知り、SJIS-WIN でハマり・・・もういやだ。
もうCSVで涙目になるのは嫌なので、一個クラスを作る。
要はCP932エンコードされたCSVファイル用の SplFileObject が欲しい!ってことなんですけどね。
マルチバイト用のSplFileObject、mb_SplFileObjectみたいなもの標準で入れてくんないのかな・・・。
ってなわけで?、
SplFileObjectから派生したクラスを定義。ついでに、read / readAll / each メソッドを追加。これは自己満。
コンストラクタでCP932なCSVファイルを引き受けて、UTF-8に変換した作業用ファイルを作成して、デストラクタで消去。
はじめは file_get_contentsで一気に変換しようかと思ったが、巨大サイズのファイルを渡されるとmemory_limitに引っかかるので・・・(^^;
/usr/bin/nkf とか /usr/bin/iconv とかに丸投げしようかと思ったけど、Windows環境だとメンドーだし。
とりあえず、 jQuery のeachみたいな感じのものが欲しかったので・・・
<?php //エクセルから排出したCSVは・・・ $csv = new Csv($csvpath); //CSVがUTF-8なら文字コード変換はスルーする // というか、CSVファイルがUTF-8の場合は、SplFileObjectをそのまま使えばいいじゃん! ってことなんですけどね。 $csv = new Csv($utf8_csvpath,array('encoding' => 'UTF-8')); //テスト出力 $csv->each(function($num,$i,$row) { printf("%03d : %03d : %s\n",$num,$i,$row[1]); });
クラス作るほどのものじゃないんだけどなー・・・・
<?php /* あんまテストしてない。動けばいいや的な。 */ class Csv extends SplFileObject { private static $DEFAULT_OPTIONS = array( 'remove' => false, 'encoding' => 'SJIS-WIN', 'mode' => 'r'); protected static function prepare(&$filepath,&$options) { $path = $filepath . '.utf8'; $fout = new SplFileObject($path,'w'); $fin = new SplFileObject($filepath); $fin->rewind(); foreach($fin as $line) $fout->fwrite(mb_convert_encoding($line,'UTF-8',$options['encoding'])); $fout->fflush(); unset($fin,$fout); $filepath = $path; $options['remove'] = true; } protected $path; protected $options; // constructor & destructor public function __construct($csvpath,$options = array()) { if(!is_array($options)) throw new Exception("second argument is invalid type"); $this->options = array_merge(self::$DEFAULT_OPTIONS,$options); $this->path = $csvpath; if(strlen($this->path) == 0) throw new Exception('CSV file path is required.'); // change encoding... if(!preg_match('/utf-?8/i',$this->options['encoding']) && file_exists($this->path)) self::prepare($this->path,$this->options); parent::__construct($this->path,$this->options['mode']); $this->setFlags(SplFileObject::READ_CSV); } public function __destruct() { if($this->options['remove']) unlink($this->path); } /************************************************************************** * read all and returns array of rows ( helper method ) **************************************************************************/ public function readAll($ignore_first = false) { return $this->read($ignore_first ? 1 : 0,-1); } /********************************************************************* * read and call $callable with CSV row. * $callable must be function with 3 arguments. * first argument is line number, * second argument is index number of loop, * third argument is array of row. * placefolder: function callable($linenumber,index,$row); * and if $callable returns -1, loop process is stop immediately. *********************************************************************/ public function each($callable, $offset = 0) { return $this->read($offset,-1,$callable); } /*********************************************************************** * read csv if $length is -1, returns all. if $callable is set, call $callable and return value num of calls ***********************************************************************/ public function read($offset = 0,$length = 0,$callable = null) { $rv = false; if($length) { $is_call = $callable && is_callable($callable); $count = 0; $num = $offset; $ite = new LimitIterator($this, $offset, $length); foreach($ite as $row) { $num++; if(is_null($row[0])) continue; if($is_call) { if(!is_int($rv)) $rv = 0; $result = call_user_func_array($callable,array($num,$count++,$row)); $rv++; if(intval($result) < 0) break; } else { if(!is_array($rv)) $rv = array(); $rv[] = $row; } } } return $rv; } }