回数を指定して文字列置換
文字列を置換するのはふつうstr_replace関数を使用しますが
PHP: str_replace - Manual
これは文中の特定文字をすべて置換することしかできません。
最初の1回だけ置換したいような場合はpreg_replaceで回数指定することができます。
第1~第3引数はいつも通りに「置換前のパターン」「置換後の文字列」「置換対象の文字列」で、
第4引数に回数を入れておくだけでOK。
<?php // 置換前の文字列 $str = "abcabcabc"; // 1回だけ置換 $str1 = preg_replace("/a/", "A", $str, 1); // Abcabcabc // 2回だけ置換 $str2 = preg_replace("/a/", "A", $str, 2); // AbcAbcabc
先読み後読みと併用することで、「あるパターンより前の特定文字を1回だけ置換」「○文字目より後の特定文字を1回だけ置換」なんてことも。
<?php // 置換前の文字列 $str = "abcabcabc"; // あるパターンより前にある特定文字を1回だけ置換 // ※この例では存在しないパターンとなるので実際は置換されない $str3 = preg_replace("/a(?=bcd)/", "A", $str, 1) // abcabcabc // ○文字目より後の特定文字を1回だけ置換 $str4 = preg_replace("/(?<=.{3})a/", "A", $str, 1) // abcAbcabc
(先読み後読みについてはこちら)
正規表現の先読み・後読み - アナログCPU:5108843109
さらに、preg_replaceのパターンは配列でも指定できるので、配列指定かつ回数指定する場合の挙動はどうなってるんだろう…? と思い試してみました。
<?php $str = "abcabcabc"; var_dump( preg_replace(array("/a/", "/b/"), array("A", "B"), $str, 2) ); // ABcABcabc $str = "aaabbbccc"; var_dump( preg_replace(array("/a/", "/b/"), array("A", "B"), $str, 2) ); // AAaBBbccc var_dump( preg_replace(array("/a/", "/b/"), "X", $str, 2) ); // XXaXXbccc
配列で複数パターンを指定した場合、パターンごとに回数が適用されるようですね。
蛇足な雑談
いや、今回ハマったのが、「後読みには文字数が可変になるようなパターンを指定できない」というものでして、
具体的には、HTMLエスケープされたある文字列について、特定のタグのみエスケープを解除したいという事案でした。
例えばdivタグのみエスケープ解除するとして、
<div> <p> </p> </div> <div class="hoge"> <p> </p> </div>
を
<div> <p> </p> </div> <div class="hoge"> <p> </p> </div>
としたい、ということですね。
これを一発
<?php $str = " ... "; //(省略。エスケープ後文字列) $pattern_before = array( "/<(?=\/?div[\s\/>&])/", "/(?<=(<|\/)div[^&]*)>/", ); $pattern_after = array( "<", ">", ); $str = preg_replace($pattern_before, $pattern_after, $str);
こんな感じで置換してしまいたかったのですが、後読みの方がうまくいかず。
仕方ないので、今回調べた内容を使って以下のように実装しました。
<?php $str = " ... "; //(省略。エスケープ後文字列) while (true) { // divタグの位置を探す、なければ終了 // 「<(<)」と「div」の間はスラッシュのみ許容、「div」と「>(>)」の間は&以外を許容 if (!preg_match($"/<\/?div[\s\/>&]/", $str, $matches, PREG_OFFSET_CAPTURE)) { break; } // divタグが存在する場合: // 見つかった位置以降について、最初に見つかった<と>を置換するパターンを生成 $pattern_before = array( "/(?<=.{" . $matches[0][1] . "})</", "/(?<=.{" . $matches[0][1] . "})>/", ); $pattern_after = array( "<", ">", ); // 置換した文字列で$strを上書き $str = preg_replace(array(, ), array("<", ">"), $str, 1); }
もっと美しい実装があるかもしれません。
あまり厳密ではないですしなんかダサい気もします。
(今回実装していたのはCMSなので厳密さは一応こんなもんで良いのですが)