タグ別アーカイブ: 正規表現

Search RegexプラグインでWordPress投稿を正規表現検索置換

※今回の記事の内容、特に自己責任で試してね。投稿記事の破損についてなど一切の責任は負いません。

そもそもの問題

過去にこのような投稿を上げていた。

小ネタ:WordPressがHTML5に対応して、過去記事の見出しに困る

WordPressがHTML5に対応した際、それまで記事部分での見出しレベルトップはh3であったのが、h2に繰り上げとなった。その影響で過去投稿をいちいち引っ張り出してきて、手動で修正するのが面倒だよねという話。
それに対して、見出しレベルを変更するこんなプラグインもありますよと紹介していたのがこの投稿。

WordPressテーマ変更に伴う記事のルート見出しレベルの変動を吸収するプラグイン WP hn Convert

記事内でも書いている通り、このプラグインを使ってWordPress関数the_content()をリプレースする形だと、以降も見出しトップレベルをh3にして投稿をし続けなければならず、この時は採用を断念していた。結局、見出しトップレベルの問題は全投稿の見出しを手動で書き換えることで解決した。

そんな貴方にオススメ。Search Regexプラグイン!

3年くらい経って、解決法に気付いた。全投稿を正規表現を用いて検索・置換するプラグインがあれば、この問題は容易に解決することが出来たのではないだろうか。そういう痒いところに手が届くプラグイン、きっとあるはず。
ありました。それがSearch Regexプラグイン。早速インストールして、具体的な指定方法を考えてみよう。

こんな感じで、インストール後は"ツール"メニューに出現

こんな感じで、インストール後は”ツール”メニューに出現

まず何より、左下にあるRegexチェックボックスにチェックを入れよう。これに気付かず、しばらく格闘してしまった(ココの表示がボールドになっているの、おかしくない?)。Search patternフィールドに検索文字列を、Replace patternフィールドに置換文字列を入力する。Replaceボタンを押すと、置換例を表示してくれるけれどデータベースに反映はしない。Replace & Saveボタンは押すと取り返しのつかないボタン。データベースに即時反映するヤツなので、前者のボタンで置換例を見て微調整しつつ、上手くいっているようだったら後者のボタンを押すようにしよう。

正規表現を使って見出しレベルを一段ずつ上げる

では実際にやってみよう。見出しレベルが一段ずつ上がるので、たとえば先にh6を置換してh5にして…というように下からやっていってしまうと、最終的に全部h2になってしまう(笑)。h3をh2に、その次にh4をh3に…という順序で手をつけよう。

 
Search pattern:/<h3>(.*?)<\/h3>/
Replace pattern:<h2>$1</h2>

このようなパターンで、h3の見出しが見出し語そのままh2に昇格する。Replace pattern側の$1というのが参照文字(preg_replaceの解説なども参照)で、h3タグに挟まれた文字を一旦預かり、置換パターン側に展開してくれる。もし預かる箇所が2ヶ所以上だったら、$2、$3…という形で指定が出来る。ちなみに$0にはh3タグも含んだ文字列全体が格納されている。

Search Regexによる置換イメージ

Search Regexによる置換イメージ

この置換を、次はh4…とどんどんやっていけば良い。

自サイトの内部リンクからtarget=”_blank”を外す

次の課題。昔のWEB制作ではひたすらリンクにtarget=”_blank”を付与していたものである。とりあえず付けられるなら付けておくか、といった感覚で、WordPressのエディタでもチェックボックス1つで付けられてしまうものだからかかさず付けていた。
それが時代の変遷とともに訪問者の閲覧環境がモバイルデバイス中心になると、モバイルブラウザ上でウィンドウやタブを何枚も開くことは難しく、即ユーザビリティの低下となるため、頻繁にtarget=”_blank”を使うサイトは忌避されユーザの離脱率が高くなるという問題が顕在化した。そのため、最近は内部リンクについてはtarget=”_blank”をつけないページ作りが正解になっているらしい。
では、過去の投稿の内部リンクからtarget=”_blank”をはがすパターンについて考えてみよう。

 
Search pattern:/<a\shref="http:\/\/akisi\.tabiyaku\.net([^>]*?)\starget="_blank">/
Replace pattern:<a href="http://akisi.tabiyaku.net$1>

このサイトのアドレスがhttp://akisi.tabiyaku.netから始まるので前方一致に入れているわけで、自身のサイトで流用する場合は適宜自サイトのアドレスに変えて欲しい。また、このパターンはtarget=”_blank”がaタグの最後に来るという性善説で考えているので、流用の際にはそこも注意である。

その他Search Regexの応用例として、Tabnabbing防止にtarget=”_blank”付の外部リンクに自動的にrel=”noopener”を付ける処理を出そうかとも思ったのだけれども、なんだか分岐の可能性が多そうなのでやめました(元々rel属性に何かの値が指定されていた場合など)。我こそはと思う方はやってみて、上手いやり方を教えて下さいな。


PHPのもう一つのパターンマッチング関数glob

PHP4.3.0以降搭載されているglob関数は、パス文字列を引数に与えることでパターンマッチするパスを探して配列として返してくれる便利な関数です。パターンマッチということで、正規表現のパターンを与えるとマッチングをしてくれるPCRE関数(preg_matchなど)に近いように思われますが、globが解釈するパターンはUNIXシェルのパス名マッチのパターンであり、正規表現より煩雑にならない書き方ができるようです。
たとえば前回preg_grep関数と正規表現を用いて実装した、ディレクトリを検索してその中の拡張子が.jpgのファイルだけ抽出し、セレクトメニューの項目名にするという処理は、globを使うとこのような感じになります。

<?php
$dir = "sample";
$parentlength = strlen($dir) + 1;
echo '<form name="" method="" action=""><select name="">';
foreach(glob($dir."/*.csv") as $val){
echo '<option value="'.htmlentities(substr($val,$parentlength),ENT_QUOTES,"UTF-8").'">'
.htmlentities(substr($val,$parentlength),ENT_QUOTES,"UTF-8").'</option>';
}
echo '</select><input type="submit" value="表示"></form>';
?>

preg_grepでの例の再現のため、項目名としてファイル名のみを取り出そうとしてややこしくなっている部分はありますが、注目すべきはかの例にあったscandir関数のような、ディレクトリ内のパスを取得し配列に納めるというプロセスが省略されているという点です。glob関数はパス検索に特化しており、scandirの直接の上位互換と捉えても良いかもしれません。
さて、関数の説明です。まずパターン文字列の表記方法ですが、PCRE関数のように文字列の前後をスラッシュで囲む必要は無く、パスにワイルドカードなどの特殊記号を交えたものをそのまま与えます。以下が特殊記号です。

記号 意味 使用例 マッチするテキスト
*(アスタリスク) 0文字以上の任意の文字列(ワイルドカード) /home/*/lib /home/userA/lib,home/userB/lib
?(クエスチョンマーク) 任意の1文字 /img/img???.jpg /img/img001.jpg,/img/img002.jpg
¥(円マーク) 特殊文字のエスケープ(windows) /post.php¥?* /post.php?post=””
\(バックスラッシュ) 特殊文字のエスケープ(windows以外) /post.php\?* /post.php?post=””
[](角括弧) 括弧内に現れる文字のどれか /te[xs]t.php /text.php,/test.php
-(ハイフン)を使った応用編 /te[a-z]t.php /tebt.php,/tewt.php(aからzまで可)
-(ハイフン)を使った応用編 /te[1-9]t.php /te3t.php,/te8t.php(1から9まで可)
!(エクスクラメーション)は角括弧中で否定の意 /te[!nx]t.php /test.php(text,tentはダメ)

正規表現と細かな違いがありますね。アスタリスクがワイルドカードの役割をするという辺りは、CSSやSQLの熟練者にはわかり易くて良いのではないでしょうか。
書式と定数の説明です。

glob(引数1,引数2);
引数1 : パターン文字列
引数2 : オプションフラグ。省略可。以下の定数をとり得る。
GLOB_MARK ディレクトリがマッチした場合最後にセパレータのスラッシュを付加
GLOB_NOSORT 結果として返す配列をソートしない(デフォルトはソート)
GLOB_NOCHECK マッチする結果が無かった場合、検索パターン自体を返す
GLOB_NOESCAPE エスケープ文字(¥,\)を機能させない
GLOB_BRACE {パターン1,パターン2,パターン3}という指定で複数パターンの検索をする
GLOB_ONLYDIR マッチしたディレクトリのみを返す
GLOB_ERR エラー時に停止する(PHP5.1.0以降)

正規表現における|(パイプライン)でのOR検索は、引数にGLOB_BRACEを与えた上で{}(中括弧)で括って行う必要があります。

preg_grepを使った例では、読み込んだファイル名を一旦ユーザインターフェースに出力しユーザに選ばせるという、完全にリモートのWEBサービスを意識した構成でした。それに対してglobの強みが生きてくるのは、ディレクトリに追加されたファイルを強制的に全部読み込んで表示するといった、ユーザの了解を省略したローカルディレクトリベースのアプリケーションなのかもしれません。


PHPのPCRE関数を使ってデイレクトリ内の特定拡張子ファイルのリストを取得する

前回解説した正規表現とPCRE関数を使って、ディレクトリ内の特定の拡張子のファイルのリストを作成してみます。今回のサンプルプログラムではさらに、特定の拡張子のファイルリストを得た後それをhtmlのselect要素に整形することで、ユーザにファイルを選ばせて処理を加えるWEBアプリケーションのインターフェース部分という体裁で書いてみようと思います。

<?php
$dir = "sample";
echo '<form name="" method="" action=""><select name="">';
foreach(preg_grep("/\.jpg$/",scandir($dir)) as $val){
echo '<option value="'.htmlentities($val,ENT_QUOTES,"UTF-8").'">'
.htmlentities($val,ENT_QUOTES,"UTF-8").'</option>';
}
echo '</select></form>';
?>

ディレクトリを検索してその中の拡張子が.jpgのファイルだけ抽出し、セレクトメニューの項目名にしています。ここで使っている関数は、引数2に配列を与えると引数1に与えた正規表現文字列にマッチする要素だけを配列に格納し、返り値として返すpreg_grepという関数です。

preg_grep(引数1,引数2,引数3);
引数1 : 正規表現文字列(前後にスラッシュで囲む)
引数2 : 抽出元の配列
引数3 : 反転フラグ。PREG_GREP_INVERTを与えると、条件にマッチしない要素の配列を返す。省略可。

例では引数2にscandir関数を与えて、ディレクトリ内の全てのファイル/ディレクトリ名を配列にしています。その中からの拡張子.jpgの抽出を、preg_grep関数が担当するようになっています。
この例ではあまり問題になりませんが、preg_grep関数には気をつけなければならない点が一点あります。返り値として返す配列の添字が、引数に与えた元の配列のものを継承するということです。つまり配列の添字が必ずしも連番になっていないので、添字をカウントアップさせて要素をひとつずつ取り出すループとは上手く連携できません。そうした処理と連携する場合は、array_values関数を使って配列の要素に連番の添字をアサインし直しましょう。


PHPで正規表現による文字列のパターンマッチングを行う

文字列の中の特定のパターンを検索する方法として、正規表現というものがあります。以前、.htaccessでディレクトリ内の特定文字列を含むファイルへのアクセスを禁止する方法について書いたエントリの中で少し触れたのですが、今回は書式のおさらいと、PHPでの対応する関数の紹介を行います。

まず正規表現の書式ですが、以前紹介したエスケープ(\または¥)、行頭(^)、行末($)、または(|)の他にもいくつか記号があるので、一覧表にしてみます。

記号 意味 使用例 マッチするテキスト
.(ピリオド) 任意の一文字(ワイルドカード) te.t text,tent,te!t
*(アスタリスク) 直前の文字の0回以上の繰り返し te.*t text,texxxt,tet
+(プラス) 直前の文字の1回以上の繰り返し te.+t text,texxxt
?(クエスチョンマーク) 直前の文字が0または1回登場 te.?t text,tet
¥(円マーク) 特殊文字のエスケープ(windows) te¥.¥?t te.?t
\(バックスラッシュ) 特殊文字のエスケープ(windows以外) te\.\?t te.?t
^(カレット) 行頭 ^\.ht .htaccess,.htpasswd
$(ドル) 行末 nt$ tent,plant
[](角括弧) 括弧内に現れる文字のどれか te[xyz]t text,tezt
-(ハイフン)を使った応用編 te[a-z]t tebt,tewt(aからzまで可)
-(ハイフン)を使った応用編 te[1-9]t te3t,te8t(1から9まで可)
^(カレット)は角括弧中で否定の意 te[^nx]t test(text,tentはダメ)
()(丸括弧) グルーピング (te)*xt xt,text,tetext
|(パイプライン) または ([a-z]|[1-9])* aaa,a,9999

PHPで正規表現によるマッチングを行うには、PCRE関数というPerlでの正規表現に準拠した関数群を使います。preg_という接頭辞がついたこの関数群に正規表現のパターンを与える時は、パターン文字列の前後をスラッシュで囲むという約束事があります。基本的なpreg_match関数で例を見てみましょう。

<?php
$teststring = "Hello!";
preg_match("/He[a-z]+/",$teststring);
?>

引数1にパターン文字列、引数2に検索対象を与えています。引数3、4、5もあるのですが、省略可です。 パターン文字列にマッチしたものがあると、preg_match関数は返り値として1を返します。ちなみに返り値として検索がマッチした回数を返してくれる、preg_match_allという関数もあります。
その他、検索置換機能を備えたpreg_replaceや、配列を精査してくれるpreg_grepという関数もありますので、それらの活用例もいずれエントリでアップできればと思っています。