Technote

by sizuhiko

複雑な正規表現を簡単に作れるようにした

この記事は先日の正規表現を簡単に作るにはの続編になります。

前回、RFC3986のような複雑な正規表現をVerbal Expressionの既存実装でやろうとすると、うまくできないという問題に直面しました。 そこでRubyで実装されていたHEXPRESSをPHPへ移植しようという流れです。

PHP版Hexpressを作った

さっそく移植してみました。

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ではじめるアプリケーション開発」というセッションを担当します。 他の番組が強力なため私の会場は比較的空いているのではないかと思うので、もしご都合がつく方はよろしくお願いします。