サポンテ 勉強ノート

サポンテの勉強ノート・読書メモなどを晒します。

PHP の正規表現(PCRE)に入門してみる

はじめに

試験勉強です。

今回は見事に全部スニペットになってしまいました。

PHP のマニュアルに掲載されているサンプルはコメント部分が翻訳されていなかったり「日本語を処理する場合はどうなるのだろう」という細かいことがわからなかったりします。

いずれにしても、手を動かして細かい挙動を探ってみないといけないのは、どの言語でも同じです。

今回はテキスト処理なので例文を考えてみるのも少し楽しかったです。

サンプルプログラム

<?php
header('Content-type: text/html; charset=utf-8');

$subject1 = '犬が2頭歩いていく。その先に別の犬が1頭座っている。';
$subject2 = '人が2人歩いていく。その先に別の人が1人座っている。';
$subject3 = '猫が2匹歩いていく。その先に別の猫が1匹立っている。';
$subject = array($subject1, $subject2, $subject3);
$pattern = array('/犬/u', '/頭/u');
$replace = array('猫', '匹');

// -----
// preg_filter() のテスト
// -----
print_r(preg_filter($pattern, $replace, $subject));
/*
Array
(
    [0] => 猫が2匹歩いていく。その先に別の猫が1匹座っている。
)
※ preg_filter() は、マッチしない場合に何も返さない。
 */
var_dump(preg_filter($pattern, $replace, $subject2));
/*
null
※ preg_filter() は、マッチしない場合に何も返さない。$subject が文字列なら NULL。
 */
// -----
// preg_replace() のテスト
// -----
print_r(preg_replace($pattern, $replace, $subject));
/*
Array
(
    [0] => 猫が2匹歩いていく。その先に別の猫が1匹座っている。
    [1] => 人が2人歩いていく。その先に別の人が1人座っている。
    [2] => 猫が2匹歩いていく。その先に別の猫が1匹立っている。
)
※ preg_replace() は、マッチしない場合に何も置き換わっていない文字列を返す。
 */
// -----
// preg_grep() のテスト
// -----
print_r(preg_grep('/座っている/u', $subject));
/*
Array
(
    [0] => 犬が2頭歩いていく。その先に別の犬が1頭座っている。
    [1] => 人が2人歩いていく。その先に別の人が1人座っている。
)
 */
// -----
// preg_last_error() のテスト
// (マニュアルのサンプルそのまま)
// -----
if (preg_last_error() !== PREG_NO_ERROR) {
    echo preg_last_error();
}
preg_match('/(?:\D+|<\d+>)*[!?]/u', 'foobar foobar foobar');

if (preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR) {
    print 'バックトラック制限を使い果たしました。';
}
/*
バックトラック制限を使い果たしました。
 */
// -----
// preg_match_all() のテスト
// -----
$pattern2 = '/(?<creature>(?<!\d)人|犬|猫)+.*?(?<unit>(?<=\d)人|頭|匹)+/u';
$out = null;
$count = preg_match_all($pattern2
        , $subject1
        , $out
        , PREG_PATTERN_ORDER);
print_r($out);
/*
Array
(
    [0] => Array
        (
            [0] => 犬が2頭
            [1] => 犬が1頭
        )

    [creature] => Array
        (
            [0] => 犬
            [1] => 犬
        )

    [1] => Array
        (
            [0] => 犬
            [1] => 犬
        )

    [unit] => Array
        (
            [0] => 頭
            [1] => 頭
        )

    [2] => Array
        (
            [0] => 頭
            [1] => 頭
        )

)
 */
$count = preg_match_all($pattern2
        , $subject1
        , $out
        , PREG_SET_ORDER);
print_r($out);
/*
Array
(
    [0] => Array
        (
            [0] => 犬が2頭
            [creature] => 犬
            [1] => 犬
            [unit] => 頭
            [2] => 頭
        )

    [1] => Array
        (
            [0] => 犬が1頭
            [creature] => 犬
            [1] => 犬
            [unit] => 頭
            [2] => 頭
        )

)
 */
// -----
// preg_match() のテスト
// -----
$offset = 0;
while (preg_match($pattern2
        , $subject1
        , $out
        , PREG_OFFSET_CAPTURE
        , $offset)) {
    $match = $out[0];
    
    print_r($match);
    $offset = $match[1] + strlen($match[0]);
}
/*
Array
(
    [0] => 犬が2頭
    [1] => 0
)
Array
(
    [0] => 犬が1頭
    [1] => 46
)
 */
// -----
// preg_quote() 関数はマニュアルの使用例がとてもわかりやすいので割愛
// -----

// -----
// preg_replace_callback() のテスト
// -----
$replaced = preg_replace_callback(
        '/\d+/'
        , function ($match) {
            return mb_convert_kana($match[0], 'N', 'utf-8');
        }
        , $subject2
    );
echo $replaced . "\n";
/*
人が2人歩いていく。その先に別の人が1人座っている。
 */

// -----
// preg_replace_callback_array() のテスト
// =< PHP7
// -----
$replaced = preg_replace_callback_array(
        array (
            '/(?<!\d)+人/u' => function ($match) {
                return 'お客様';
            },
            '/(?<=\d)+人/u' => function ($match) {
                return '名';
            },
            '/\d+/u' => function ($match) {
                return mb_convert_kana($match[0], 'N', 'utf-8');
            }
            )
        , $subject2
        );
echo $replaced . "\n";
/*
お客様が2名歩いていく。その先に別のお客様が1名座っている。
 */
// -----
// preg_split() のテスト
// -----
$subject4 = "かつて,横書きの場合は、句読点をカンマとピリオドにすべしという"
        . "指導がなされた時期があった。その後,「てん」だけをカンマにすべしとさ"
        . "れ,続いて縦書きと同じように「てん」と「まる」でもよいことになった."
        . "二転三転した指導により世は乱れ、その混乱は今日まで続いている。世に言"
        . "う、句読点暗黒時代である.";
$lines = preg_split('/[' . preg_quote('..。') . ']/u', $subject4, -1, PREG_SPLIT_NO_EMPTY);
foreach ($lines as $line) {
    echo preg_replace('/[,,、]/u', '、', $line) . "。\n";
}
/*
かつて、横書きの場合は、句読点をカンマとピリオドにすべしという指導がなされた時期があった。
その後、「てん」だけをカンマにすべしとされ、続いて縦書きと同じように「てん」と「まる」でもよいことになった。
二転三転した指導により世は乱れ、その混乱は今日まで続いている。
世に言う、句読点暗黒時代である。
 */
$lines = preg_split('/([' . preg_quote('..。') . '])/u'
        , $subject4
        , -1
        , PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
var_dump($lines);
/*
array (size=8)
  0 => string 'かつて,横書きの場合は、句読点をカンマとピリオドにすべしという指導がなされた時期があった' (length=130)
  1 => string '。' (length=3)
  2 => string 'その後,「てん」だけをカンマにすべしとされ,続いて縦書きと同じように「てん」と「まる」でもよいことになった' (length=159)
  3 => string '.' (length=3)
  4 => string '二転三転した指導により世は乱れ、その混乱は今日まで続いている' (length=84)
  5 => string '。' (length=3)
  6 => string '世に言う、句読点暗黒時代である' (length=45)
  7 => string '.' (length=1)
 */

// -----
// preg_match() の誤用のテスト
// -----
$offset = 0;
while (preg_match('/もも/u'
        , 'もももももも'
        , $out
        , PREG_OFFSET_CAPTURE
        , $offset)) {
    $match = $out[0];
    
    print_r($match);
    $offset = $match[1] + mb_strlen($match[0]); // ※
    // ※ ここを strlen() ではなく mb_strlen() にすると期待通り動かないという
    //    サンプル。
}
/*
Array
(
    [0] => もも
    [1] => 0
)
Array
(
    [0] => もも
    [1] => 3
)
Array
(
    [0] => もも
    [1] => 6
)
Array
(
    [0] => もも
    [1] => 9
)
Array
(
    [0] => もも
    [1] => 12
)
 */
echo "<br />\n";
$offset = 0;
while (preg_match('/^.{2}/u'
        , 'もももももも'
        , $out
        , PREG_OFFSET_CAPTURE
        , $offset)) {
    $match = $out[0];
    
    print_r($match);
    $offset = $match[1] + strlen($match[0]);
    // ^ は文字列の先頭だけにマッチする。offset で新たな文字列が切り出されるわけ
    // ではないので、1度しかマッチしないというサンプル。
}
/*
Array
(
    [0] => もも
    [1] => 0
)
 */

関連するマニュアル

使用した関数

その他

今回、さまざまなブログを参照しながら勉強をしましたが、多くのサイトでこちらの本を勧めていました。未読ですが、ぜひとも近日中に読んでみたいです。