こんにちは。ウェブ開発担当の木戸です。
突然ですが、PHP で CSV ファイルを連想配列に変換してゴニョゴニョ…ってよくやりますよね?
私も先日開発中に、CSV の関数なんだったかなーと思って「php csv」で検索していたのですが、ファーストビューが定番の fopen して fgetcsv の公式サンプルや記事ばかりでタイトル通り「?」だったので、調査してみたところ、意外と面白いことがわかりました。
やっぱり fgetcsv だけじゃなかった
似たような機能を持つ関数が複数あり、公式ドキュメントを見てもどれを使っていいかわからない、なんてこと PHP ではよくありがちです。CSV 変換もやっぱりいろんな方法がありました。
そこで今回は、あらゆる CSV 変換処理を検証し、処理時間とメモリ使用量を比較します。処理はレコードごとの連想配列に変換するのみで、よくセットでやる文字コードの変換や、空行チェック、ファイル終端チェックなどはしません。
使用する CSV は 1 レコードが 5 カラムの 50B で10万件のものを用意しました。実行環境は Virtualbox CentOS 6.5。1CPU 2 コア 3.20GHz。メモリ 1G 。PHP version 5.4.28 です。
fopen + fgetcsv
まずはいつもの fgetcsv です。
if (($handle = fopen($filepath, "r")) !== false) { while (($line = fgetcsv($handle, 1000, ",")) !== false) { $records[] = $line; } fclose($handle); }
Time: 1.122s
MemoryPeak: 110.75MB
プログラムは数回実行していますが Time は微差です。これを基準に以下も見ていきましょう。
PEAR File_CSV
かなりレガシーですが、PEAR に CSV 操作用のクラスがあります。
$conf = File_CSV::discoverFormat($filepath); while ($line = File_CSV::read($filepath, $conf)) { $ret[] = $line; }
Time: 11.275s
MemoryPeak: 111.50MB
なんと 10 秒以上かかってしまいました。
さらに他処理にない点として、レコード全てのカラム数が一致しないと、途中で false になって止まるのも信じ難い仕様ですね…
file_get_contents + str_getcsv (explode)
PHP 5.3 以上なら、CSV を指定文字でパースできる str_getcsv が使えます。
$buf = file_get_contents($filepath); $lines = str_getcsv($buf, "\r\n"); foreach ($lines as $line) { $records[] = str_getcsv($line); }
Time: 1.491s
MemoryPeak: 136.5MB
あれ…? file_get_contents でメモリを多く使うのは仕方ないとして、foreach を使っても fgetcsv より遅いのは意外でした。
ではレコード分割処理は、処理の速い explode でやってみます。
$buf = file_get_contents($filepath); $lines = explode("\r\n", $buf); foreach ($lines as $line) { $records[] = str_getcsv($line); }
Time: 1.136s
MemoryPeak: 136.5MB
少し速くなりました!いっそ explode のみでやってみましょう。
$buf = file_get_contents($filepath); $lines = explode("\r\n", $buf); foreach ($lines as $line) { $records[] = explode(",",$line); }
Time: 679ms
MemoryPeak: 136.5MB
!? これは速い! str_getcsv が相当重いようですね。
(2014/7/2 追記) ただしレコードを explode すると、”a”,”b,c,d,”,”e” のような enclosure にカンマ付きのものだと、正しく取得できません。
SplFileObject
ファイル操作用インターフェースです。狙って調べないとなかなかヒットしないのでマイナーかも。
$file = new SplFileObject($filepath); $file->setFlags(SplFileObject::READ_CSV); foreach ($file as $line) { $records[] = $line; }
Time: 630ms
MemoryPeak: 110.75MB
なんと explode よりも速い!さらにメモリも Good です。
期待をこめて、SplFileObject 内メソッドの fgetcsv もやってみます。
$buf = new SplFileObject($filepath); while (!$buf->eof()) { $ret[] = $buf->fgetcsv(); }
Time: 1.565s
MemoryPeak: 110.75MB
…はい、以上です。
まとめ
いかがでしたでしょうか。パフォーマンスだけでも大きな差がありましたね。
以下に、検証結果に対応PHPバージョンを含めて表にしてまとめました。速度順にソートしています。
処理 | Time | MemoryPeak | Version | |
1 | SplFileObject::READ_CSV | 630ms | 110.75MB | 5.1.0~ |
2 | file_get_contents + explode | 679ms | 136.5MB | 4.3.0~ |
3 | fopen + fgetcsv | 1.122s | 110.75MB | 4~ |
4 | file_get_contents + explode + str_getcsv | 1.136s | 136.5MB | 5.3.0~ |
5 | file_get_contents + str_getcsv | 1.491s | 136.5MB | 5.3.0~ |
6 | SplFileObject::fgetcsv | 1.565s | 110.75MB | 5.1.0~ |
7 | PEAR File_CSV | 11.275s | 111.50MB | 4.3.0~ |
この結果からふまえると、
PHP 5.1 以上を使えるなら SplFileObject::READ_CSV で決まりですね。ただし、バージョンによって使えるメソッドが異なる点には注意しましょう。
PHP 4 系を使ってるサーバもまだまだあると思いますが、スピードを重視する場合は、レコード内容に注意しつつ explode の使用を検討していいかと思います。メモリも気にするなら定番の fopen + fgetcsv をオススメします。
要件にあわせて、fgetcsv 以外も 本当に? という想いで是非使ってみてください。
以上です。
フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!