PHPの正規表現

2015-06-22      GMOリサーチ 寺田渉

facebook: 寺田渉
github:   waterada
twitter:  @wa_terada

こう書くといいよ

自己紹介 (会社)

- PHP (CakePHP) / java (Spring MVC) を主に使って開発

- 継続的インテグレーション

github + git flow で運用

- PHPUnit / JUnit で カバレッジ 100%

- Behat (Selenium Driver 経由の画面テスト) 利用

- vagrant で開発環境構築

自己紹介 (趣味)

CakePHP 公式ドキュメント 翻訳

自己紹介 (趣味)

ボードゲーム 翻訳

自己紹介 (趣味)

TED 翻訳

自己紹介

プログラミング & 翻訳

大好き人間です

「正規表現」

知ってます?

で、本題。

こういうやつ

if (preg_match('/[^0-9]/', $str)) {
    //正の整数じゃないよエラー
}

ある文字列が定義したパターン(=正規表現)に

合致するかどうかを簡単にチェックできます。
置き換えもできます。

知らないなら すぐに

調べた方がいいです

絶対効率よい

でも使いすぎはダメ。確認しづらい

上司: メアドチェックするロジック作っといて!

新人: 出来ました!

たとえば・・・

合ってるかどうかじゃありません。

メンテしていけるかどうかです。

if(preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:
\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2
F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C
[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(
?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[
^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--
)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?
:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1
,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})
(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:
25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1
[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $email)) {

メアドチェックする正規表現:

これ来たら、つらいですよね?

前置きはこのくらいにして、

を紹介していきます。

こう書くと いいよ ダメだよ

デリミタ変えれるよ

$html = preg_replace('/^(\/[^\/]+)+\//', '', $html);

/aa/bb/cc/dd → dd

$html = preg_replace('{^(/[^/]+)+/}', '', $html);
$html = preg_replace('#^(/[^/]+)+/#', '', $html);

$終端という意味じゃない  【\z】

if (! preg_match('/^[0-9]+$/', $str)) {
    //数字以外ならエラー
$str = "123\n" でもOKとなってしまう!
($終端 or 終端の改行という意味なので)
if (! preg_match('/^[0-9]+\z/', $str)) {
    //数字以外ならエラー

2015-06-24 イカID:yuyat.jpさんの指摘がとても良かったのでこのページにて共有させて頂きました。

$str = "123\n" でも意図通りエラーとなる!

(\z終端という意味なので)

非貪欲マッチ    【?】

$html = preg_replace('#<b>.*</b>#', '', $html);
AA<b>BB</b>CC<b>DD</b>EE → AAEE
$html = preg_replace('#<b>.*?</b>#', '', $html);
AA<b>BB</b>CC<b>DD</b>EE → AACCEE
――――――
―――
―――

2015-06-24 @hnwさんの指摘で「最小」→「非貪欲」に直させて頂きました。

. は基本改行含めない    【s】

if (preg_match('/<img.*?>/', $str)) {
"<img 
>"     → 合致しない
if (preg_match('/<img.*?>/s', $str)) {
"<img 
>"     → 合致する

マルチライン    【m】

$str = preg_replace('/^|(\n)/', '$1- ', $str);
結果:
$str = preg_replace('/^/m', '- ', $str);
- aa
- bb
- 
結果:
- aa
- bb 
各行の先頭に「- 」を

後方参照がたくさんあるなら

コメントあると解りやすい

//             1     2     3     4     5     6
$pattern = '/^(.*?),(.*?),(.*?),(\d+)/(\d+)/(\d+)$/';
$replace = '$6-$5-$4 $2:$3:$1';
$str = preg_replace($pattern, $replace, $str);
\Q\E で見やすく
$str = preg_replace('/\(\*\^-\^\*\)/', '', $str);
$str = preg_replace('/\Q(*^-^*)\E/', '', $str);
\Q\Eユーザ入力に使っちゃダメ
// $name = "xxxx-abcd";
if (preg_match('/\Q'.$input.'\E/', $name)) {
    // $input に '\E' が入っていると困る
if (preg_match('/'.preg_quote($input,'/').'/', $name)) {
x 使って わかりやすく書く
if (preg_match("/^-?(?:[1-9]\d*|0)(?:\.\d+[1-9])?$/", 
$num)) {
if (preg_match("/
    ^       # 先頭
    -?      # マイナスは有っても無くても良い
    (?:
        [1-9]   # 整数部一桁目は0禁止
        \d*     # 整数部
    |
        0       # 0 のみ整数部の1桁目が 0 でも良い
    )
    (?:
        \.      # 小数点
        \d*     # 小数部
        [1-9]   # 小数部の末尾は 0 禁止
    )?          # 小数部は無くてもいい
    $       # 末尾
/x", $num)) {
言明は使わないほうが身のため
言明とは下記のようなやつ。
(?=  (?!  (?<=  (?<!
マッチするけど結果に含めないとかいうやつです。
メンバー全員がテストケース洗い出せるくらいでないと
バグの温床になります!
(?: )( ) とほぼ同じ
//                                     1     2     3
$pattern = '/^(?:.*?),(?:.*?),(?:.*?),(\w+)/(\w+)/(\w+)$/';
$replace = '$3-$2-$1';
$str = preg_replace($pattern, $replace, $str);
(?: ) と ( ) の違いは
$1 として 後で参照するか

読みやすくなると思ったら使おう

応用編

何が問題?
# <script>~</script>を撤去して出力
# $str = "[<script>alert(1);</script>]";
# なら
# []
# が出力される。
echo preg_replace("#<script>.*?</script>#", "", $str);
この正規表現には抜け穴があり、下記を出力できる。
[<script>alert(1);</script>]

$str  がどんな文字のときにそうなる?

答え
$str = "[<scr<script></script>ipt>alert(1);</script>]";
echo preg_replace("#<script>.*?</script>#", "", $str);
このように置き換え後の文字は生き残るのだ。
[<script>alert(1);</script>]
これで下記が出力される:

以上です!

感想などあれば:

facebook: 寺田渉
github:   waterada
twitter:  @wa_terada

ご静聴ありがとうございました!