正規表現を読む

正規表現はプログラミングで文字列が、あるパターンに従っているかどうか確認したい時等によく使いますが、?とか+とか*とかの記号を使った記述が多いため、一見するとどういうパターンにマッチさせようとしているのか理解しにくい場合があります。こいういう場合、パターンの頭から注意深く見て行く必要があります。

 


var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

これは、JavaScriptでURL文字列にマッチさせる正規表現です。一見すると頭がクラクラしてきそうな記号の羅列です。

この正規表現を読み解いて行こうと思います。

 

最初の/と最後の/で、これが正規表現リテラルであることを示しています。意味的には次のように書いた場合と同じです。


var parse_url = new RegExp("^(?:([A-Za-z]+):)?(\\/{0,3})([0-9.\\-A-Za-z]+)(?::(\\d+))?(?:\\/([^?#]*))?(?:\\?([^#]*))?(?:#(.*))?$");

パターンを文字列として渡しているため、正規表現のエスケープ記号である\をさらにエスケープして\\と書く必要がある点に注意が必要です。通常は正規表現リテラルを使った方が分り易いです。

この正規表現を使ってURL文字列をマッチさせると以下のような結果を得られます。


var url = "http://www.example.com:80/hoge?q=fuga#fragment";
var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
var result = parse_url.exec(url);
console.log(result);

# 結果
[
  "http://www.example.com:80/hoge?q=fuga#fragment",	// URL(入力値)
  "http",										// スキーム
  "//",										// スラッシュ
  "www.example.com",						// ホスト
  "80",										// ポート
  "hoge",										// パス
  "q=fuga",									// クエリ文字
  "fragment"									// フラグメント
]

正規表現に渡したURL文字列全体と、パターンの中で「(」と「)」で囲まれた部分(但し、「(?:」で始まり「)」で終わる部分は除く)が結果として得られています。

では、この正規表現を最初から見て行きましょう。


^

まず、^ですが、これは正規表現の最初で使われた場合、文字列の始まりを意味します。^をつけない場合、文字列中のどこかにパターンにマッチする部分があるかどうかを探すことになります。^で始めると、文字列の最初からパターンにマッチすることを条件に含めることになります。


(?:([A-Za-z]+):)?

ここは、URLの「http:」のようなスキーマの部分にマッチさせようとしています。

パターンにマッチした場合、()で囲まれた部分はキャプチャグループと呼ばれ、結果の配列に含まれることになりますが、(の後に?:がついている場合、非キャプチャグループとなり、結果に含まれません。そして外側のかっこで囲まれた部分の最後に?があるので、この部分全体があってもなくても構わないことを示しています。つまり、http:の無い、//www.example.com:80/hoge?q=fuga#fragment のような文字列でもマッチします。

[A-Za-z][]で囲まれた部分は文字クラスと呼ばれ、この場合、AからZまでの文字と、aからzまでの文字の何れかを表しています。 そして、その後に+がついているので、1つ以上存在する必要があります。

その次の:は単に文字としての:を表しています。:を内側のキャプチャグループから外し、非キャプチャグループに含めることで、結果に、:を含めないようにしています。マッチした場合、「http」や「https」、「ftp」といった結果が得られます。


(\/{0,3})

()で囲まれた2番目のキャプチャグループです。「htt:」の後に続く//の部分にマッチさせようとしています。/は特別な意味を持つ(正規表現の終わりを意味する)ので、\をつけてエスケープしています。

{0,3}があるので、0個から3個までの/を意味しています。


([0-9.\-A-Za-z]+)

3番目のキャプチャグループです。「www.example.com」の部分にマッチさせようとしています。ここでも文字クラスが使われています。0から9の数値、.(ピリオド)、-(ハイフン)(-は文字クラスの中では、範囲を示す特別な記号なので、\をつけてエスケープしています。)、AからZの文字、a-zの何れかを意味し、文字クラスの後に+がつけてあるので、これらの文字の何れかが1つ以上現れることを意味しています。


(?::(\d+))?

次は、ポート番号にマッチさせようとしています。外側のかっこで囲まれた部分は非キャプチャグループです。(?:で非キャプチャグループを表し、次の:はポート番号の前にある:です。それに続くかっこで番号の部分のみをキャプチャしています。

\dは、数値を意味し、[0-9]と同じ意味になります。+がつけてあるので、1つ以上の数値を意味しています。


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

次は、パスの部分です。外側のかっこは非キャプチャグループで、最後に?がついているので、この部分全体が省略可能であることを示しています。

/\でエスケープされています。

つづくキャプチャグループが、結果として得るパスの部分です。文字クラスの中が^で始まっています。正規表現の最初で、^が使われた場合、文字列の最初を意味しましたが、文字クラスの最初に^が使われた場合、後に続く文字の集合を否定することになります。ここでは、?#以外の全ての文字がマッチすることを意味します。言い換えると、?#が現れるところまでマッチすることになります。文字クラスの後に*がついているので、?#以外の文字が0個以上連続することを意味します。0個以上なので、全く現れなくてもマッチします。

?#以外の文字ということは、改行コードやコントロールコードも含まれてしまうので厳密な意味では正しくないのですが、ここでは説明用に簡易的な表現にされています。


(?:\?([^#]*))?

次は、クエリ文字列にマッチさせる部分です。これも全体が非キャプチャグループになっており、キャプチャ部分から、最初の?を除くようにしています。最後に?がついているので、この部分全体が省略可能であることを示しています。

キャプチャグループの部分は上と同じパターンです。#以外の文字が0個以上続くことを意味しています。#は次でキャプチャする、フラグメントの始まりを意味します。


(?:#(.*))?

次は、フラグメントにマッチさせる部分です。パターン的には今までと同様です。全体が非キャプチャグループになっており、キャプチャ部分から、最初の#を除くようにしています。最後に?をつけて全体を省略可能にしているのも同様です。

キャプチャグループの.*は全ての文字を意味しています。


$

$は文字列の最後にマッチします。$で終わっているので、URLの後ろに何もついていないことを意味します。

以上が、URLにマッチさせる正規表現の説明になります。一見、意味不明な記号の羅列のように見える正規表現もこのように頭から読み解いていくと、何にマッチさせようとしているのかが見えてきます。

 

正規表現はバターンに忠実に従おうとすればするほど、複雑になりがちです。そして複雑になればなるほどメンテナンス性を損ないます。どこまで正確さを求めるかと性能を天秤にかけ、後から自分が読む時に理解できる程度にはシンプルに書く事を心がけた方が良いと思います。少なくとも何にマッチさせようとしているのかコメントを残しておくことが大事ですね。今回の正規表現も、URLにマッチさせようとしているのだという事前情報があるのとないのとでは、読み解き易さも大分変わってきます。