2022-08-14

正規表現先読みを使って、マッチから除外されるべき表現を書く

IPアドレス(IPv4)にマッチする正規表現は以下である

^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$

(出典:https://www.javadrive.jp/regex-basic/sample/index4.html)


0~255を判定している部分は置いといて、0~255を判定する記述が二つに分かれているのが気になる。

間に"."が挟まるからだ。

IPアドレスでは先頭に"."があってもおかしいし、末尾に"."があってもおかしい。

そのため、まずipアドレスの先頭三組("0~255".)だけドット付きで判定し、最後の一つだけドットなしで判定させている。

しかしそれなら、間に"."が挟まっても挟まらなくてもどっちでもマッチするように書いたあと、

その後、マッチした文字列の全体のフォーマットが正しいかを判定すればいいのではないか

例えば以下のように。

^(?=^(\d+\.){3}\d+$)((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.?){4}$

0~255を判定する部分が一つになっている。にもかかわらず末尾に"."がついてしまパターンマッチしないのは

(または、途中の組で"."がつかないパターンマッチしないのは)、

先読みで正しいフォーマットだけにマッチするようにして、それ以外の文字列をはじいているからだ。


正規表現が単純に短くなったし、「全体のフォーマットを判定する先読み部」と「各8bit+"\.?"の複雑な判定部分」に分けて描くことができた為、

ある意味、読みやすくすらなったと感じる。

先読みを使うことで、判定対象となる文字列の、「各部分判定の複雑さ」と「全体のフォーマットの判定の複雑さ」を分けることができる。


次に、0~255判定も複雑になっている。

正規表現では"0以上255以下"のような、複数桁にまたがる数の大小判定は出来ない。

例えば"2"と"10"では、数としては当然"10"が大きいが、辞書順で考えれば"2"の方が後に来る。

そして正規表現数字の並びを数として判断しない。

まり辞書順に並んだ文字列の中から、0~255の範囲だけにマッチするような正規表現を書かないといけない。そのため複雑になる。


しかし、0~255のような複数桁にまたがる数の大小判定ができないとしても、

全ての0~255に当てはまる厳密なパターンを書く((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))より、

ある程度絞れる範囲だけ書いた後、除外したいパターンを書いて弾いた方が、読みやすく、意図理解やすくなるのではないか

例えは以下のように。

^(?![3-9]\d\d)(?!2[6-9]\d)(?!25[6789])(?!0\d)\d{1,3}$

まず、上記正規表現末尾の\d{1,3}で、1~3桁のあらゆる数字列にマッチする。

しかし、正規表現左側に書いた4つの先読みで、

・300以上999以下を否定先読みで除外

・260以上299以下を否定先読みで除外

・256以上259以下を否定先読みで除外、また、

数字列の先頭が0になるパターン否定先読みで除外

するように記述されている。

「元々の0~255判定部分より複雑じゃん」と言われればそうなのだが、読み方が異なっている。

元々の正規表現は、

「250以上255以下、または、240以上249以下、または、100以上199以下、または、0~99」という読み方になる。

先読みで除外する方の正規表現は、

「3桁の数字列にマッチする、かつ、300以上999以下を除外、かつ、260以上299以下を除外、かつ、256以上259以下を除外、かつ、先頭が"0"の2ケタ以上の数字列を除外」という読み方になる。

個人的意見になるが、「~、または、~」の連言では、「対象となる範囲の全体感と、除外されるべきパターン」が見えないため、どういった範囲の話をしているのかがピンとこない(場合もある)。

一方、「~、かつ、~~を除外」では、「対象となる範囲の全体感と、除外されるべきパターン」がそのまま書かれているため、対象文字列のどの部分のことを言及しているのか(比較的)理解やすい(、と言いたい)。

ちょっと無理がある言い方か。


つの変更を組み合わせたIPアドレス(IPv4)を判定する正規表現は以下になる。

^(?=^(\d+\.){3}\d+$)((?![3-9]\d\d)(?!2[6-9]\d)(?!25[6789])(?!0\d)(\d{1,3})\.?){4}$

「元々の正規表現より複雑じゃん」と言われればそうなのだが、

"(?=..."や、"(?!..."で始まる部分は、「除外または許可されるべきパターン指定している部分」であり、文字マッチを行っていない。

先読み部分を無視して、文字マッチする部分だけに注目すると、この正規表現マッチしようとしている文字列の全体感が把握できる。

((\d{1,3})\.?){4}

これは、IPアドレス(IPv4)のざっくりとした範囲説明として、意図が十分伝わる記述であると思う。

記事への反応(ブックマークコメント)

ログイン ユーザー登録
ようこそ ゲスト さん