URI 分解 正規表現 作ってみた

shiogumar

License: MIT License

Fork
0
Fav
0
View
35
  • Play

Fullscreen

Smart Phone

  • Readme
  • JavaScript 21 lines
  • HTML 24 lines
  • CSS 14 lines
URI を分解する関数と、そのテストページ。

◆背景
・前回作って使った URL 分解の正規表現をもっと仕様に近づけるために
URI の仕様を勉強しようと思った。

◆目的
・URI の仕様を理解した上で、可能な限り簡潔に
正規表現でササっとできる範囲で URI を分解してみよう!

◆成果
・正規表現を使った parseUri 関数を作ってみた。
・現時点の知識で parseUrl 関数を作り直してみた。
・どちらも、文字列を入れると分解後のオブジェクトを返す関数です。
・画面の [start] を押すと解析されるよ!
・JavaScript は一応そのままライブラリとして使えなくもない。

◆仕上がり
・URI を scheme, userinfo, host, port, path, query, fragment
に分解します。(parseUri 関数)
・もしくは、 URL を href, protocol, username, password, host
port, pathname, search, hash に分解します。(parseUrl 関数)
・IPv6 アドレスでも分解できます。
・各要素の中身までは解析してません。
・その結果、日本語などの非 ASCII が混じってても区切り文字にさえ問題が
なければとりあえず分解することはできるはずです。
・参考にしたのは URI の仕様書だけで、 IRI の仕様書と URL の仕様書は
流し読み程度です。
・ですので、 URL の分解、非 ASCII 文字対応については、おまけ程度の
ものだと思って下さい。
・正規表現は、横に長くならないように文字列の断片をつなぎ合わせて正規表現
オブジェクトのコンストラクタを使って作っています。

◆感想
・そもそもの URI が区切り文字だけを判定すれば分解ができるような構造に
なっているため、分解するだけのパーサーの作成は比較的ラクな方でした。
・前に仕様書を見ないで直感で書いたパーサーよりも少し厳密になっていて、
仕様から論理的に考えて正規表現を導き出すという方法を取りました。
・結果前回と大差ないですが、IPv6 対応できるようになったので個人的には
収穫があったと思っています。
・segment に ":" を入れられるというのは意外でした。
でも segment-nz-nc には ":" を入れられないので、相対参照で
authority が省略されていて "/" で始まらない path の最初のセグメ
ントだけ、 ":" はパーセントエンコードされていなければいけません。
・仕様書の読み始めからプロトタイプ完成までは一日。手直し二日。
作りながら思考をメモしていたので、そちらのほうが時間がかかりました。
・「確かに上手くいったし、そうすれば上手くいくという自信があったけど、
どうしてそう思ったのかを言葉にできない」というジレンマがあり、
思考メモを書くのは思った以上に難しかったです。
・URL の仕様書も軽く読んだけど URI と書き方がうんと違くて困惑しました。


◆思考メモ

前提として、今回 URI 分解する単位は以下のとおり。

scheme, userinfo, host, port, path, query, fragment

host, port についてはさらに細かな要素に分けられるが、正規表現で
サクッと行えるレベルではないため、ここでは判断しない。

1. URI は、 RFC 3986 の URI-reference の定義から、要素の出現順序、
区切り文字の位置、省略の可不可のみに焦点を当てて抽出すると、次のよう
な構文になっている。

URI = [ scheme ":" ] [ "//" [ userinfo "@" ] host [ ":" port ] ] path [ "?" query ] [ "#" fragment ]

要点は以下の通り。

<path は実質省略可能>

上記の構文上では path は省略可能と記載していないが、実際は省略可能
である。
RFC 3986 の定義によれば、 authority が存在する場合は path には
path-abempty だけが利用可能だが、この書式は 0 文字でも許容する。
authority が無い場合は path-empty が利用可能で、その内容は空。
つまり、path は実質的に省略可能といえる。

<先頭にくる可能性がある要素>

URI の定義によれば、 先頭から ":" までが scheme である。しかし、
相対参照の場合は scheme は存在しないため、今回のように両者を混在
させる場合は省略可能と判断する。
上記の path が実質省略可能であることも踏まえると、全ての要素が
省略可能ということになる。
つまり、最初に来る要素は scheme, authority, path, query,
fragment のいずれか、または末尾(全て省略)である。

<区切り文字について>
"#" "[" "]" ":" "/" "@" "?" は URI の構文上の区切り文字として
出現するため、これらを検出して文字列を分割することで URI を上記構文
の要素単位に分解することが可能である。
ただし ":" "/" "@" "?" は少々特殊で、区切り文字として出現する箇所
を過ぎると、以降は要素内の文字として利用可能になるという特徴がある。

<出現順序について>
URI の構文上、各要素は省略可能だが、要素の出現順序については明確に
決まっている。そのため、グローバルサーチは使用せずに前方から順に解析
していくことにする。

2. IPv6 のアドレスは ":" を含んでおり、 host には IPv6 を指定するこ
ともできる。その場合、 "[" と "]" で IPv6 のアドレスを囲うという
ルールがあるので、それらを判定すれば host と port の区切りとしての
":" と誤認せずに済む。

これを踏まえると次のようになる。

URI = [ scheme ":" ] [ "//" [ userinfo "@" ] ( ( "[" IPv6 "]" ) / IPv4 / reg-name ) [ ":" port ] ] path [ "?" query ] [ "#" fragment ]

IPv6 = IPv6address / IPvFuture
IPv4 = IPv4address

3. 分解する URI が正規のルールに則っており、かつ上記 1. の各要素にのみ
分解するだけであれば、各要素の中の構文については無視することができる。
なぜなら、 URI は区切り文字の出現だけを判断すれば URI を分解できる
ような作りになっているからである。

( URI の仕様をよく読んでみると、区切り文字だと誤認され兼ねない場所
では区切り文字は利用できないか、パーセントエンコーディングによって
他の文字に置き換えることになっている。そのため、分解する URI がそ
のルールさえしっかり守ってあれば、区切り文字の位置と有無を判定する
だけで URI の分解ができるし、そうでない文字列は URI ではないので
分解のしようがない)

それを踏まえ、要素の内部的な構文を無視するため、各要素を「出現する可
能性がある文字の集合」に変換すると、以下のようになる。

URI = [ scheme-chars ":" ] [ "//" [ userinfo-chars "@" ] ( ( "[" IPv6-chars "]" ) / IPv4-chars / reg-name-chars ) [ ":" port-chars ] ] path-chars [ "?" query-chars ] [ "#" fragment-chars ]

scheme-chars = *( DIGIT / ALPHA / "+" / "-" / "." )
userinfo-chars = *( DIGIT / ALPHA / "+" / "-" / "." / "_" / "~" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "," / ";" / "=" / "%" / ":" )
IPv6-chars = *( DIGIT / ALPHA / "+" / "-" / "." / "_" / "~" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "," / ";" / "=" / ":" )
IPv4-chars = *( DIGIT / "." )
reg-name-chars = *( DIGIT / ALPHA / "+" / "-" / "." / "_" / "~" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "," / ";" / "=" / "%" )
port-chars = *( DIGIT )
path-chars = *( DIGIT / ALPHA / "+" / "-" / "." / "_" / "~" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "," / ";" / "=" / "%" / ":" / "/" / "@" )
query-chars = *( DIGIT / ALPHA / "+" / "-" / "." / "_" / "~" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "," / ";" / "=" / "%" / ":" / "/" / "@" / "?" )
fragment-chars = *( DIGIT / ALPHA / "+" / "-" / "." / "_" / "~" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "," / ";" / "=" / "%" / ":" / "/" / "@" / "?" )

4. 上記の 3. と同じ理由から、URI が正規のルールに則っているのであれば、
区切りの位置と有無だけを判別すれば最小限の定義で分解することができる。
そのためには「内部に出現しない区切り文字」を抽出すると、以下のように
なる。

scheme-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可
userinfo-chars = "#" "[" "]" "/" "@" "?" は使用不可
IPv6-chars = "#" "[" "]" "/" "@" "?" は使用不可
IPv4-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可
reg-name-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可
port-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可
path-chars = "#" "[" "]" "?" は使用不可
query-chars = "#" "[" "]" は使用不可
fragment-chars = "#" "[" "]" は使用不可

これにより、 host 内の IPv4-chars / reg-name-chars について、
それらが使用不可能な文字の集合は全く同じのため、分岐は必要なくなった。
両方を合わせて IPv4-name-chars という一つの要素とした。

IPv4-name-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可

とすると、以下のように式を変えることができる。

URI = [ scheme-chars ":" ] [ "//" [ userinfo-chars "@" ] ( ( "[" IPv6-chars "]" ) / IPv4-name-chars ) [ ":" port-chars ] ] path-chars [ "?" query-chars ] [ "#" fragment-chars ]

5. 上記 4. のルールをそのまま拡張正規表現(ERE)に変換すると、以下の
ようになる。ただし、 "<>" 内の文字列は各要素の正規表現を入れること。

URI-regexp = ^(<scheme-regexp>:)?(//(<userinfo-regexp>@)?((\[<IPv6-regexp>\])|<IPv4-name-regexp>)(:<port-regexp>)?)?<path-regexp>(\?<query-regexp>)?(\#<fragment-regexp>)?

scheme-regexp = [^#[\]:/?@]*
userinfo-regexp = [^#[\]/?@]*
IPv6-regexp = [^#[\]/?@]*
IPv4-name-regexp = [^#[\]:/?@]*
port-regexp = [^#[\]:/?@]*
path-regexp = [^#[\]?]*
query-regexp = [^#[\]]*
fragment-regexp = [^#[\]]*

6. 上記 5. のURI-regexp に各要素の正規表現を入れると、以下のように
なる。

^([^#[\]:/?@]*:)?(//([^#[\]/?@]*@)?((\[[^#[\]/?@]*\])|[^#[\]:/?@]*)(:[^#[\]:/?@]*)?)?[^#[\]?]*(\?[^#[\]]*)?(\#[^#[\]]*)?

これで URI が区切り文字の扱いに間違いがないかを確認する正規表現が
できた。これを元に、各要素を取得することで分解できる。

7. 上記 6. を JavaScript の正規表現リテラルにした場合、以下の
ようになる。 "()" を利用していて出力が不要なものについては、
先頭に "?:" をつけて戻り値に入ってこないようにしている。

URI 分解の正規表現リテラル("/" のエスケープ処理込み)
/^([^#[\]:\/?@]*:)?(?:\/\/(?:([^#[\]\/?@]*)@)?((?:\[[^#[\]\/?@]*\])|[^#[\]:\/?@]*)(?::([^#[\]:\/?@]*))?)?([^#[\]?]*)(?:\?([^#[\]]*))?(?:\#([^#[\]]*))?/;

この正規表現オブジェクトの exec() の戻り値は、以下の値を持つ
配列となる。

[0]: URI
[1]: scheme
[2]: userinfo
[3]: host
[4]: port
[5]: path
[6]: query
[7]: fragment

8. おまけで、 JavaScript の Location オブジェクトと同じような
要素を取り出すには以下のようにする。

userinfo の削除
username の追加
password の追加
host (host と port) の追加
search には、 query の前の ? も含まれる
hash には、 fragment の前の # も含まれる

URI の仕様に則ったまま username と password の分解を含めると、
以下のようになる。なお、 username , password は共に ":" が使用
不可能なので、":" が 2 回以上出現した場合(URL ではあり得ないが、
URI の仕様上はあり得る)のエラー回避のために、 other-userinfo
を用意し、読み捨てることにした。

username-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可
= [^#[\]:/?@]*
password-chars = "#" "[" "]" ":" "/" "@" "?" は使用不可
= [^#[\]:/?@]*
other-userinfo-chars = "#" "[" "]" "/" "@" "?" は使用不可
= [^#[\]/?@]*

URI = [ scheme-chars ":" ] [ "//" [ username-chars [ ":" password-chars [ ":" other-userinfo-chars ] ] "@" ] ( ( "[" IPv6-chars "]" ) / IPv4-name-chars ) [ ":" port-chars ] ] path-chars [ "?" query-chars ] [ "#" fragment-chars ]
= ^(<scheme-regexp>:)?(//((<username-chars>(:<password-chars>(:<other-userinfo-chars>)?)?)@)?((\[<IPv6-regexp>\])|<IPv4-name-regexp>)(:<port-regexp>)?)?<path-regexp>(\?<query-regexp>)?(\#<fragment-regexp>)?
= ^(([^#[\]:/?@]*:)?(?://(?:(([^#[\]:/?@]*)(?::([^#[\]:/?@]*)(:[^#[\]/?@]*)?)?)@)?((?:\[[^#[\]/?@]*\])|(?:[^#[\]:/?@]*))(?::([^#[\]:/?@]*))?)?)([^#[\]?]*)(\?[^#[\]]*)?(\#[^#[\]]*)?

URL 分解の正規表現リテラル("/" のエスケープ処理込み)
/^([^#[\]:\/?@]*:)?(?:\/\/(?:([^#[\]:\/?@]*)(?::([^#[\]:\/?@]*)(?::[^#[\]\/?@]*)?)?@)?(((?:\[[^#[\]\/?@]*\])|(?:[^#[\]:\/?@]*))(?::([^#[\]:\/?@]*))?))?([^#[\]?]*)(\?[^#[\]]*)?(\#[^#[\]]*)?/;

この正規表現オブジェクトの exec() の戻り値は、以下の値を持つ
配列となる。

[0]: href (URI)
[1]: protocol (scheme)
[2]: username (userinfo 最初の区間)
[3]: password (userinfo 二番目の区間)
[4]: host (host[:port])
[5]: hostname (host)
[6]: port (port)
[7]: pathname (path)
[8]: search (?query)
[9]: hash (#fragment)

  • URI 分解 正規表現 作ってみた
  • URI 分解 正規表現 作ってみた

play

Complete!

Description What kind of game?

Control Device

jsdo.it websocket controller

Mouse

keyboard

smartphone

Fullscreen

shiogumar

Author

Default Panel

Size

  • Width: px
  • Height: px

code

QR Code

Discussion

Questions on this code?

Tags