PHP
正規表現
初心者

○○を含む?/○○で始まる?/○○で終わる?

More than 1 year has passed since last update.

○○を含む?/○○で始まる?/○○で終わる?

この手の処理に何でも正規表現を使うコードをしばしば見かけますが、正規表現を使うのはあくまで最終手段にしましょうというお話です。

  • 簡単な処理にも正規表現を多用している人
  • 正規表現の ^ や $\A や \z との違いを知らない人

などに参考にして頂ければ幸いです。


オススメの書き方

○○を含む文字列か?

function isContainsString($pHaystack, $pNeedle)
{
    return strpos($pHaystack, $pNeedle) !== FALSE;
}

使い方

//-- $strは .com を含んでいるかどうか
$str = 'https://qiita.com/fallout/';
var_dump(isContainsString($str, '.com'));    // bool(true)

○○から始まる文字列か?

function isStartsWith($pHaystack, $pNeedle)
{
    return strpos($pHaystack, $pNeedle) === 0;
}

使い方

//-- $strは http で始まっているかどうか
$str = 'https://qiita.com/fallout/';
var_dump(isStartsWith($str, 'http'));    // bool(true)

○○で終わる文字列か?

function isEndsWith($pHaystack, $pNeedle)
{
    $lenDiff = strlen($pHaystack) - strlen($pNeedle);

    return ($lenDiff < 0) ? FALSE
                          : strpos($pHaystack, $pNeedle, $lenDiff) !== FALSE;
}

使い方

//-- $strは jp/ で終わっているかどうか
$str = 'https://qiita.com/fallout/';
var_dump(isEndsWith($str, 'jp/'));    // bool(true)

正規表現を使った駄目な書き方

同じ処理を正規表現で書こうとすると、色んな罠が潜んでいます。

駄目な例1

$str = 'https://qiita.com/fallout/';

//-- $strは .com を含んでいるかどうか
if (preg_match('/.com/', $str)) {    }

一見正しいように見えますが、このコードは…

$str = 'https://qiita/com/fallout/';

…だった場合でも動いてしまいます。正規表現では . は全ての文字にマッチするというメタ文字ですので…

if (preg_match('/\.com/', $str)) {    }

…とエスケープしなければいけません。


駄目な例2

$str = 'http://qiita.com/fallout/';

//-- $strは http:// で始まっているかどうか
if (preg_match('/^http:///', $str)) {    }

これはそもそも動きません。
Warning: preg_match(): Unknown modifier '/' in ... というエラーになります。

if (preg_match('/^http\:\/\//', $str)) {    }

デリミタをエスケープするか…

if (preg_match('#^http\://#', $str)) {    }

…とデリミタを変更する必要があります。


駄目な例3

$str = "https://qiita.com/fallout/\n";

//-- $strは jp/ で終わっているかどうか
if (preg_match('/jp\/$/', $str)) {    }

今度は / をエスケープしたから大丈夫!と思われますが、ちょっと意地悪に $str の末尾に "\n" が入っている点に注目。
$str の末尾に "\n" があるため、このコードの { } 内は実行されない筈ですが、実行されてしまいます。

正しい挙動にするためには…

if (preg_match('/jp\/$/D', $str)) {    }

…と D修飾子(PCRE_DOLLAR_ENDONLY) を付けるか…

if (preg_match('/jp\/\z/', $str)) {    }

…と $ の代わりに、文字列の終端を表すエスケープシーケンス \z を使う必要があります。この件については、ウェブ上は元より書籍ですら間違った情報が散見されますので、ご注意を!

正規表現で文字列の始端 と 文字列の終端 を表す際は、^$ ではなく \A\z というエスケープシーケンスの方を使いましょう。コチラの記事も併せてご確認ください。


正規表現で同じ動きをする関数を作成するなら

このように正規表現には色んな罠?があるため…

function isContainsString($pHaystack, $pNeedle)
{
    return (bool) preg_match('/' . preg_quote($pNeedle, '/') . '/', $pHaystack);
}

function isStartsWith($pHaystack, $pNeedle)
{
    return (bool) preg_match('/\A' . preg_quote($pNeedle, '/') . '/', $pHaystack);
}

function isEndsWith($pHaystack, $pNeedle)
{
    return (bool) preg_match('/' . preg_quote($pNeedle, '/') . '\z/', $pHaystack);
}

このように、preg_quote() と 始端の \A 終端の \z とを組み合わせればOKです。

 

正規表現はコストがかかる

ここまで書いたコードの中で特に isEndsWith() については、正規表現を使った方がかなりスッキリとしたコードに見えて良い気がしますが、ここで非常に簡単なベンチマークを取ってみます。

function isEndsWithByStrpos($pHaystack, $pNeedle)
{
    $lenDiff = strlen($pHaystack) - strlen($pNeedle);
    return ($lenDiff < 0) ? FALSE
                          : strpos($pHaystack, $pNeedle, $lenDiff) !== FALSE;
}

function isEndsWithByRegex($pHaystack, $pNeedle)
{
    return (bool) preg_match('/' . preg_quote($pNeedle, '/') . '\z/', $pHaystack);
}

$str = str_repeat("\r", 100)
     . str_repeat('あ', 100)
     . str_repeat('<>', 100)
     . str_repeat('\n', 100);

$timeStart = microtime(TRUE);

for ($i = 1; $i <= 10000; $i++) {
    // isEndsWithByStrpos($str, 'あああああ');
    // isEndsWithByRegex($str, 'あああああ');
}

echo microtime(TRUE) - $timeStart;

実行環境による差はあるでしょうが、私の環境(オンボロWindowsPC上のPHP5.6.31)で確認したところ…

  • isEndsWithByStrpos() :平均約0.0049秒
  • isEndsWithByRegex() :平均約0.0650秒

…とかなりの差がつきました。

一般的に、組み込み関数に比べると正規表現はコストがかかります。

PHPマニュアルでも、特定の haystack に needle があるかどうかを調べるだけの場合、高速でメモリ消費も少ない strpos() の使用を推奨しているようです。


まとめ

PHPには標準で便利な組み込み関数がたくさん用意されています。

正規表現は大変便利なものですが、正規表現を使うのは最終手段と考え、標準関数に何か良いものはないか?とまずは探してみる事が正しい選択ではないかと思います。