PHPの正規表現 (regular expression)

PHPの正規表現には、

の3種類がありますが、ここではPerl互換のpregについて解説します。

PCRE関数
  関数 説明
  preg_match 正規表現によるマッチングを行う
preg_match_all 繰り返し正規表現検索を行う
preg_grep パターンにマッチする配列の要素を返す
  preg_replace 正規表現検索および置換を行う
preg_replace_callback 正規表現検索を行い、コールバック関数を使用して置換を行う
preg_filter 正規表現による検索と置換を行う
  preg_split 正規表現で文字列を分割する
  preg_quote 正規表現文字をクォートする
  preg_last_error 直近の PCRE 正規表現処理のエラーコードを返す
PHP: PCRE 関数 - Manual

PHPの正規表現

パターン修飾子 (モード修飾子)

正規表現のマッチモードを指定します。

修飾子 モード 説明
i   大文字と小文字の違いを無視する
s ドット全マッチ モード  
m 拡張行アンカーマッチ モード メタ文字の^$が、行頭と行末にマッチするようにする
x フリーフォーマット モード 文字クラス以外で、エスケープされていない空白文字を無視する
u   対象文字列を、UTF-8として扱う
e   置換文字列を、PHPコードとして実行
X   PCREの独自機能を有効
S   PCREのstudy最適化を実行
PHP: 正規表現パターンに使用可能な修飾子 - Manual

修飾子はPerlなどと同様で、

/[a-z]/i

のように、区切り文字の後に記述します。

g修飾子はサポートされないため、パターンマッチではpreg_match_allを使用します。一方で置換では、既定でg修飾子を指定した場合と同じ動作をします。

文字クラスの略記法

\d、\w、\sなどの文字クラスの略記法が使用できます。PHPではこれを、文字型 (character types) と呼称しています。

Unicode (UTF-8モード)

マルチバイト文字にはmb_ereg()を使用することになりますが、対象がUTF-8ならばpregでパターン修飾子 (/u) を指定することでも対応できます。

Unicodeプロパティ

漢字のみで構成される文字列にマッチさせるには、

preg_match( '/\p{Han}+/u', $text, $matches );

のようにします。

Unicodeプロパティ

PHP: Unicode 文字プロパティ - Manual

パターンマッチ (preg_match)

int preg_match (
    string $pattern,   // 検索するパターンを表す文字列
    string $subject,   // パターンマッチの対象となる文字列
    [, array &$matches // マッチした文字列の配列
    [, int $flags = 0
    [, int $offset = 0 // 検索の開始位置 (バイト単位)
    ]]] )
PHP: preg_match - Manual

文字列に日本語を含むならば、符号化方式をUTF-8としてUTF-8モードで処理すべきです。

マッチに成功した場合は1を、さもなくば0が返されます。エラーが発生した場合にはFALSEが返されますので、マッチの失敗の0とは区別する必要があります。$patternPerlなどと同様に、区切り文字 (デリミタ) で囲む必要があります。

たとえば、

preg_match( '/([A-Z]+).*?([0-9]+)/', 'fooBar2000', $matches );

とすると、$matchesには次のように結果が格納されます。

Array
(
    [0] => Bar2000 パターン全体のマッチ部分
    [1] => B       1つ目のキャプチャのマッチ部分
    [2] => 2000    2つ目のキャプチャのマッチ部分
)

マッチ位置の取得

preg_match()のflagsに、PREG_OFFSET_CAPTUREを指定します。

preg_match(
    '/([A-Z]+).*?([0-9]+)/',
    'fooBar2000',
    $matches,
    PREG_OFFSET_CAPTURE
    );

結果は次のように返されます。

Array
(
    [0] => Array
        (
            [0] => Bar2000
            [1] => 3
        )
    [1] => Array 1つ目のキャプチャのマッチ部分
        (
            [0] => B マッチした文字列
            [1] => 3 対象文字列中における、マッチした文字列のオフセット値
        )
    [2] => Array 2つ目のキャプチャのマッチ部分
        (
            [0] => 2000
            [1] => 6
        )
)

マッチするすべての文字列の取り出し (preg_match_all)

int preg_match_all (
    string $pattern,
    string $subject,
    [, array &$matches
    [, int $flags = PREG_PATTERN_ORDER
    [, int $offset = 0
    ]]] )
PHP: preg_match_all - Manual

マッチした回数が返され、マッチしなかった場合には0となります。また、エラーが発生した場合はFALSEが返されます。

preg_match_allで返される第3引数の$matchesは、

$matches[ 0 ][ n ] n回目のパターンマッチで、パターン全体にマッチした文字列
$matches[ 1 ][ n ] n回目のパターンマッチにおける、Perlの$1と同様の内容
$matches[ 2 ][ n ] n回目のパターンマッチにおける、Perlの$2と同様の内容

のような2次元の配列となります。このとき $matches[ 1 ]は、マッチしたすべての$1の配列になります。

n回目にパターンマッチした内容ごとに処理したい場合には、preg_match_allの$flags引数にPREG_SET_ORDERを指定します。このとき$matches[ 1 ]は、1回目のパターンマッチでマッチした$1や$2などのすべての要素の配列になります。たとえば、

preg_match_all(
    '/([0-9])([A-Z])/',
    '1A 00 2B',
    $matches );

とすると、$matchesには次のように格納されます。

Array
(
    [0] => Array パターン全体のマッチ部分
        (
            [0] => 1A
            [1] => 2B
        )
    [1] => Array 1つ目のキャプチャのマッチ部分
        (
            [0] => 1 1回目のマッチ部分
            [1] => 2 2回目のマッチ部分
        )
    [2] => Array 2つ目のキャプチャのマッチ部分
        (
            [0] => A
            [1] => B
        )
)

一方でPREG_SET_ORDERを指定して、

preg_match_all(
    '/([0-9])([A-Z])/',
    '1A 00 2B',
    $matches,
    PREG_SET_ORDER );

とすると、$matchesは次のようになります。

Array
(
    [0] => Array 1回目のマッチ部分
        (
            [0] => 1A パターン全体のマッチ部分
            [1] => 1  1つ目のキャプチャのマッチ部分
            [2] => A  2つ目のキャプチャのマッチ部分
        )
    [1] => Array 2回目のマッチ部分
        (
            [0] => 2B
            [1] => 2
            [2] => B
        )
)

またPREG_SET_ORDERを指定しない状態で、

preg_match_all( '/(AA)(BB)/', 'foo', $matches );

のように2つのかっこがある正規表現で、1つもマッチしないと次のようになります。

Array
(
    [0] => Array パターン全体のマッチ部分
        (
        )
    [1] => Array 1つ目のキャプチャのマッチ部分
        (
        )
    [2] => Array 2つ目のキャプチャのマッチ部分
        (
        )
)

このときpreg_match_all()は0を返します。

preg_matchによるすべての文字列の取り出し

preg_match()でもマッチ位置の取得を行うことで、マッチするすべての文字列を取得できます。preg_match_all()でも同様のことができますが、こちらの方法の方がより柔軟に処理できます。

$result = array();

$offset = 0;
while( preg_match( $pattern, $subject, $matches, PREG_OFFSET_CAPTURE, $offset ) )
{
    $match = $matches[ 0 ];

    $result[] = $match[ 0 ];
    $offset = $match[ 1 ] + strlen( $match[ 0 ] );
}

置換 (preg_replace)

mixed preg_replace (
    mixed $pattern,      // 検索するパターンを表す文字列または配列
    mixed $replacement,  // 置き換える文字列または文字列の配列
    mixed $subject       // 置換対象の文字列または文字列の配列
    [, int $limit = -1   // 置換を行う最大回数 (-1は無制限)
    [, int &$count ]]    // 置換が行われた回数を返す
    )
PHP: preg_replace - Manual ※ [ ]で囲まれた項目は省略可能です。

文字列に日本語を含むならば、符号化方式をUTF-8としてUTF-8モードで処理すべきです。

既定で、マッチするすべての文字が置換されます。1つだけ置換したいならば、引数の$limitに1を指定します。

マッチの結果によって、次のように戻り値が異なります。

  • マッチした場合 … 置換された新しい文字列または配列
  • マッチしなかった場合 … 置換対象の文字列または配列
  • エラーが発生 … NULL

簡単な置換ならば文字列の関数 (str_replace) でも処理できます。

置換の例

$subject = 'abc 123 45';
$str = preg_replace( '/[0-9]/', 'B', $subject );

echo $str; // abc BBB BB
後方参照 (Back references)
$subject = 'abc 123 45';
$str = preg_replace( '/([0-9])/', '($1)', $subject );

echo $str; // abc (1)(2)(3) (4)(5)
置換対象が配列
$subject = array( 'abc', 123, 45 );
$str = preg_replace( '/[0-9]/', 'B', $subject );

print_r( $str );
// Array
// (
//     [0] => abc
//     [1] => BBB
//     [2] => BB
// )
パターンが配列
$patterns = array( '/[a-z]/', '/[0-9]/' );
$replacements = array( 'A', 'B' );

$subject = 'abc 123 456';
$str = preg_replace( $patterns, $replacements, $subject );

echo $str; // AAA BBB BB

独自の置換処理 (preg_replace_callback)

複雑は置換処理はpreg_replace_callback()によって実現できます。この関数はマッチした文字に対して、独自に定義した関数で置換文字列を決定します。

mixed preg_replace_callback (
    mixed $pattern ,
    callable $callback ,
    mixed $subject
    [, int $limit = -1
    [, int &$count ]]
    )
PHP: preg_replace_callback - Manual

記述上の注意

区切り文字 (delimiter)

pregの正規表現エンジンでは、正規表現を区切り文字で囲まなければなりません。一般的にはスラッシュ (/) が使用されますが、実際には英数字とバックスラッシュ以外ならば何でも構いません。たとえば、

/[0-9]/

これは、次のように記述しても同じです。

<[0-9]>

正規表現のなかで区切り文字と同一文字を使用するにはエスケープしなければなりませんが、区切り文字を変更するとそれが不要となるため、この手法が役に立ちます。

バックスラッシュ (\) へのマッチ

バックスラッシュにマッチさせるには、これが正規表現ではエスケープの記号として使用されていることから、これ自体をエスケープして「\\」と記述する必要があります。またPHPの文字列リテラルでもバックスラッシュがエスケープに使用されるため、同様に「\\」とする必要があります。

よってPHPの文字列リテラルで記述された正規表現でバックスラッシュにマッチさせるには、「\\\\」とします。

PHPの終了タグ (?>)

正規表現のパターン中に「?>」と記述した場合、エディタによってはPHPスクリプトの終了タグとみなされる場合があります。その場合にはASCIIコードで記述することで、この問題を回避できます。

参考

参考書