IPv4 地址的正则表达式:正确匹配 0 到 255
IPv4 地址由四个 0 到 255 的数字组成。本文讲解为什么显而易见的 \d{1,3} 写法会放过 999.999.999.999,如何用分支匹配一个八位组,前导零该怎么处理,以及如何在文本中找出地址。

· 阅读约 5 分钟 · 更新于
/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/
抓住
- 0
- 42
- 199
- 255
放过
- 256
- 300
- 07
- 1000
IPv4 地址是用点分隔的四个数字,比如 192.168.1.1。每个数字叫做一个八位组(octet),取值范围是 0 到 255。
这听起来很好匹配,但大多数人写出的第一个正则又短又错。本文从这个正则出发,一步步修正它,最后给出一个在文本中查找地址的正则。每个示例都可以交互,修改正则就能看到各行的结果变化。
01显而易见的写法
四组一到三位的数字,中间用点隔开:
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/应当匹配4/4
- 192.168.1.1
- 8.8.8.8
- 999.999.999.999
- 300.1.2.3
不应匹配2/2
- 1.2.3
- 1.2.3.4.5
它确实能挡住段数不对的输入。如果你扫描的是可信的日志,这也许就够了。但作为校验器,它会接受最大到 999 的任何数字。
重复部分可以写得更短。前三个八位组后面各跟一个点,所以把“八位组加点”编成一组,用 {3} 重复三次:
/^(\d{1,3}\.){3}\d{1,3}$/02别忘了反斜杠
在正则里,单独的 . 表示“任意字符”。漏掉反斜杠,点就不再表示点了:
1234567 最隐蔽。每个 . 都匹配了一个数字,于是正则把它读成 1、3、5、7,而 2、4、6 充当了“点”。
03一个八位组,0 到 255
正则表达式不知道一个数字有多大,它只看得到字符。所以“0 到 255”必须拆成文本规则,分成几个各自形状简单的区间:
| 区间 | 正则 | 含义 |
|---|---|---|
| 250–255 | 25[0-5] |
25,然后是 0 到 5 |
| 200–249 | 2[0-4]\d |
2,然后是 0 到 4,再跟任意一位数字 |
| 100–199 | 1\d\d |
1,然后是任意两位数字 |
| 0–99 | [1-9]?\d |
一个可选的 1 到 9,然后是任意一位数字 |
用 | 把它们连起来,再包进一个分组:
04前导零
看最后一个分支:是 [1-9]?\d,而不是 \d{1,2}。正是它拒绝了 07 和 010。两位数的八位组必须以 1 到 9 开头。
这比看起来更重要。有些工具会把带前导零的数字当成八进制。Python 的 socket.inet_aton 遵循旧的 C 网络规则,会把 010.0.0.1 变成 8.0.0.1,因为八进制的 10 就是 8。如果系统的一部分把 010 读作十,另一部分读作八,它们说的就不是同一个地址了。拒绝前导零,就能彻底避开这个问题。
05完整的正则
把八位组分组放到原来 \d{1,3} 的位置:
/^((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)$/应当匹配4/4
- 0.0.0.0
- 10.0.0.1
- 192.168.1.1
- 255.255.255.0
不应匹配6/6
- 256.1.1.1
- 192.168.1.300
- 999.999.999.999
- 010.0.0.1
- 1.2.3
- 1.2.3.4.5
它很长,但长是因为八位组写了两遍,而不是因为它复杂。下面对比一下,用 {3} 比把八位组写四遍省了多少:
棋盘0.0.0.0 · 10.0.0.1 · 192.168.1.1 · 255.255.255.0 · 8.8.4.4 vs 256.1.1.1 · 10.0.0 · 1.2.3.4.5 · 192.168.1.300 · 010.0.0.1
- 全部写出能解出140
/^(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)\.(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)$/ - 用分组能解出75
/^((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)$/
06在文本中查找地址
^ 和 $ 只在整个输入就是一个地址时才管用。要在日志行里找地址,通常的做法是把它们换成单词边界 \b。这几乎可行:
/\b((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)\b/应当匹配2/2
- server at 10.0.0.1.
- version 1.2.3.4.5
不应匹配1/1
- error 999.1.1.1
你真正想要的是“前面紧挨着的不是数字或点,后面也没有更多地址内容”。这正是环视的用武之地:
/(?<![\d.])((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)(?!\.?\d)/应当匹配2/2
- server at 10.0.0.1.
- ping 8.8.8.8 now
不应匹配3/3
- version 1.2.3.4.5
- error 999.1.1.1
- build 1.2.3
这个正则在 Python 的 re 里也能用。Python 只允许固定宽度的后行断言,而 (?<![\d.]) 每次正好检查一个字符。
07或者用解析器
如果你是在代码里校验输入,标准库已经帮你做好了。在 Python 中:
import ipaddress
ipaddress.ip_address("192.168.1.1") # IPv4Address('192.168.1.1')
ipaddress.ip_address("192.168.1.1").is_private # True
ipaddress.ip_address("256.1.1.1") # ValueError
从 Python 3.9.5 开始,ipaddress 也会拒绝 010.0.0.1 这样的前导零。在 Node 中,net.isIPv4("010.0.0.1") 同样是 false,而且在本文的每个示例上,它的结果都和上面的完整正则一致。解析器还能处理 IPv6,而 IPv6 的正则要大得多。
08试一试
整个正则的核心是八位组。你能凭记忆把它写出来吗?
抓住 0 到 255 的数字,放过其余的。
目标 · 抓住 0/10
- 0目标
- 7目标
- 42目标
- 99目标
- 100目标
- 199目标
- 200目标
- 249目标
- 250目标
- 255目标
无辜词 · 误抓 0
- 256无辜
- 260无辜
- 300无辜
- 999无辜
- 1000无辜
- 07无辜
- 007无辜
输入正则——棋盘会实时更新。
09检查清单
\d{1,3}允许 0 到 999。匹配一个八位组请用(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)。- 每个点都要转义:
\.。 - 用分组来重复:
(octet\.){3}octet。 - 小数字用
[1-9]?\d,这样就能拒绝前导零。 - 在文本中,用
(?<![\d.])和(?!\.?\d)代替\b。 - 在代码中,优先使用
ipaddress(Python)或net.isIPv4(Node)。
10继续练习
- 指南中的“分支”章节讲解了构成八位组的
|。 - 试试今天的狩猎,或者在往期中浏览过去的棋盘。