PHPで拡張子等の末尾文字列を比較するのに最も速い方法を検証してみた
…と、仰々しいタイトルのワリにそんなにすごいたくさんのパターンの検証をしているわけではないのであらかじめご了承くださいw
コトの発端は拡張子がlogのファイルの抽出
ぼくは複数のサイトを運営しているので、各種サイトのApacheログを自動でダウンロードし、データベースへ格納、そしてそのログを自動解析して不正アクセスがないかチェックする、といったことをプログラムで自動的に実行しています。
ひとりでいくつものサイトを運営するには自動化できるところはトコトン自動化する必要があるためです。
ただ、それらのプログラムはもう何年も前に書いたコードで、データベースにPostgreSQLを使っているのですが、自分しか使わないデータベースにPostgreSQL使うのもオーバースペックだし、ハードウェアを変更するたびにデータベースシステムのインストールしなおしするのも面倒くさい。
じゃあもう、内部で使うデータベースなんてSQLiteで良いじゃないか、とプログラムをせっせと書き換えているのですが、ふとこんなコードが目に留まりました。
while (false !== ($entry = $d->read())) {
if (preg_match('/.log\z/', $entry)) {
//略
}
}
ディレクトリを走査し拡張子がlogのファイル一覧を取得している部分ですね。
特に問題なく動きますが、なにゆえglob関数を使っていないのか、なにゆえわざわざ正規表現を使っているのか、トンと思い出せません。昔自分が書いたコードなんて数年どころか数週間経っただけでも他人のコードと変わりないですよね…。
末尾文字列の比較速度について検証してみたくなった
そして唐突に、preg_matchやsubstr関数の速度を比較してみたくなりました。ちょっとググれば似たような記事は出てくるので、結果はだいたいわかっているのですが、前提条件(ランダムな文字列なのか、ファイル名なのか等)が微妙に異なるため、ここはいっちょ自分なりにコードを書いて試してみたい、とそういうわけです。
文字列比較の検証用のコード
const FILENAME = 'ほにゃらら.log';
const COUNT_MAX = 1000000;
echo '<table>';
test('pathinfo', function () { $result = pathinfo(FILENAME, PATHINFO_EXTENSION)=='log'; });
test('preg_match', function () { $result = preg_match('/.log$/', FILENAME); });
test('substr', function () { $result = (substr(FILENAME, -4) == '.log'); });
test('substr_compare', function () { $result = (substr_compare(FILENAME, '.log', -4) == 0); });
echo '</table>';
echo '</textarea>';
function test($testname, $function)
{
echo '<tr><td>'.$testname.'</td><td>';
$start_time = microtime(true);
for ($i = 0; $i < COUNT_MAX; $i++) {
$function();
}
echo (microtime(true) - $start_time);
echo '</td></tr>';
}
…単純です。
時間を測るためのtest関数を用意し、その関数に無名関数としてpathinfo、preg_match、substr、substr_compareを使ったパターンのコードを渡しているだけです。
テストするまでもなく、pathinfoが遅いのはわかりきっていましたが、具体的にどのくらいの差があるのか興味があったため入れてみた感じです。
実行回数は100万回。
実行結果
pathinfo | 5.1880660057068 |
preg_match | 0.54199385643005 |
substr | 0.3945140838623 |
substr_compare | 0.34936785697937 |
だいたい想像どおりではないでしょうか。
いや、想像以上にpathinfoが遅いかなw
しかしpathinfo以外は100万回も実行してこの程度の差なので、好きなの使えば?という感じもします。ただ、個人的にはどうにも正規表現が苦手なので、先述していたコードはpreg_matchからsubstr_compareへ変更したいところ。
なぜ過去の自分はlogファイルの抽出にpreg_matchを使ったのだろう…。きっとググって出てきたコードをコピペしたのだろうなぁw
もう少し汎用的にファイルの拡張子を比較する例
コトの発端となったlogファイルの抽出については拡張子固定なので良いのですが、もっと汎用的に、そう例えば以前投稿した「PHPでアップロードするファイルの拡張子をチェックするサンプル」のような例ではどうでしょうか。
あらかじめ許可する拡張子の一覧を用意しておき、それにマッチするかどうかをチェックするような動作。
pathinfoで拡張子をチェックする例
//アップロードを許可する拡張子
$cfg['ALLOW_EXTS'] = array('jpg', 'jpeg', 'png', 'log');
const FILENAME = 'ほにゃらら.log';
const COUNT_MAX = 1000000;
test('pathinfo', function () { $result = (checkExt(getExt(FILENAME))); });
//ファイル名から拡張子を取得する関数
function getExt($filename)
{
return pathinfo($filename, PATHINFO_EXTENSION);
}
//許可された拡張子か確認する関数
function checkExt($ext)
{
global $cfg;
return in_array(strtolower($ext), $cfg['ALLOW_EXTS']);
}
最後にそれぞれの実行速度を一覧にしますが、pathinfoを使った例は激遅でした。
strrposで拡張子を抽出し、in_arrayでチェックする例
//アップロードを許可する拡張子
$cfg['ALLOW_EXTS'] = array('jpg', 'jpeg', 'png', 'log');
const FILENAME = 'ほにゃらら.log';
const COUNT_MAX = 1000000;
test('strrpos', function () { $result = (checkExt(getExt(FILENAME))); });
//ファイル名から拡張子を取得する関数
function getExt($filename)
{
if (($pos = strrpos($filename, '.')) !== false) {
return substr($filename, $pos + 1);
} else {
return '';
}
}
//許可された拡張子か確認する関数
function checkExt($ext)
{
global $cfg;
return in_array(strtolower($ext), $cfg['ALLOW_EXTS']);
}
pathinfoの代わりにstrrpos(末尾から指定した文字を検索して見つかった位置を返す)を使った例ですが、pathinfoに比べれば相当早くなります。残り2つの例でもこの部分は変えずに使います。
strrposで拡張子を抽出し、array_key_existsでチェックする例
//アップロードを許可する拡張子
$cfg['ALLOW_EXTS'] = array('jpg'=>1, 'jpeg'=>1, 'png'=>1, 'log'=>1);
const FILENAME = 'ほにゃらら.log';
const COUNT_MAX = 1000000;
test('array_key_exists', function () { $result = (checkExt(getExt(FILENAME))); });
//ファイル名から拡張子を取得する関数
function getExt($filename)
{
if (($pos = strrpos($filename, '.')) !== false) {
return substr($filename, $pos + 1);
} else {
return '';
}
}
//許可された拡張子か確認する関数
function checkExt($ext)
{
global $cfg;
return array_key_exists(strtolower($ext), $cfg['ALLOW_EXTS']);
}
拡張子を抽出する部分(getExt関数)は変更せず、拡張子のチェック部分(checkExt関数)をin_arrayからarray_key_existsへ変更してみました。
それに伴い、$cfg['ALLOW_EXTS']も通常の配列から連想配列へ変えてあります。連想配列のほうがインデックスサーチされて速いんじゃないかなぁーと思って試したわけですが、そのとおりでした。
strrposで拡張子を抽出し、issetでチェックする例
//アップロードを許可する拡張子
$cfg['ALLOW_EXTS'] = array('jpg'=>1, 'jpeg'=>1, 'png'=>1, 'log'=>1);
const FILENAME = 'ほにゃらら.log';
const COUNT_MAX = 1000000;
test('isset', function () { $result = (checkExt(getExt(FILENAME))); });
//ファイル名から拡張子を取得する関数
function getExt($filename)
{
if (($pos = strrpos($filename, '.')) !== false) {
return substr($filename, $pos + 1);
} else {
return '';
}
}
//許可された拡張子か確認する関数
function checkExt($ext)
{
global $cfg;
return isset($cfg['ALLOW_EXTS'][strtolower($ext)]);
}
array_key_existsよりissetのほうが速そうだよなぁーという理由でなんとなく試した例。これも想像どおり早くなりました。
ファイルの拡張子をチェックするコードの速度比較
結果は次のとおり。
pathinfo | 6.4745759963989 |
strrpos | 1.8200578689575 |
array_key_exists | 1.7440540790558 |
isset | 1.6984531879425 |
チェックする拡張子の数や、大文字小文字を区別するかしないか(今回の例では区別しない設定)でも結果は変わってくるでしょうし、strrposとissetを使う組み合わせよりもっともっと速い方法はありそうな気がします。
実際、拡張子が「末尾3文字固定」という条件付きであればstrrposでピリオドを検索するよりも、substr_compare関数で3文字決め打ちで比較したほうが速かったです。
…が、とりあえず、自分の知識欲は満たせたので今回の検証はここまで。
まとめ
- logファイルを抽出するプログラムでpreg_matchを使っていたため、もっと速い方法がないか調べてみた。
- (当たり前だけれど)preg_matchよりsubstr_compareのほうが速かった。
- ついでに拡張子をチェックする例について速度比較してみた。
- (当たり前だけれど)pathinfoよりstrrposのほうが速かった。
- 更にin_array/array_key_exists/issetの速度を比較してみたところissetが速かった。
ということで、「PHPでアップロードするファイルの拡張子をチェックするサンプル」にはstrrposを使った例を追記しておこうと思います。