IPv4 地址正则表达式
IPv4 地址由四个 0-255 之间的十进制数组成,用点号()分隔,168.1.1。
推荐的正则表达式
这是一个既精确又相对易读的版本,能够严格匹配 0-255 的范围。
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
正则表达式解析
让我们把这个复杂的表达式拆解开来:
^和 :分别表示字符串的开始和结束,这确保了整个输入字符串都必须是一个合法的 IP 地址,而不是包含其他字符。- 这是一个非捕获分组,它将括号内的内容组合成一个整体,但不会像 那样创建一个反向引用,这更高效。
(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):这是匹配 0-255 这个范围的核心逻辑,它由三个部分通过 (或) 连接:25[0-5]:匹配250到255的数字。2[0-4][0-9]:匹配200到249的数字。[01]?[0-9][0-9]?:匹配0到199的数字。[01]?:匹配 0 个或 1 个0或1。[0-9]:匹配一个0到9的数字。[0-9]?:匹配 0 个或 1 个0到9的数字。- 组合起来可以匹配
0到99([01]?[0-9]?),100到199(1[0-9][0-9]), 以及单个数字如5([0-9])。
\.:匹配一个字面量点号 ,需要反斜杠\来转义,因为在正则表达式中 是一个特殊字符(匹配任意字符)。{3}:表示前面的分组(即xxx.)必须出现恰好 3 次。
所以整个表达式的意思是:从字符串开头开始,匹配一个 0-255 的数字,后跟一个点号,重复三次;最后再匹配一个 0-255 的数字,直到字符串结束。
Java 代码示例
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class Ipv4Validator {
// IPv4 正则表达式
private static final String IPV4_REGEX =
"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
private static final Pattern IPV4_PATTERN = Pattern.compile(IPV4_REGEX);
public static boolean isValidIPv4(String ip) {
if (ip == null) {
return false;
}
Matcher matcher = IPV4_PATTERN.matcher(ip);
return matcher.matches();
}
public static void main(String[] args) {
String[] validIps = {"192.168.1.1", "10.0.0.1", "255.255.255.255", "0.0.0.0", "127.0.0.1"};
String[] invalidIps = {"256.1.2.3", "192.168.01.1", "192.168.1", "192.168.1.1.1", "a.b.c.d"};
System.out.println("--- Valid IPv4 Addresses ---");
for (String ip : validIps) {
System.out.println(ip + ": " + isValidIPv4(ip));
}
System.out.println("\n--- Invalid IPv4 Addresses ---");
for (String ip : invalidIps) {
System.out.println(ip + ": " + isValidIPv4(ip));
}
}
}
IPv6 地址正则表达式
IPv6 地址比 IPv4 复杂得多,它由 8 组 4 位的十六进制数组成,组之间用冒号()分隔。2001:0db8:85a3:0000:0000:8a2e:0370:7334。
为了简化,IPv6 允许一些压缩规则:
- 可以省略每组前导的零,
2001:db8:85a3:0:0:8a2e:370:7334。 - 可以使用 来表示一组或多组连续的零,但 在一个地址中只能出现一次,
2001:db8:85a3::8a2e:370:7334。
推荐的正则表达式
这个正则表达式考虑了上述所有规则,包括 的压缩。
^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?$
正则表达式解析
这个表达式由两个主要部分通过 (或) 连接,分别处理标准格式和带 的压缩格式。
第一部分:标准格式
^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
[0-9a-fA-F]{1,4}:匹配 1 到 4 位十六进制字符(数字 0-9,以及字母 a-f,不区分大小写)。([0-9a-fA-F]{1,4}:){7}:匹配由 "1-4位十六进制字符 + 冒号" 构成的组合,共 7 次。- 最后再跟一个
[0-9a-fA-F]{1,4}。 - 这正好匹配了 8 组十六进制数,前 7 组后面带冒号。
第二部分:压缩格式(含 )
^(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?$
这部分比较复杂,它允许 出现在地址的开头、中间或结尾。
(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?:这是 左边的部分。- 表示整个分组是可选的(可以匹配 0 次或 1 次)。
([0-9a-fA-F]{1,4}:){0,6}:匹配 0 到 6 次 "1-4位十六进制字符 + 冒号"。[0-9a-fA-F]{1,4}:最后再跟一个 1-4 位十六进制字符。- 所以这个分组可以匹配像
2001:db8:这样的前缀。
- 匹配字面量的双冒号。
(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?:这是 右边的部分,与左边部分的结构完全相同,可以匹配像85a3:0000这样的后缀。
组合起来, 可以代表:
- 开头的零:
:1(相当于0000:0000:0000:0000:0000:0000:0000:0001) - 中间的零:
2001:db8::1(相当于2001:0db8:0000:0000:0000:0000:0000:0001) - 结尾的零:
2001:db8:0:0:0:0:0::(相当于2001:0db8:0000:0000:0000:0000:0000:0000)
Java 代码示例
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class Ipv6Validator {
// IPv6 正则表达式
private static final String IPV6_REGEX =
"^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?$";
private static final Pattern IPV6_PATTERN = Pattern.compile(IPV6_REGEX);
public static boolean isValidIPv6(String ip) {
if (ip == null) {
return false;
}
Matcher matcher = IPV6_PATTERN.matcher(ip);
return matcher.matches();
}
public static void main(String[] args) {
String[] validIps = {
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
"2001:db8:85a3:0:0:8a2e:370:7334",
"2001:db8:85a3::8a2e:370:7334",
"2001:db8::1",
"::1",
"fe80::1%eth0" // 注意:这个包含作用域ID的正则不匹配,需要扩展
};
String[] invalidIps = {
"2001:0db8:85a3:0000:0000:8a2e:0370:7334:1234", // 组数太多
"2001:0db8:85a3:::8a2e:370:7334", // 多个::
"2001:0db8:85a3::8a2e::370:7334", // 多个::
"2001:0db8:85a3:0:0:8a2e:370:733g", // 包含非法字符
"192.168.1.1" // IPv4地址
};
System.out.println("--- Valid IPv6 Addresses ---");
for (String ip : validIps) {
System.out.println(ip + ": " + isValidIPv6(ip));
}
System.out.println("\n--- Invalid IPv6 Addresses ---");
for (String ip : invalidIps) {
System.out.println(ip + ": " + isValidIPv6(ip));
}
}
}
重要提示与替代方案
性能考虑
复杂的正则表达式(尤其是 IPv6 的)在处理大量数据或高频调用时可能会有性能开销,如果只是做简单的校验,并且性能是关键因素,可以考虑将字符串按分隔符( 或 )拆分,然后逐部分进行逻辑判断,这种方式通常比正则表达式更快。
更健壮的 IPv6 正则
上面的 IPv6 正则表达式是常见且实用的,但它无法匹配包含“作用域ID”的 IPv6 地址,fe80::1%eth0,一个更全面的正则表达式会非常复杂,对于大多数场景,上述版本已经足够。
最佳实践:使用 Java 内置 API
对于 IP 地址的验证,最推荐、最健壮、最简单的方式是使用 Java 标准库中的 InetAddress 类,它已经内置了所有复杂的解析逻辑,并且能正确处理各种边界情况。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressValidator {
public static boolean isValidIP(String ip) {
try {
// 尝试将字符串解析为 InetAddress
InetAddress.getByName(ip);
// 如果没有抛出异常,说明 IP 地址格式正确
return true;
} catch (UnknownHostException e) {
// 如果抛出 UnknownHostException,说明 IP 地址格式无效
return false;
}
}
public static void main(String[] args) {
String testIp = "2001:0db8:85a3::8a2e:0370:7334";
System.out.println(testIp + " is valid: " + isValidIP(testIp)); // true
String testIp2 = "256.1.2.3";
System.out.println(testIp2 + " is valid: " + isValidIP(testIp2)); // false
String testIp3 = "localhost";
System.out.println(testIp3 + " is valid: " + isValidIP(testIp3)); // true (getByName可以解析主机名)
// 如果只想验证 IP 格式,不解析主机名,需要额外处理
// 检查字符串中是否包含字母
}
}
InetAddress.getByName() 的优点:
- 准确性高:由 Java 官方实现,符合所有 RFC 标准。
- 代码简洁:无需编写和维护复杂的正则表达式。
- 功能强大:不仅能验证 IPv4 和 IPv6,还能处理主机名。
缺点:
- 它会尝试进行网络解析(如果传入的是主机名),可能会有轻微的性能开销(虽然通常可以忽略不计)。
- 它会将
localhost和0.0.1视为有效地址,如果你只想验证纯 IP 格式,需要自己加上判断(检查字符串中是否包含字母)。
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 正则表达式 | 纯字符串匹配,无网络开销。 逻辑完全在代码控制中。 |
正则表达式复杂,难以编写和维护。 可能有性能问题。 难以覆盖所有边缘情况(如 IPv6 作用域 ID)。 |
需要严格的、高性能的、无副作用的纯格式校验。 不能使用网络 API 的受限环境。 |
InetAddress API |
最准确、最健壮,符合官方标准。 代码极其简单,易于维护。 自动处理 IPv4 和 IPv6。 |
可能会尝试解析主机名,有极小的网络开销。 可能会将主机名(如 localhost)视为有效。 |
绝大多数 Java 应用场景下的首选方案,当需要验证一个字符串是否是有效的 IP 地址时,这应该是你的第一选择。 |
