日常用的 IPv4 正则
错误的 IPv4 正则到处都是,因为它短。正确的那条长一点,因为它真的把每个数字框住了。下面这条是我信得过的单地址版本:
^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$
四段,每段用点分开,每段都强制落在 0 到 255。不多不少。
为什么 \d{1,3} 是错的
最诱人的捷径是 \d{1,3}(\.\d{1,3}){3}。它看着对,错得狠。一个一到三位数字的单段会接受 999,于是这条正则会乐呵呵地匹配 999.999.999.999,而那根本不是 IP 地址。更糟的是,它接受 256.1.1.1,尽管 256 已经超范围。一旦你在乎这个值是不是真地址,这条正则就在骗你。
| 正则 | 接受 999.999.999.999 | 接受 256.1.1.1 | 结论 |
|---|---|---|---|
\d{1,3}(\.\d{1,3}){3} | 是 | 是 | 错 |
[0-9]{1,3}(\.[0-9]{1,3}){3} | 是 | 是 | 错 |
| 上面那条按段框定的 | 否 | 否 | 对 |
拆开看段的正则
每段由几个互斥的备选拼成,按顺序尝试:
25[0-5]覆盖 250 到 255。2[0-4]\d覆盖 200 到 249(十位是 0-4,个位随意)。1\d\d覆盖 100 到 199。[1-9]?\d覆盖 0 到 99,它要求首位可省但非零,于是既放0和7,又在该较真的地方挡掉多余的前导零。
备选从高到低排,引擎先匹配最大的合法数。四段是"一组加前导点"重复三次,两头再包住,不让任何多余字符混进来。
前导零的坑
哪怕数值对了,还有一层语义坑:010.1.1.1。在不少 C 和 Unix 解析器里,前导零表示八进制,所以 010 是十进制的 8,不是 10。上面那条严格正则会拒绝 010,因为段的备选从 [1-9]?\d 起,多位数字的前导零被禁掉。如果你的下游用了 C 风格解析器,这种拒绝正是你想要的。要是你放行前导零,得写清楚——同一串在不同系统里是不同的数。
这个正则匹配了什么、漏了什么
| 输入 | 期望 | 说明 |
|---|---|---|
192.168.1.1 | 接受 | 私有段,教科书样例 |
127.0.0.1 | 接受 | 回环地址 |
256.1.1.1 | 拒绝 | 首段超范围 |
999.999.999.999 | 拒绝 | 四段全超范围 |
01.2.3.4 | 严格版拒绝 | 前导零被视为八进制风险 |
CIDR 后缀与私有段
单地址常常不够,你可能需要 10.0.0.0/8 这种网段。下面这个变体加了可选前缀长度:
^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}(/(3[0-2]|[12]?\d))?$
末尾的 (/...)? 允许一个斜杠加 0 到 32 的数。那描述的是一个网络,不是一台主机,所以别喂给期望单地址的代码。私有段检查要在解析后拿整数去比对 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,而不是用正则。
为什么范围校验该写在代码里
核心论点:用正把 0-255 表达出来既丑又脆,而且它依然只证明这串文字"像地址"。老实做法是拆开字符串、拿整数比:
def is_ipv4(s):
parts = s.split(".")
if len(parts) != 4:
return False
for p in parts:
if not p.isdigit() or not 0 <= int(p) <= 255:
return False
return True
这更好懂,一次比较就拒掉 256 和 999,而且如果你显式禁掉前导零,还能躲开八进制歧义。正则适合快速 grep 或表单提示;凡是你要拿它分支判断的,就解析它。
另外,整数比较还能顺手做私网判断:把四段拼成 32 位整数后,和 10.0.0.0/8 之类的段做位与,一眼就知道是不是内网。这种事正则连边都摸不着,却是安全代码天天要干的活。
怎么测你的 IPv4 正则
把表中的样例跑一遍,再补上 0.0.0.0、255.255.255.255 和只有三段的 1.2.3。如果你的正则会放过 256.1.1.1、999.0.0.1 或三段串,先修好段的备选再信它。而如果这个值要驱动网络决策,就转成整数去比;让正则当门房,别当法官。多写一行整数比较,往往比多写十行正则更值当。
八进制、十进制,以及你的解析器怎么假设
同样四个数字,在不同读者眼里意思可能不同。严格正则会拒绝 010.0.0.1 来躲开八进制坑;但如果你放宽它,就得知道代价:C 库可能把 010 读成 8,而像 Python 这样的语言会读成 10,浏览器则可能直接拒掉。这些正则都看不见。稳妥的设计是:在校验里禁掉前导零,然后用显式的十进制去转换每一段。这样在到达系统调用之前就消除了歧义,让你接受的每个地址都只有一个可预期的含义。
顺带一提,有些老系统还认 0x 开头的十六进制,0x7f.0.0.1 在它们看来就是 127.0.0.1。正则更管不到这种写法。所以一句话收尾:带数值含义的地址,交给会算数的代码,别让只会数字符的正则拍板。