数字・電話番号・IPアドレスをチェックする正規表現
数値入力、電話番号、IPアドレス用のそのまま使えるパターンを、すべてテストしたうえで紹介します。パーサーやライブラリに任せたほうがよい場面もあわせて説明します。
数字、電話番号、IPアドレスはほとんどが数字でできているため、正規表現で簡単に扱えそうに見えます。しかし、それぞれに落とし穴があります。\dの意味は言語によって異なり、電話番号はどんなパターンでも表しきれない国ごとのルールに従い、IPアドレスはすべての部分が範囲内に収まっていなければ有効になりません。以下のパターンは、ほかのエンジン名を挙げていないかぎりNode.jsでテストしたもので、通過・拒否の例はすべて実際に実行した結果です。
数字のみ
| 目的 | パターン | 通過する例 | 拒否される例 |
|---|---|---|---|
| 数字だけ | ^[0-9]+$ | 123、007 | -42、3.14、先頭の空白、空の入力 |
| 整数(符号は任意) | ^[+-]?[0-9]+$ | -42、+42、007 | 3.14、1e3 |
| 整数(先頭ゼロなし) | ^-?(?:0|[1-9][0-9]*)$ | 0、-42 | 007、+42 |
| 小数 | ^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$ | 3.14、-0.5、.5 | 5.、1e3、1,000 |
| JSON形式の数値 | ^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$ | 1.5E-7、-0 | .5、01.5、+42 |
| 3桁区切りのカンマ付き | ^[0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]+)?$ | 1,000、12,345,678.90、100 | 1000、1,00 |
正規表現が確認するのは形であって、値ではありません。1から100のような範囲を扱うなら、正規表現で数字であることを確認してから、コードで数値を比較してください。正規表現を省いて型変換だけに頼るのも禁物です。JavaScriptではNumber('')は0、Number(' 42 ')は42、Number('0x1A')は26、parseInt('12abc')は12になります。
\dは0から9だけにマッチするのか
どのエンジンでもそうとは限りません。^\d+$をアラビア・インド数字の٣٤に対してテストした結果は次のとおりです。
| エンジン | ASCII以外の数字にマッチしたか | 0〜9に限定する方法 |
|---|---|---|
| JavaScript | いいえ。uフラグを付けても同じ | 対処は不要。uフラグ付きの\p{Nd}ならすべての10進数字にマッチ |
| Python 3(str型のパターン) | はい。全角の12にもマッチ | [0-9]またはre.ASCIIフラグ |
| PHP(PCRE) | いいえ。u修飾子を付けるとはい | [0-9] |
| .NET | はい | [0-9]またはRegexOptions.ECMAScript |
| Perl 5 | はい | [0-9]または/a修飾子 |
これが問題になるのは、正規表現を通過した値が、それを別の形で解釈するコードに渡ったときです。Pythonのint('٣٤')は34を返しますが、データベースのカラムや別のサービスはこの値を拒否するかもしれません。ASCIIの数字を意図しているなら[0-9]と書いてください。こちらはどのエンジンでも同じ意味になります。
電話番号
シンプルで国際的な定義がある形式は、E.164だけです。プラス記号、0で始まらない国番号、そして合計15桁以内の数字で構成されます。
^\+[1-9]\d{1,14}$ E.164, plus sign required (best for storage)
^\+?[1-9]\d{1,14}$ plus sign optional
1行目のようにプラス記号を必須にすると、+14155552671と+442071838750は通過し、+0123456789、16桁の番号、国際プレフィックスの00を付けて書いた004914155552671は拒否されます。2行目はプラス記号を省略できる版です。ユーザーは空白、ドット、ハイフン、括弧を入力するので、まずvalue.replace(/[\s().-]/g, '')でそれらを取り除いてください。これで+1 415 555 2671は+14155552671になります。
限界はすぐに見えてきます。このパターンは形しか確認しないため、実在する番号としては短すぎる+12でも通過します。さらに困るのは、+49 (0)30 1234567が整形後に+490301234567となって通過してしまうことです。これは誤った番号です。括弧内の0は国内用のトランクプレフィックスで、海外からかけるときには省かなければなりません。
国内形式のパターンも同様です。米国の番号によく使われる^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$は、括弧が閉じていない(415-555-2671や、実在しえない000-000-0000を受け入れました。桁数、プレフィックス、携帯電話の番号帯は国ごとに異なり、しかも変わっていきます。本格的な検証と整形には、GoogleのlibphonenumberかJavaScript版などの移植版を使ってください。JavaScriptならlibphonenumber-js、Pythonならphonenumbers、PHPならgiggsey/libphonenumber-for-phpです。保存にはE.164形式の結果を使い、番号の所有者確認が重要ならSMSで送るコードで確かめます。
IPv4アドレス
よく見かける簡易パターンの^\d{1,3}(?:\.\d{1,3}){3}$は、256.1.1.1や999.999.999.999を受け入れました。各部分を0〜255に制限するには、範囲を書き出します。
^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$
オクテット部分は0から999までのすべての数値を1桁・2桁・3桁で書いたものに対して、パターン全体はランダムな200,000件のアドレスに対してチェックしました。結果はNodeのnet.isIPv4()と毎回一致しました。0.0.0.0、192.168.1.1、255.255.255.255は通過し、192.168.1、192.168.1.1.1、そして192.168.01.1のような先頭ゼロ付きのものは拒否されます。
先頭ゼロを拒否するのは意図的です。パーサーによっては先頭ゼロ付きの値を8進数として読むからです。Pythonのsocket.inet_aton('010.0.0.1')は、私たちのテストで8.0.0.1というアドレスを返しました。Nodeのnet.isIPv4()、Pythonのipaddress.ip_address()、PHPのfilter_var($ip, FILTER_VALIDATE_IP)も、すべて192.168.01.1を拒否しました。
ログの行からアドレスを探すなら、^と$を外して両側に\bを付け、gフラグを使います。from 10.0.0.5 to 192.168.1.300 via 8.8.8.8に対しては、10.0.0.5と8.8.8.8が返りました。\bがないと、1192.168.1.300xというテキストから192.168.1.30を誤って抽出しました。ただし限界が1つ残ります。1.2.3.4.5では、\bを付けた版でも1.2.3.4にマッチしてしまいます。
IPv6アドレスはパーサーで検証する
IPv6では、連続するゼロのグループ1か所を::で置き換えられ、先頭のゼロは省略でき、::ffff:192.0.2.1のようなIPv4埋め込みやfe80::1%eth0のようなゾーンIDも使えます。これらすべてに対応する正規表現は長く、気づきにくい誤りが入りがちです。パーサーを使うほうが短く、しかも十分にテストされています。
- Node.js:
net.isIPv6(value)、または4・6・0のいずれかを返すnet.isIP(value)。どちらもIPv4埋め込みとゾーンIDを受け入れました。 - Python:
ipaddress.ip_address(value)は、無効な場合にValueErrorを送出します。IPv4埋め込みとゾーンIDを受け入れました。 - PHP:
filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)。IPv4埋め込みは受け入れましたが、fe80::1%eth0は拒否しました。 - ブラウザー:IPアドレスを検証する組み込みの仕組みはないため、
ipaddr.jsなどのライブラリを使います。
それでも正規表現が必要な場合、たとえば正規表現しか受け付けないスキーマや設定ファイルでは、次の408文字のパターンが16進数による表記をカバーします。IPv4埋め込みとゾーンIDは意図的に拒否します。ドットもパーセント記号も含まない、生成・変異させた500,000件の文字列で、すべてnet.isIPv6()と一致しました。
^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$
不正な入力を通してしまう間違い
- アンカーの付け忘れ。
/\d+/.test('abc123')は、どこかに数字があれば見つけてしまうのでtrueになります。検証には^と$が必要です。 - グループの外に書いた選択。
^cat|dog$は「catで始まる、またはdogで終わる」という意味なので、cats and dogs!にマッチしました。^(?:cat|dog)$と書いてください。 mフラグ。これを付けると^と$が各行にマッチするため、/^\d+$/mは2行の文字列"abc\n123"を受け入れました。単一の値を検証するときにmは使わないでください。- 末尾の改行の前でマッチする
$。Python、PHP、.NETでは、^\d+$が"42\n"を受け入れました。Pythonならre.fullmatch()、PHPならD修飾子、PHPと.NETなら\zを使います。JavaScriptはこの値を拒否しました。 test()とgフラグの組み合わせ。g付きの正規表現はlastIndexを覚えているため、同じオブジェクトで/^\d+$/g.test('42')を3回呼ぶと、true、false、trueが返りました。- アンカーなしで順序を誤った選択肢。オクテットを
(?:\d|1\d\d|2[0-4]\d|25[0-5])と書いて255.255.255.255を検索すると、255.255.255.2にしかマッチしませんでした。エンジンは最初に成立した選択肢を採用するからです。長い選択肢を先に書くか、パターンにアンカーを付けてください。
公開する前にパターンをテストする
パターンをスラッシュなしで正規表現テスターに貼り付け、フラグをgmにして、サンプルを1行に1つずつ入力します。有効な値、無効な値、そしてこのページで取り上げた厄介なケースを入れてください。マッチした行が強調表示されます。テスターはブラウザーのJavaScriptエンジンで動くので、パターンをPython、PHP、.NETで動かす場合は、上で説明した\dと$の違いを確認し直してください。メールアドレスを検証したい場合は、メールアドレスの正規表現をご覧ください。