【PHP】○○を含む？/○○で始まる？/○○で終わる？

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

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

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

--------------------------------------------------------------------------------

# オススメの書き方

## ○○を含む文字列か？

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

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

--------------------------------------------------------------------------------

## ○○から始まる文字列か？

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

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

--------------------------------------------------------------------------------

## ○○で終わる文字列か？

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

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

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

--------------------------------------------------------------------------------

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

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

## 駄目な例1

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

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

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

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

…だった場合でも動いてしまいます。正規表現では `.` は全ての文字にマッチするという[メタ文字](http://php.net/manual/ja/regexp.reference.meta.php)ですので…

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

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

--------------------------------------------------------------------------------

## 駄目な例2

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

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

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

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

と[デリミタ](http://php.net/manual/ja/regexp.reference.delimiters.php)をエスケープするか…

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

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

--------------------------------------------------------------------------------

## 駄目な例3

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

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

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

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

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

…と [D修飾子(PCRE_DOLLAR_ENDONLY)](http://php.net/manual/ja/reference.pcre.pattern.modifiers.php) を付けるか…

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

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

**正規表現で文字列の始端 と 文字列の終端 を表す際は、`^` と `$` ではなく `\A` と `\z` という[エスケープシーケンス](http://php.net/manual/ja/regexp.reference.escape.php)の方を使いましょう。**[コチラの記事](https://qiita.com/fallout/items/a13cebb07015d421fde3#%E8%A7%A3%E8%AA%AC)も併せてご確認ください。

--------------------------------------------------------------------------------

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

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

```php
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()](http://php.net/manual/ja/function.preg-quote.php) と 始端の `\A` 終端の `\z` とを組み合わせればOKです。
　
--------------------------------------------------------------------------------

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

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

```php
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() の使用を推奨](http://php.net/manual/ja/function.strstr.php)しているようです。

--------------------------------------------------------------------------------

# まとめ

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

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