Raymond Chen 2006年05月22日
简要
这篇文章发表于2006年5月22日,讨论了如何编写一个匹配IPv4点分十进制地址的正则表达式。
正文
编写一个匹配IPv4点分十进制地址的正则表达式,其难易程度取决于你想要完成的工作有多好。
实际上,为了简化问题,我们只匹配十进制点分表示法,忽略十六进制变体以及非点分的变体。
为了这次讨论,我将限制自己使用Perl、JScript和.NET Framework共有的正则表达式语言的常见子集,并假设是ECMA模式,其中\d
仅匹配字符0到9。(在.NET Framework的默认情况下,\d
匹配任何十进制数字,不仅仅是0到9。)
最简单的版本就是接受由点分隔的四个十进制数字的字符串。
/^\d+\.\d+\.\d+\.\d+$/
这样处理很不错,但它错误地接受了像“448.90210.0.65535”这样的字符串。一个正确的十进制点分地址没有任何值大于255。
但是编写一个匹配0到255的整数的正则表达式是困难的,因为正则表达式不理解算术;它们纯粹是文本操作。因此,你必须用纯文本方式描述0到255之间的整数。
- 任何单个数字都是有效的(代表0到9)。
- 任何非零数字后面跟着另一个数字都是有效的(代表10到99)。
- “1”后面跟着两个数字是有效的(100到199)。
- “2”后面跟着“0”到“4”再后面跟着另一个数字是有效的(200到249)。
- “25”后面跟着“0”到“5”是有效的(250到255)。
鉴于对0到255之间整数的这种文本分解,你的第一次尝试可能是这样的:
/^\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]$/
通过认识到上述前两条规则可以合并为:
- 任何数字,前面可选地跟着一个非零数字,都是有效的。
得到:
/^[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]$/
现在我们只需要在四个部分之间加上点,重复四次:
/^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/
恭喜,我们刚刚将点分十进制表示法的简单描述转换成了一个基本上是不可读的庞大正则表达式。想象一下,如果你在维护一个程序,偶然遇到这个正则表达式,你需要多久才能弄清楚它的作用?
哦,它可能还不对,因为一些解析器接受每个十进制值前的前导零而不会影响它。(例如,127.0.0.001与127.0.0.1相同。另一方面,一些解析器将前导零视为八进制前缀。)更新我们的正则表达式以接受前导小数零意味着我们现在有:
/^0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/
这就是为什么我既爱又恨正则表达式。它们是表达简单模式的绝佳方式。它们也是表达复杂模式的可怕方式。正则表达式可能是世界上最受欢迎的一次性编写语言。
啊哈,但你看,所有这些深入正则表达式的时间都是一个错误。因为我们没有弄清楚实际的问题是什么。
这是一个人“解决”了他们问题的一半,然后寻求另一半的帮助的情况:“我有一段字符串,我想检查它是否是点分十进制的IPv4地址。我知道,我会写一个正则表达式!嘿,有人能帮我写这个正则表达式吗?”
真正的问题不是“我如何编写一个正则表达式来识别点分十进制IPv4地址。”
真正的问题只是“我如何识别点分十进制IPv4地址。”
有了这个更广泛的目标,你意识到将自己限制在正则表达式上只会让问题变得更难。
function isDottedIPv4(s)
{
var match = s.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
return match != null &&
match[1] <= 255 && match[2] <= 255 &&
match[3] <= 255 && match[4] <= 255;
}
WScript.StdOut.WriteLine(isDottedIPv4("127.0.0.001"));
WScript.StdOut.WriteLine(isDottedIPv4("448.90210.0.65535"));
WScript.StdOut.WriteLine(isDottedIPv4("microsoft.com"));
这只是一个简单的点分十进制IPv4地址。如果你想解析电子邮件地址,那就更糟糕了。
不要让你的正则表达式做它们不擅长的事情。如果你想匹配一个简单模式,那么就匹配一个简单模式。如果你想做数学运算,那么就做数学运算。正如评论者Maurits所说,“诀窍不在于花时间开发一个组合的锤子/螺丝刀,而是只使用锤子和螺丝刀。”