LoginSignup
14
15

More than 5 years have passed since last update.

正規表現を使い、URIをパースするPHPの関数を作る

Last updated at Posted at 2014-10-30

注意

本記事で書かれている関数及びロジックはバリデーションをするための物ではありません。
また、RFC3986に記載されている正規表現を利用しており、内容を見て分かる通り、粒度の低い分解になっております。
上記を踏まえた上で、ご指摘等頂けますと幸いです。

該当の正規表現

RFC3986の下部、Appendix B. Parsing a URI Reference with a Regular Expressionと見出しがある箇所に、以下のような正規表現が書かれています。
^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?

この正規表現を利用することで、次のURIは以下のように分解されます。
http://www.ics.uci.edu/pub/ietf/uri/#Related

$1 = http:
$2 = http
$3 = //www.ics.uci.edu
$4 = www.ics.uci.edu
$5 = /pub/ietf/uri/
$6 = <undefined>
$7 = <undefined>
$8 = #Related
$9 = Related

詳しくは、RFC3986に書かれていますので、併せて参照されると良いでしょう。

各パートの分類

前項のように分解された各パートは以下のように分類されます。

scheme    = $2
authority = $4
path      = $5
query     = $7
fragment  = $9

これを連想配列として返却する関数を作ります。

コード

function parse_uri($uri) {
    // 返却するキー
    $intersect  = ['scheme', 'authority', 'path', 'query', 'fragment'];
    // 名前付きサブパターンを使って各分類に分解
    $regex      = join([
        '^((?<scheme>[^:/?#]+):)?',
        '(//(?<authority>[^/?#]*))?',
        '(?<path>[^?#]*)',
        '(\?(?<query>[^#]*))?',
        '(#(?<fragment>.*))?',
    ]);
    preg_match('`' . $regex . '`i', $uri, $matches);
    // array_filter()で空要素を削除
    $matches    = array_filter($matches, 'strlen');
    // array_flip()で$intersectのキーと値を反転
    $intersect  = array_flip($intersect);
    // array_intersect_key()で共通のキー以外の要素を削除し、返す。
    return  array_intersect_key($matches, $intersect);
}

ちなみに、PHPの組み込み関数にparse_urlという関数がありますが、こちらの記事でも触れられていますように、うまく(?)パースできない場合があります。
(※そもそも動作しないと言われている相対パスを指定していたり、予約文字をエンコーディング無しで使っていたりするので、当たり前っちゃ当たり前ですが。)

この辺の話を突っ込んで話すと本筋から離れてしまいますので、割愛させて頂きますが、このparse_urlという関数に似せて書きなおしたのもおいておきます。(コメント等は割愛)

namespace MyFunctions;

const PHP_URI_SCHEME    = 0b0000000001;
const PHP_URI_AUTHORITY = 0b0000000010;
const PHP_URI_PATH      = 0b0000000100;
const PHP_URI_QUERY     = 0b0000001000;
const PHP_URI_FRAGMENT  = 0b0000010000;
function parse_uri($uri, $component = -1) {
    $intersect  = [];
    if (PHP_URI_SCHEME      & $component) array_push($intersect, 'scheme');
    if (PHP_URI_AUTHORITY   & $component) array_push($intersect, 'authority');
    if (PHP_URI_PATH        & $component) array_push($intersect, 'path');
    if (PHP_URI_QUERY       & $component) array_push($intersect, 'query');
    if (PHP_URI_FRAGMENT    & $component) array_push($intersect, 'fragment');

    $regex      = '^((?<scheme>[^:/?#]+):)?(//(?<authority>[^/?#]*))?(?<path>[^?#]*)(\?(?<query>[^#]*))?(#(?<fragment>.*))?';
    preg_match('`' . $regex . '`i', $uri, $matches);
    return  array_intersect_key(array_filter($matches, 'strlen'), array_flip($intersect));
}

なお、userやpass、host、port等には分解できません。あしからずご了承下さい。

おまけ

厳密にパースしたい場合は、こんな感じになるのでしょうか。
自信は無いので、もし使われる場合は十分ご注意下さい。 使わないほうが身のためです。
もっと効率の良い表現があるような気もするし、でも思い浮かばないし、所々間違ってるような気もするし。
もし、これはダメだよとか、こうしたほうがいいよとか有りましたら、ご指摘・編集リクエスト等頂けますと幸いです。
まあ、そもそも正規表現一発でやる必要は無いのですが・・・。

namespace MyFunctions;

const PHP_URI_SCHEME    = 0b0000000001;
const PHP_URI_AUTHORITY = 0b0000000010;
const PHP_URI_PATH      = 0b0000000100;
const PHP_URI_QUERY     = 0b0000001000;
const PHP_URI_FRAGMENT  = 0b0000010000;
const PHP_URI_USER      = 0b0000100000;
const PHP_URI_PASS      = 0b0001000000;
const PHP_URI_USERINFO  = 0b0010000000;
const PHP_URI_HOST      = 0b0100000000;
const PHP_URI_PORT      = 0b1000000000;

function parse_uri($uri, $component = -1) {
    $intersect  = [];
    if (PHP_URI_SCHEME      & $component) array_push($intersect, 'scheme');
    if (PHP_URI_AUTHORITY   & $component) array_push($intersect, 'authority');
    if (PHP_URI_PATH        & $component) array_push($intersect, 'path');
    if (PHP_URI_QUERY       & $component) array_push($intersect, 'query');
    if (PHP_URI_FRAGMENT    & $component) array_push($intersect, 'fragment');
    if (PHP_URI_USER        & $component) array_push($intersect, 'user');
    if (PHP_URI_PASS        & $component) array_push($intersect, 'pass');
    if (PHP_URI_USERINFO    & $component) array_push($intersect, 'userinfo');
    if (PHP_URI_HOST        & $component) array_push($intersect, 'host');
    if (PHP_URI_PORT        & $component) array_push($intersect, 'port');

    $ipv4Part       = '(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)';
    $ipv4           = '(?:(?:'.$ipv4Part.'\.){3}'.$ipv4Part.')';
    $ipv6Part       = '[0-9a-f]{1,4}';
    $ipv6LowerPart  = '(?:'.$ipv6Part.':'.$ipv6Part.'|'.$ipv4.')';
    $ipvFuture      = 'v[0-9a-f]+\.[!$&-.0-;=_a-z~]+';
    $ipv6           = join('|', [
        '(?:'.$ipv6Part.':){6}'.$ipv6LowerPart,
        '::(?:'.$ipv6Part.':){5}'.$ipv6LowerPart,
        '(?:'.$ipv6Part.')?::(?:'.$ipv6Part.':){4}'.$ipv6LowerPart,
        '(?:(?:'.$ipv6Part.':)?'.$ipv6Part.')?::(?:'.$ipv6Part.':){3}'.$ipv6LowerPart,
        '(?:(?:'.$ipv6Part.':){0,2}'.$ipv6Part.')?::(?:'.$ipv6Part.':){2}'.$ipv6LowerPart,
        '(?:(?:'.$ipv6Part.':){0,3}'.$ipv6Part.')?::'.$ipv6Part.':'.$ipv6LowerPart,
        '(?:(?:'.$ipv6Part.':){0,4}'.$ipv6Part.')?::'.$ipv6LowerPart,
        '(?:(?:'.$ipv6Part.':){0,5}'.$ipv6Part.')?::'.$ipv6Part,
        '(?:(?:'.$ipv6Part.':){0,6}'.$ipv6Part.')?::',
    ]);
    $ip             = '\[(?:'.$ipv6.'|'.$ipvFuture.')]|'.$ipv4;
    $pathPart       = '(?:[-.0-9_a-z~]|%[0-9a-f][0-9a-f]|[!$&-,:;=@])';

    $scheme         = '(?:(?P<scheme>[a-z][-+.0-9a-z]*):)';
    $user           = '(?P<user>(?:[-.0-9_a-z~]|%[0-9a-f][0-9a-f]|[!$&-,;=])*)?';
    $pass           = '(?::(?P<pass>(?:[-.0-9_a-z~]|%[0-9a-f][0-9a-f]|[!$&-,;=])*))?';
    $userinfo       = '(?:(?P<userinfo>'.$user.$pass.')@)?';
    $host           = '(?P<host>'.$ip.'|(?:[-.0-9_a-z~]|%[0-9a-f][0-9a-f]|[!$&-,;=])*)';
    $port           = '(?::(?P<port>\d*))?';
    $authority      = '(?P<authority>'.$userinfo.$host.$port.')';
    $path           = '(?P<path>(?<!//)/(?:'.$pathPart.'+(?:/'.$pathPart.'*)*)?|(?<!//)'.$pathPart.'+(?:/'.$pathPart.'*)*|(?:/'.$pathPart.'*)*)?';
    $query          = '(?:\?(?P<query>(?:[-.0-9_a-z~]|%[0-9a-f][0-9a-f]|[!$&-,/:;=?@])*))?';
    $fragment       = '(?:#(?P<fragment>(?:[-.0-9_a-z~]|%[0-9a-f][0-9a-f]|[!$&-,/:;=?@])*))?';

    $regex          = $scheme.'(?://'.$authority.')?'.$path.$query.$fragment;
    preg_match('`' . $regex . '`i', $uri, $matches);
    return  array_intersect_key(array_filter($matches, function($v) {return '' !== strval($v);}), array_flip($intersect));
}

以下の記事を参考にさせて頂きました。
名前付きサブパターン等を利用するにあたり、元のコードからいじっている部分が多々有ります。

助けて正規表現ガチ勢のエライ人

追記

スキーム部に不要な ? が入ってました。
RFC3986の下記部分に違反してましたね。。

The scheme and path components are required,

追記の追記

と思ったら、参考にしたRFC3986の正規表現に ? ついてた…
最初のコードの方はRFC3986の正規表現を利用するということで、元に戻しておきました。
スキーム部が無いとホスト部(と思わしき文字列)が path とかに入っちゃうのでパースする前にバリデーションをかませるべきでしょうね。
そもそも、RFC3986のルールに則るのであれば、スキーム要素区切り子 (:) がなく、//から始まらないので、path に入っちゃうのも頷ける話ですが。

でも、スキーム部が省略可能とはどこにも書いてなかったような気がするんだけど、私が見落としているだけなんでしょうか…

14
15
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15