IPアドレスにマッチする正規表現について考えてみる

<h2>はじめに</h2>
　PythonでIPアドレスを扱うことが時々あって、IPv4アドレスにマッチする正規表現をいつもググって以下のサイトなどを参考にさせていただいて盲目的に使うのですが、自分がちゃんと理解した上でQiitaに記載しておくと何かと便利だと思って、その理解のためのレポートを書くつもりで書いておきます。<br><br>

参考サイト:

https://livra.geolocation.co.jp/iplearning/1803/

<h2>1. 結論</h2>
結論から言うとこれでうまく動作する(Pythonで扱う場合)。

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

例えば、あるリストの要素にIPv4とIPv6の文字列が混在していて、そこからIPv4だけを抜き出したいときにこんな感じで使える。

```python
import re

#IPv4にマッチする正規表現
ptn = '^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'

#IPv4とIpv6が混在したリスト
ip_lst = ['192.168.1.1', '::ffff:192.168.1.1']

#IPv4だけのリスト
ipv4_lst = list(filter(lambda x: re.search(ptn, x), ip_lst))

```

では、この正規表現の意味の解釈を以下につぶさに記載する。

<h2>2.そもそもIPv4アドレス表記に関してRFCはどう定義してるか</h2>
　IPv4表記のRFCの定義なんて、「ちょっと検索すれば出てくるでしょ」とたかを括ってたらですよ、それを見つけられないばかりか、コミュニティサイトで「そんなのない」って言ってる人を見かけた...<br><br>

Leading zeros in IPv4 address; is that a no-no by convention or standard?(Superuser)

https://superuser.com/questions/929153/leading-zeros-in-ipv4-address-is-that-a-no-no-by-convention-or-standard

でも、このQAには「RFCに採用されずに、期限が切れたIETFのドラフト」へのリンクがあったので、これを参考にすることにする。

Textual Representation of IPv4 and IPv6 Addresses(IETF)

https://datatracker.ietf.org/doc/html/draft-main-ipaddr-text-rep-02#section-3.1

以下、抜粋。

原文:
>3.1 IPv4 Dotted Octet Format
>
>   A 32-bit IPv4 address is divided into four octets.  Each octet is
   represented numerically in decimal, using the minimum possible number
   of digits (leading zeroes are not used, except in the case of 0
   itself).  The four encoded octets are given most-significant first,
   separated by period characters

Google翻訳 + α:
>3.1 IPv4 ドット付きオクテット形式
>
>　32ビットのIPv4アドレスは4つのオクテットに分割されます。各オクテットは可能な限り最小桁数を使用して、10進数で表されます(それ自体が0の時以外は前にゼロを付与しません)。
 エンコードされた4つのオクテットは、ピリオドで区切られた最上位のものが最初に置かれます。
>
>
>        IPv4address = d8 "." d8 "." d8 "." d8
>
>        d8          = DIGIT               ; 0-9
>                    / %x31-39 DIGIT       ; 10-99
>                    / "1" 2DIGIT          ; 100-199
>                    / "2" %x30-34 DIGIT   ; 200-249
>                    / "25" %x30-35        ; 250-255

このドラフトによればオクテット表記は表1の通りとなる。
(オクテット表記はすごく簡単に言うと、「前ゼロを含まない0-255までの10進数の数値」ということではあるけれど...)

<table>
    <caption>表1</caption>
    <theader>
      <tr>
        <th>
            説明
        </th>
        <th>
            範囲
        </th>
        </tr>
    </theader>
    <tbody>
    <tr>
        <td>
            Digit
        </td>
        <td>
            0-9
        </td>
    </tr>
    <tr>
        <td>
            1-9(0x31-39)とDigit
        </td>
        <td>
            10-99
        </td>
    </tr>
    <tr>
        <td>
            "1"と2Digit
        </td>
        <td>
            100-199
        </td>
    </tr>
    <tr>
        <td>
            "2"と0-4(0x30-34)とDigit
        </td>
        <td>
            200-249
        </td>
    </tr>
    <tr>
        <td>
            "25"と0-5(0x30-35)
        </td>
        <td>
            250-255
        </td>
    </tr>
    </tbody>
</table>

<h2>3.正規表現をひとつずつ見ていく</h2>
　権威がありそうでないIPv4アドレス表記ルールがわかったところで、ここからは冒頭の正規表現を分解しながらひとつずつ見ていく。<br>

<h3>3.1 オクテットのパターン</h3>
　まずは最初のこの部分の

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


これ<code>[1-9]?[0-9]</code>。

　・<code>[1-9]</code>は123456789のいずれか1文字にマッチ
　・<code>?</code>は前の文字(<code>[1-9]</code>)がないか、1文字だけある
　・<code>[0-9]</code>は0123456789のいずれか1文字にマッチ

つまり、<code>[1-9]?[0-9]</code>の意味は表1のこの2行を表している。

<table>
    <theader>
      <tr>
        <th>
            説明
        </th>
        <th>
            範囲
        </th>
        </tr>
    </theader>
    <tbody>
    <tr>
        <td>
            Digit
        </td>
        <td>
            0-9
        </td>
    </tr>
    <tr>
        <td>
            1-9(0x31-39)とDigit
        </td>
        <td>
            10-99
        </td>
    </tr>
    </tbody>
 </table>


　次にこの部分の
<code>^(([1-9]?[0-9]|**1[0-9]{2}**|2[0-4][0-9]|25[0-5])\\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])¥</code>

これ<code>1[0-9]{2}</code>。

　・<code>1</code>は1にマッチ
　・<code>[0-9]{2}</code>は0123456789のいずれかが2文字ある場合にマッチ

つまり、<code>1[0-9]{2}</code>の意味は表1のこの行を表している。

<table>
    <theader>
      <tr>
        <th>
            説明
        </th>
        <th>
            範囲
        </th>
        </tr>
    </theader>
    <tbody>
    <tr>
        <td>
            "1"と2Digit
        </td>
        <td>
            100-199
        </td>
    </tr>
    </tbody>
</table>

　今度はこの部分の
<code>^(([1-9]?[0-9]|1[0-9]{2}|**2[0-4][0-9]**|25[0-5])\\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$</code>

これ<code>2[0-4][0-9]</code>。

　・<code>2</code>は2にマッチ
　・<code>[0-4]</code>は01234のいずれか1文字にマッチ
　・<code>[0-9]</code>は01234567899のいずれか1文字にマッチ


つまり、<code>2[0-4][0-9]</code>の意味は表1のこの行を表している。

<table>
    <theader>
      <tr>
        <th>
            説明
        </th>
        <th>
            範囲
        </th>
        </tr>
    </theader>
    <tbody>
    <tr>
        <td>
            "2"と0-4(0x30-34)とDigit
        </td>
        <td>
            200-249
        </td>
    </tr>
    </tbody>
</table>

　オクテットの表現としては最後のこの部分の
<code>^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|**25[0-5]**)\\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$</code>

これ<code>25[0-5]</code>。

　・<code>25</code>は25にマッチ
　・<code>[0-5]</code>は012345のいずれか1文字にマッチ

つまり、<code>25[0-5]</code>の意味は表1のこの行を表している。

<table>
        <theader>
      <tr>
        <th>
            説明
        </th>
        <th>
            範囲
        </th>
        </tr>
    </theader>
    <tbody>
        <td>
            "25"と0-5(0x30-35)
        </td>
        <td>
            250-255
        </td>
    </tbody>
</table>

　ここまででオクテット表記は全て網羅したので、表1に正規表現を追記して整理してみる。

<table>
    <caption>表2</caption>
    <theader>
      <tr>
        <th>
            説明
        </th>
        <th>
            範囲
        </th>
        <th>
            正規表現
        </th>
      </tr>
    <tbody>
    <tr>
        <td>
            Digit
        </td>
        <td>
            0-9
        </td>
        <td rowspan="2">
            [1-9]?[0-9]
        </td>
    </tr>
    <tr>
        <td>
            1-9(0x31-39)とDigit
        </td>
        <td>
            10-99
        </td>
    </tr>
    <tr>
        <td>
            "1"と2Digit
        </td>
        <td>
            100-199
        </td>
        <td>
            1[0-9]{2}
        </td>
    </tr>
    <tr>
        <td>
            "2"と0-4(0x30-34)とDigit
        </td>
        <td>
            200-249
        </td>
        <td>
            2[0-4][0-9]
        </td>
    </tr>
    <tr>
        <td>
            "25"と0-5(0x30-35)
        </td>
        <td>
            250-255
        </td>
        <td>
            25[0-5]
        </td>
    </tr>
    </tbody>
</table>
<h3>3.2 オクテットとドット、そして繰り返し</h3>

　オクテットのパターンがわかったところで、この部分の
<code>^(**([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])**\\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$</code>

これに注目していきたい。
<code>([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])</code>

 よく見てみると、今まで出てきたオクテットのパターンが<code>()</code>(カッコ)で括られ、<code>|</code>(パイプ)で区切られていることがわかる。
正規表現の代わりに表2の「範囲」で表してみると、このようになっている。

<code>(0~99|100~199|200~249|250~255)</code>

<code>|</code>(パイプ)は<code>()</code>(カッコ)でグルーピングして並列に記載されている文字列のいずれかにマッチするので、<code>0~99</code>, <code>100~199</code>, <code>200~249</code>, <code>250~255</code>のいずれかにマッチする。

そして、全体を眺めてみると、

　<code>^(A\\.){3}A$</code>
　ただし、A = ([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])

という構造になっていることがわかる。これを見ていくと。

　・<code>\\.</code>は<code>.</code>(ドット)(<code>\\</code>(バックスラッシュ)でエスケープ)にマッチ
　・<code>{3}</code>は直前のグループ、<code>(A\\.)</code>が3回続く文字列にマッチ
　・それらにAを加えた文字列にマッチ
　・行の先頭と最後を示すメタ文字<code>^</code>と<code>$</code>で挟まれている上記にマッチ

ここで、エスケープ文字の<code>\\</code>(バックスラッシュ)を外し、繰り返しの<code>{3}</code>を分解して、行の先頭と最後を示す<code>^</code>と<code>$</code>を外して考えると、

　<code>A.A.A.A</code>

という見慣れた構造になっていることがわかり、冒頭のこの正規表現がIPv4アドレスにマッチすることがわかった(気がする)。

<pre><code>^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|</code></pre>


<h2>まとめ</h2>
　ここまで読み進めてくれた方は余程の暇人かIPv4アドレスがマッチする正規表現を本当に知りたかった方だと思いますが、いずれにしても感謝いたします。そんな方にささやかながら感謝の意味を込めて一つプレゼントを贈ります。<br><br>

はい。どうぞ。

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

これまでの正規表現とそっくりだと思いますが、CIDR表現のネットワークアドレス(例:192.168.1.0/24)とマッチする正規表現です。

違いは後ろに追記された<code>/([1-2]?[0-9]|3[0-2])</code>です。
ここまで、読んだ方にとって、これを理解するのは造作もないことだと思うので、解説は控えておきます。

:::note warn
PHPなどで正規表現のデリミタに<code>/</code>(スラッシュ)を使用する場合は<code>/</code>を<code>\\</code>(バックスラッシュ)でエスケープしてください。
:::