PHPでファイル一覧を取得するならglobよりopendirのほうが27倍早い
PHPのglob関数って便利ですよね。
$result = glob('*.jpg');
拡張子jpgのファイル一覧がほしいなぁ~と思ったら、こんな感じでサラっと1行で書けるし、後から見た人にもわかりやすい。良い関数だと思います。
ただ、注意点もけっこうあって個人的には本運用ではなるべくopendirを使いたいと思っているのですが、まるでopendirは古臭い書き方で、いまどきはglob関数で書くほうがスマートです(ドヤァ ※誇張表現)みたいな記事を見かけたので、脳死でglob使うのは違うぞ、特に速度を重視するならopendirのほうが良いぞ、というのを少しでも広めたく、記事を投稿しようと思いました。
お急ぎの方のために先に結論を述べておきますが、我が家の環境※においては2000個のファイル一覧を取得する際、glob関数を使うよりopendir関数を使ったほうが27倍ほど早くなりました。
(当たり前ですが存在するファイル数が少なければ少ないほどこの差は縮まります)
glob関数は相対パス指定時にやや使いづらい
先ほどのように現在のカレントディレクトリにあるファイル一覧を取得するだけなら便利なんですよ。でも、実運用においてはコードの使いまわしがしやすいように、相対パスないし絶対パスで指定するケースが多いのではないでしょうか。
$result = glob('../images/*');
例えばこんなふうに「一階層上にあるimagesディレクトリ内のファイル一覧を取得したい、とか。
特にブログシステムのようなCMS(コンテンツ・マネージメント・システム)を社内で開発していて、画像や資料の一覧をWebシステム上に表示するようなケース。Webサーバー上でのディレクトリでは "../images" で良いものの、Webページ上に表示するときには [トップURL]+[ファイル名] のように加工する必要がありますよね。
しかし、glob関数に相対パス指定をするとそのパスも含めたファイル名が返ってきてしまうのが困りもの。返されるファイル名が "../images/1.jpg", "../images/2.jpg", "../images/3.jpg" と、全部ディレクトリ名付きになっちゃうんですね。絶対パス指定をした場合も同様。
ですから、そういう場合はbasename関数を使ってファイル名のみを取り出します。
foreach (glob('../images/*') as $file) {
$file = basename($file);
echo '<a href="http://example.com/'.$file.'">'.$file.'</a> ';
}
書き方自体はシンプルですし、コードの見た目もわかりやすい。
…ただなぁ。内部の動作を想像すると、すごく無駄なことをしている気がして気分が悪くなりませんか?
必要のないディレクトリ名を全ファイル数分メモリに格納して、その後、文字列解析して除去してるんですよ。MS-DOS時代からのロートルとしては非常に、その、ムズムズします。
いまどきミリ秒単位でのチューニングなんて流行らないよ!最近はフレームワークを使ってゴテゴテに盛り付けて中身をブラックボックス化して、マクロを書くくらいの気軽さでWebアプリを作るのが主流なんだよ!
そう言われてしまうとぐうの音も出ないのですが、ぼくと同じように「後で見直すことがほとんどないシンプルな箇所こそチューニングしておくべき」と考える人向けに、じゃあglob関数より早い書き方はどんな書き方なのか、というのをザっと調べていこうと思います。
テストケースを6個作ってみた
簡単ではありますが、6個のサンプルコード(関数)を用意しました。
- glob関数でファイル一覧を取得するサンプル
- glob関数でファイル一覧を取得しファイル名だけ切り出すサンプル
- scandirでファイル一覧を取得するサンプル
- scandirでファイル一覧を取得した後、ドットエントリを除去するサンプル
- opendirでファイル一覧を取得するサンプル
- opendirでドットエントリ以外のファイル一覧を取得するサンプル
//glob関数でファイル一覧を取得するサンプル
function test_glob($path)
{
$t1 = microtime(true);
$result = glob($path.'*');
echo 'glob:'.(microtime(true) - $t1).' sec.<br>';
}
//glob関数でファイル一覧を取得しファイル名だけ切り出すサンプル
function test_glob_basename($path)
{
$t1 = microtime(true);
foreach (glob($path.'*') as $file) {
$result[] = basename($file);
}
echo 'glob (basename):'.(microtime(true) - $t1).' sec.<br>';
}
//scandirでファイル一覧を取得するサンプル
function test_scandir($path)
{
$t1 = microtime(true);
$result = scandir($path);
echo 'scandir:'.(microtime(true) - $t1).' sec.<br>';
}
//scandirでファイル一覧を取得した後、ドットエントリを除去するサンプル
function test_scandir_without_dot($path)
{
$t1 = microtime(true);
$result = array();
$files = scandir($path);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
$result[] = $file;
}
}
echo 'scandir (dot対応):'.(microtime(true) - $t1).' sec.<br>';
}
//opendirでファイル一覧を取得するサンプル
function test_opendir($path)
{
$t1 = microtime(true);
$dh = opendir($path);
$result = array();
while ($file = readdir($dh)) {
$result[] = $file;
}
closedir($dh);
echo 'opendir:'.(microtime(true) - $t1).' sec.<br>';
}
//opendirでドットエントリ以外のファイル一覧を取得するサンプル
function test_opendir_without_dot($path)
{
$t1 = microtime(true);
$dh = opendir($path);
$result = array();
while ($file = readdir($dh)) {
if ($file != '.' && $file != '..') {
$result[] = $file;
}
}
closedir($dh);
echo 'opendir (dot対応):'.(microtime(true) - $t1).' sec.<br>';
}
テスト結果
test_glob | 0.18225884437561 sec. |
---|---|
test_glob_basename | 0.20673608779907 sec. |
test_scandir | 0.016497850418091 sec. |
test_scandir (dot対応) | 0.017208099365234 sec. |
test_opendir | 0.0066261291503906 sec. |
test_opendir (dot対応) | 0.0066680908203125 sec. |
opendir君、圧倒的ではないか!
globと比べたらopendirのほうがちょっとくらいは早いだろうなぁと想像していましたが、まさか27倍もの差が付くとは思いませんでした。
あまりの差なので、検証に使っているPCを再起動したり、テストする順番を変えたりして、キャッシュが効かないように配慮しましたが、おおよそ27倍の差は変わりませんでした。
実はscandirが最速なのではないかとも思っていたのですが、opendirのほうが2倍早い…。なんでだ…。これじゃscandirの存在意義がないではないか。
いやそうでもないか。scandirはソート機能などもあるため、場合によっては唯一無二の存在だろうし。結局使い分けという結論にはなりそうです。
opendirで特定の拡張子のファイル一覧を取得するサンプル
なんとなく、glob関数を使いたいケースは "*.jpg" のような拡張子を指定してファイル一覧を取得したい場合なのではないかなーと思ったので、opendirで拡張子の判定をする処理を書いてみました。
//拡張子を指定してファイル一覧を取得するサンプル
function enumDir($path, $ext = null)
{
if (!is_null($ext)) {
$ext = strtolower($ext);
}
$dh = opendir($path);
$result = array();
while ($file = readdir($dh)) {
if ($file != '.' && $file != '..') {
if (is_null($ext) || strtolower(getExt($file)) == $ext) {
$result[] = $file;
}
}
}
closedir($dh);
return $result;
}
function getExt($filename)
{
if (($pos = strrpos($filename, '.')) !== false) {
return substr($filename, $pos + 1);
} else {
return '';
}
}
拡張子を指定しない場合の速度は先述のテスト結果と変わりませんが、わざと拡張子を指定して、すべてのファイルにgetExtが働くようにした場合、処理時間はおよそ2倍ほど多くかかり、scandirでファイル一覧を取得したときと同じくらいになりました。それでもglobよりは10倍ほど早いです。
まとめ
glob関数はコードの見た目はわかりやすいけれど、速度的にはopendirよりもだいぶ遅い、ということがわかりました。
だからopendirを使うべき!!などとは言いません。ぶっちゃけ多人数での開発ではもちろんのこと、プログラマー一人で作っている小規模アプリにしても、コードを書いた1週間後には自分のコードなんて忘れているのだから、結局見やすさが一番なのは変わりないです。
なので、基本的にはglob関数を使えば良いと思うし、glob関数の質問でとっても多い「ファイル名だけ取得したい」については最初に書いたとおりbasename関数を使えば良いです。
ぼくと同じ変人だけ、opendirを使って0.2秒も早くなった…とニヨニヨしましょう。