【PHP】マルチバイト(全角スペース等)対応のtrim処理

ワリと需要のある処理だと思いますが、改めてググってみるとあまりマネして欲しくないコードが散見されたため、この記事を書いてみました。

- [検索結果上位のページ](https://www.google.co.jp/search?q=php+%E5%85%A8%E8%A7%92%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9+trim&ie=UTF-8)のコードをコピペで使ってる人

- 正規表現の `\A と \z` や `++` や `\p{C}` という書き方を知らない人

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

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

## コード

```php
function mbTrim($pString)
{
    return preg_replace('/\A[\p{Cc}\p{Cf}\p{Z}]++|[\p{Cc}\p{Cf}\p{Z}]++\z/u', '', $pString);
}
```

### 解説

- `\A` と `\z`
    - **文字列の始端** と **文字列の終端** を表す[エスケープシーケンス](http://php.net/manual/ja/regexp.reference.escape.php)です。
    - 始端と終端を `^` と `$` で記述しているものが多いですが、`\A` と `\z` を使うようにしましょう。[^1]
        - [正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう(徳丸氏)](https://blog.tokumaru.org/2014/03/z.html)
        - [Railsの正規表現でよく使われる \A \z って何？？(Qiita)](https://qiita.com/jnchito/items/ea7832df6f64a9034872)

- 最後の `/u`
    - UTF-8 として処理するよという[パターン修飾子](http://php.net/manual/ja/reference.pcre.pattern.modifiers.php)です。

- `\p{C}` と `\p{Z}`
    - **コントロール文字その他諸々** と **区切り文字や空白文字** を表す[Unicode 文字プロパティ](http://php.net/manual/ja/regexp.reference.unicode.php)です。

- `++`
    - **絶対最大量指定子** と呼ばれる(他にも様々な呼び方があります)もので、`+` に比べバックトラックを抑える働きがあります。

[^1]: ruby を使ってる人にはお馴染みでしょうが、PHP ではまだまだ浸透してないような気がします。

要するに、**文字列の前後から、空白文字や改行の他 通常は不要と思われる制御文字なんかもまとめて全部削除しちゃえ！**という意味になります。

スペース 1 つとっても、[半角・全角以外にも色々とあります(Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9)。それらを 1 つ 1 つ調べ上げるのは面倒なので、Unicode 文字プロパティを使って楽をしているという事です。

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

### 別のコード1

もう少しマイルドに、PHP 本来の [trim()](http://php.net/manual/ja/function.trim.php) に全角スペースの処理を加えた程度のもので良いのであれば、以下のようなコードがお手軽だと思います。

```php
$str = preg_replace('/\A[\x00\s]++|[\x00\s]++\z/u', '', $str);
```

もしくは

```php
mb_regex_encoding('UTF-8');
$str = mb_ereg_replace('\A[\x00\s]++|[\x00\s]++\z', '', $str);
```

- `'\x00'`
    - NULLバイト（`"\0"`）

- `\s`
    - **空白文字** を表す[エスケープシーケンス](http://php.net/manual/ja/regexp.reference.escape.php)です。
    - 上記の書き方であれば、`\s` には全角スペースも含まれていますが、NULLバイトは含まれていない点に注意です。

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

### 別のコード2

以下のように、取り除きたい文字を丁寧に指定するのも、意図を汲み取りやすい書き方だと思います。

```php
$str = preg_replace("/\A[\\x0-\x20\x7F\xC2\xA0\xE3\x80\x80]++|[\\x0-\x20\x7F\xC2\xA0\xE3\x80\x80]++\z/u", '', $str);
```

- `"\\x0-\x20\x7F"`
    - 制御文字(`\\x0-\x1F` と `\x7F`) ＋ 半角スペース(`\x20`)
    - [ASCIIコード表](https://www.google.co.jp/search?q=ASCII%E3%82%B3%E3%83%BC%E3%83%89%E8%A1%A8&ie=UTF-8)

- `"\xC2\xA0"`
    - NO-BREAK SPACE(U+00A0)

- `"\xE3\x80\x80"`
    - 全角スペース(U+3000)

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

## あまりマネして欲しくないコード

ググった際に見つけた、あまりマネして欲しくないコードを解説してみます。

### その1

```php
$str = trim(mb_convert_kana($str, 's'));
```

#### 解説

悪い意味で PHP らしいコードというか…パッと見で「何か気持ち悪い」と感じてしまうのは私だけでしょうか？

```php
$str = '　 あ　い　う　え　お 　';
$str = trim(mb_convert_kana($str, 's'));
```

を実行してみると分かると思いますが、このコードでは**文字列の途中にある全角スペース**まで半角スペースに置換されてしまいます。

手っ取り早い方法ではありますが、**このような考え方はバグの原因になります**ので、避けた方が良いと思います。


### その2

```php
$str = preg_replace('/^[ 　]+/u', '', $str);
$str = preg_replace('/[ 　]+$/u', '', $str);
```

…は流石にアレなので、選択肢を意味する[メタ文字](http://php.net/manual/ja/regexp.reference.meta.php)を使って…

```php
$str = preg_replace('/^[ 　]+|[ 　]+$/u', '', $str);
```

…と、とりあえず1行にまとめてみます。

#### 解説

[trim()](http://php.net/manual/ja/function.trim.php) ではデフォルトでスペース以外の文字 `"\t\n\r\0\x0B"` の除去も行っていますが、このコードではそれらが考慮されていません。

また、文字クラス `[  ]` の中に全角スペースを直接書くというのもあまり感心はしないです。

SPACE(U+0020 `"\x20"`)・NO-BREAK SPACE(U+00A0 `"\xC2\xA0"`)・全角スペース(U+3000 `"\xE3\x80\x80"`) 等の空白文字は、環境によっては見た目の区別がほとんど付かなくなりますので、この手の文字列処理は、別の方法で書く癖を付けておいた方が良いのではないかと思います。

- 例：`'[ 　]'` ではなく `"[\x20\xE3\x80\x80]"` と書く。

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

## 絶対最大量指定子 ++ を使っている理由

ReDoS という言葉はご存知でしょうか？

**R**egular **E**xpressions **D**enial **o**f **S**ervice attack の略で、簡単に言うと、正規表現の処理を悪用したアタックの事です。

有名なのが、Stack Overflow で発生したサービスダウンです。

- [Stack Exchange Network Status — Outage Postmortem \- July 20, 2016](https://stackstatus.net/post/147710624694/outage-postmortem-july-20-2016)

上記ページ内にある通り、文字列の前後から空白文字を取り除くために `^[\s\u200c]+|[\s\u200c]+$` という正規表現を使っていたのが原因です。`++` ではなく `+` を使っています。


この正規表現に対し、2 万文字もの空白を含む文字列が送られた結果、約 2 億回（20,000 + 19,999 + 19,998 + ...）近いバックトラックが発生した事が、サービスのダウンへと繋がりました。[^2]

[^2]: PHP では、`preg_match()` 系で使われている PCRE ライブラリと `mb_ereg()`  系で使われている Oniguruma ライブラリとが採用されていますが、最近のバージョンであれば、自動でバックトラック回数制限がかかるようになっているとは思います。（未検証）


なお、この件に関しては、正規表現に通す前に文字列の長さをチェックしていないのも問題だと思います。

正規表現は大変便利なものですが、**正規表現を使うのは最終手段**と考え、まずは「正規表現に通す前にできる事はないか？」「正規表現を使わずに標準関数で処理できないか？」と検討してみる事が大事だと思います。