[JS] もう少し厳密に URL からファイル名等を取得する正規表現


よく見かける URL の正規表現に比べて、もう少し厳密にパースする正規表現の紹介になります。

正規表現なので他の言語等でも使用できると思いますが、ここでは JavaScript で動くコードを載せます。

1. RFC 3986 に書かれている URL の正規表現

WEB 上で調べると様々なひとたちが URL の正規表現を書いていると思いますが、URL の定義が書かれている RFC 3986 に既に (おそらく) 厳密な正規表現が書かれています。

自身でヘタに正規表現を書くよりもこちらを流用したほうが安全です。

以下、引用・意訳。

^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
 12            3  4          5       6  7        8 9

2 行目は、マッチ文字列のインデックスを分かりやすくするための数です。
例えば、以下のようにマッチします。

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

参考「RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax
Appendix B. Parsing a URI Reference with a Regular Expression

2. 例: ディレクトリとファイル名とクエリ文字列を取得

2.1. 一般的によく使われる JavaScript のコードの問題点

lastIndexOf() でファイル名やクエリ文字列を分離したり、split() でディレクトリやファイル名を分離するものが多いですが、厳密にはクエリ文字列そのものに /? を含めることができるため、理論上、正しく動作しない可能性があります。

const url = /* 'URL 文字列' */;
const query = url.slice(url.lastIndexOf('?') + 1);
const fileName = url.slice(0, url.lastIndexOf('?')).slice(url.lastIndexOf('/') + 1);

例:https://example.com/dir1/dir2/index.php?callback=https://example.com/dir3/dir4/index2.php?param

query === 'params'
fileName === `index2.php`

2.2. (おそらく) 厳密な正規表現

不要な括弧 ( ... ) を取り除いたりキャプチャしないように (?: ... ) に変更して、必要な部分だけをキャプチャします。

また、ファイル名にマッチする部分 5 ([^?#]*)(?:([^?#]*/)([^/?#]*))? に書き換え、(クエリ文字列が取り除かれた) パス文字列の最後のスラッシュから後ろをキャプチャします。

ファイル名を含めるには必ずスラッシュ / が 1 つ以上あるはずで、スラッシュがない場合にはファイル名が空になるはずなので、正しく動作するはずです….。

^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
 12            3  4          5       6  7        8 9

^(?:[^:/?#]+:)?(?://[^/?#]*)?([^?#]*)(\?[^#]*)?(?:#.*)?
                             1       2

^(?:[^:/?#]+:)?(?://[^/?#]*)?(?:([^?#]*/)([^/?#]*))?(\?[^#]*)?(?:#.*)?
                                1        2          3

※ここではクエリ文字列を区切り文字の ? ごとキャプチャしています。パスを再び組み立てる必要がある場合などは ? を残した方が便利です。逆に、POST 送信する目的などでは ? を除去した方が使いやすいため、状況によって使い分けてください (両方キャプチャすることもできます) 。

2.3. JavaScript でのコード

const url = /* 'URL 文字列' */;  
const matchedFileName = url.match(/^(?:[^:\/?#]+:)?(?:\/\/[^\/?#]*)?(?:([^?#]*\/)([^\/?#]*))?(\?[^#]*)?(?:#.*)?$/) ?? [];
const [, dir, fileName, query] = matchedFileName.map(match => match ?? '');

例:https://example.com/dir1/dir2/index.html?params#section1

dir === '/dir1/dir2/'
fileName === `index.html`
query === '?params'

例:https://example.com/dir1/dir2/index.php?callback=https://example.com/dir3/dir4/index2.php?param

dir === '/dir1/dir2/'
fileName === `index.php`
query === '?callback=https://example.com/dir3/dir4/index2.php?param'

3. おまけ: 拡張子も分離する

拡張子は拡張子で厳密に取り出そうとすると罠が多いため、以下のページを参考にさせていただきました。

^(.+?)(\.[^.]+)?$
 1    2

参考「ファイル名から拡張子とそうでない部分を分ける - Qiita」(※ Ruby の正規表現のため、少し記述が異なります)
参考「Railsの正規表現でよく使われる \A \z って何?? - Qiita

const url = /* 'URL 文字列' */;  

const matchedFileName = url.match(/^(?:[^:\/?#]+:)?(?:\/\/[^\/?#]*)?(?:([^?#]*\/)([^\/?#]*))?(\?[^#]*)?(?:#.*)?$/) ?? [];
const [, dir, fileName, query] = matchedFileName.map(match => match ?? '');

const matchedExt = fileName.match(/^(.+?)(\.[^.]+)?$/) ?? [];
const [, name, ext] = matchedExt.map(match => match ?? '');

例:https://example.com/dir1/dir2/index.html?params#section1

name === 'index'
ext === `.html`