常用正则内含 1 场可玩狩猎

IPv4 地址的正则表达式:正确匹配 0 到 255

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

Regex Hunter 标志

· 阅读约 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。

这听起来很好匹配,但大多数人写出的第一个正则又短又错。本文从这个正则出发,一步步修正它,最后给出一个在文本中查找地址的正则。每个示例都可以交互,修改正则就能看到各行的结果变化。

快速答案JavaScript
/^((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 到 255 之间的数字,不能有前导零。

匹配

  • 192.168.1.1
  • 10.0.0.1
  • 255.255.255.0

不匹配

  • 256.1.1.1
  • 010.0.0.1
  • 1.2.3

01显而易见的写法

四组一到三位的数字,中间用点隔开:

匹配6/6 符合预期
/^\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
在沙盒里试试
形状没错,但 \d{1,3} 的意思是“0 到 999 之间的任意数字”。最后两行并不是真正的地址。

它确实能挡住段数不对的输入。如果你扫描的是可信的日志,这也许就够了。但作为校验器,它会接受最大到 999 的任何数字。

重复部分可以写得更短。前三个八位组后面各跟一个点,所以把“八位组加点”编成一组,用 {3} 重复三次:

正则23 字符
/^(\d{1,3}\.){3}\d{1,3}$/
在沙盒里试试
和上面的正则完全相同。分组 (\d{1,3}\.) 出现三次,最后再跟一个后面没有点的八位组。

02别忘了反斜杠

在正则里,单独的 . 表示“任意字符”。漏掉反斜杠,点就不再表示点了:

匹配3/3 符合预期
/^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$/
  • 1.2.3.4
  • 1a2b3c4
  • 1234567
在沙盒里试试
最后两行都匹配了。未转义的点会接受字母,甚至数字。

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,然后是任意一位数字

用 | 把它们连起来,再包进一个分组:

匹配10/10 符合预期
/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/

应当匹配6/6

  • 0
  • 9
  • 42
  • 199
  • 249
  • 255

不应匹配4/4

  • 256
  • 300
  • 07
  • 1000
在沙盒里试试
每个分支负责一个区间。256 四个分支都不满足:25[0-5] 最大到 5,2[0-4]\d 要求中间是 0 到 4,1\d\d 要求以 1 开头,[1-9]?\d 最多只有两位。

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} 的位置:

匹配10/10 符合预期
/^((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
在沙盒里试试
三组“八位组加点”,再跟最后一个八位组。每个八位组都必须在 0 到 255 之间,且没有前导零。

它很长,但长是因为八位组写了两遍,而不是因为它复杂。下面对比一下,用 {3} 比把八位组写四遍省了多少:

对比缩短 46%

棋盘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

  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)$/
  2. 用分组能解出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)$/
试试短的那个
两个版本都能解出这个棋盘。用 {3} 分组的版本是 75 个字符,全部写出来的版本是 140 个。

06在文本中查找地址

^ 和 $ 只在整个输入就是一个地址时才管用。要在日志行里找地址,通常的做法是把它们换成单词边界 \b。这几乎可行:

匹配3/3 符合预期
/\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
在沙盒里试试
第二行是版本号,不是地址,但它还是匹配了。点不是单词字符,所以 4 和 .5 之间有一个边界。

你真正想要的是“前面紧挨着的不是数字或点,后面也没有更多地址内容”。这正是环视的用武之地:

匹配5/5 符合预期
/(?<![\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
在沙盒里试试
(?<![\d.]) 表示前面不是数字或点。(?!\.?\d) 表示后面不是数字,也不是“点加数字”。句末的句点仍然没问题。

这个正则在 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试一试

整个正则的核心是八位组。你能凭记忆把它写出来吗?

可玩狩猎标准杆 35

抓住 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继续练习