複雑な正規表現を簡単に作れるようにした
Monday, September 21, 2015 01:28:00 PM
この記事は先日の正規表現を簡単に作るにはの続編になります。
前回、RFC3986のような複雑な正規表現をVerbal Expressionの既存実装でやろうとすると、うまくできないという問題に直面しました。 そこでRubyで実装されていたHEXPRESSをPHPへ移植しようという流れです。
PHP版Hexpressを作った
さっそく移植してみました。
PSR-2の対応が実施中なのですが、Ruby側の機能とテストはすべて移植し、PHP版でいくつか機能追加をしています。
どんな感じか
READMEに書いてあるとおりインストールはcomposer経由でできます。
composer require sizuhiko/hexpress
具体的な利用方法は以下のサンプルのとおりです。
use Hexpress\Hexpress;
$pattern = (new Hexpress())
->start("http")
->maybe("s")
->with("://")
->maybe(function($hex) { $hex->words()->with("."); })
->find(function($hex) { $hex->matching(function($hex) {$hex->word()->with("-");})->many(); })
->has(".")
->either(["com", "org"])
->maybe("/")
->ending();
echo $pattern; #=> "^https?\:\/\/(?:(?:\w)+\.)?([\w\-]+)\.(?:com|org)\/?$"
echo $pattern->toRegExp(); #=> "/^https?\:\/\/(?:(?:\w)+\.)?([\w\-]+)\.(?:com|org)\/?$/"
Hexpress
クラスのインスタンスを生成して、メソッドチェインで正規表現を組み立てます。
ネストしたい場合は、コールバック関数を指定すると、新しいHexpress
のインスタンスがパラメータで渡ってくるので、
それに対してメソッドチェインでつないていきます。
Hexpress
は__toString()
を実装しており、文字列表現を取得すると指定された定義を展開します。
実際に正規表現として利用するにはtoRegExp()
を呼び出すと、左右にデリミタを挿入した文字列が取得できるので、preg_match
などの関数で利用可能になります。
例えば以下のような利用方法です。
preg_match("http://example.com/", $pattern->toRegExp());
複雑な正規表現は定義できたのか?
できました!!
テストコードとしてRFC3986Test.phpを作成しました。 実装は結構長いので、こちらに添付することは省略しますが、RFCの定義に書いてあるとおりそのままメソッドをつないでいくだけで、 RFCに対応した正規表現を構築することができました。(※IPv6の定義は未実装です、ご了承ください)
PHPUnitのデータプロバイダexampleUriProvider
でマッチさせるパターンをftp,www,mail,news,tel,telnet,urnについて定義してすべて通過しています。
PHP版では、マッチするときfind
メソッドで名前付きサブパターンを利用できるようにしました。
以下の例のように、find
の第二引数にport
を指定できます。
private function port()
{
return (new Hexpress())->has(':')->find(function ($hex) { $hex->digits(); }, 'port');
}
上記を含むパターンをpreg_match
で処理すると、マッチング結果に名前付きで値を取得できるようになります。
preg_match("http://example.com:80/", $pattern->toRegExp(), $matches);
echo $matches['port']; #=> '80'
どうなのか?
たとえば今回作成したRFC3986の定義を正規表現としてダンプすると、以下のようになります。
/^(?P<scheme>[a-zA-Z](?:[a-zA-Z0-9\+\-\.])*)\:(?P<hierPart>(?:\/\/(?P<authority>(?:(?P<userinfo>(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:))*)@)?(?P<host>(?:(?:0-9|1-90-9|10-90-9|20-40-9|250-5)\.(?:0-9|1-90-9|10-90-9|20-40-9|250-5)\.(?:0-9|1-90-9|10-90-9|20-40-9|250-5)\.(?:0-9|1-90-9|10-90-9|20-40-9|250-5)|(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]))+))(?:\:(?P<port>(?:\d)+))?)(?P<pathAbempty>(?:\/(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))*)*)|(?P<pathAbsolute>\/(?:(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))+(?:\/(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))*)+)?)|(?P<pathRootless>(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))+(?:\/(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))*)*)|(?P<pathEmpty>^(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@))))(?:\?(?P<query>(?:(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@)|\/|\?))*))?(?:#(?P<fragment>(?:(?:(?:[a-zA-Z0-9\-\._~]|%(?:[0-9A-Z]){2}|[\!\$&'\(\)\*\+,;\=]|\:|@)|\/|\?))*))?$/
私はいきなりこれを空手形で実装することはできそうにないので、今回hexpress
を移植して良かったなと思っています。
何か複雑な正規表現を定義したい場合などに役立てば幸いです。
さいごに
日本PHPカンファレンス2015が10/3(土)に行われます。 当日のプログラムも公開され、私はトラック3で10:50から11:20まで「Composerではじめるアプリケーション開発」というセッションを担当します。 他の番組が強力なため私の会場は比較的空いているのではないかと思うので、もしご都合がつく方はよろしくお願いします。