フェンリル

Developer's Blog

【PHP】その CSV 変換、本当に「fgetcsv」でいいの?

 WebDevBlogTitle

こんにちは。ウェブ開発担当の木戸です。

突然ですが、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 アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

  

Facebook コメント

コメント

トトロ2014年07月02日 11:31

単純にカンマやタブで explode した場合って、
"colmun1","c,o,l,m,u,n,2,","column3"
のようなレコードを正しく読めないのではないですか?
enclosure も考慮されていません。
そのへんのオーバーヘッドを省略して「速い!」と言うのは正しくない気がします。

kido2014年07月02日 17:13

>トトロさん

コメントありがとうございます。
ご指摘頂いた点を追記修正いたしました。

メイ2014年07月06日 3:37

PHP5.5が主流になろうとしているので、5.3とか4系よりも新しいバージョンも含めて検証していただけないでしょうか。

名前(必須)

メールアドレス(必須)

URL

スタイル用のタグが使えます

このコメント欄でのご質問、ご要望には、開発チームから回答できない場合があります。ご質問、ご要望は「User Community」内のフォーラムまでお寄せください。