IT技术博客大学习 共学习 共进步

正则表达式解题经验谈

乱象,印迹 2010-05-29 10:57:22 累计浏览 3,868 次

这两天,我的同事丁宇@felixding,极具艺术气质的设计师,推荐)遇到了一个正则表达式的问题,我琢磨了半天写了个表达式,暂时能用;今天庄表伟@zhuangbiaowei)跟我说,遇到正则表达式的问题,大家一般只能查手册,但具体的问题要怎么思考和解决问题,往往束手无策;恰好我在写作《正则表达式傻瓜书》,也希望多讲讲这方面的内容。尽管目前的写作还没有进展到介绍解题经验的阶段,但可以先在blog上写这方面的内容,希望对大家有所帮助,也希望大家多提意见;如果大家愿意,我可以继续写这类文章。
另:本例解决过程中王晖同学(@cnhacktnt)提供了大量的帮助,他使用正则表达式的熟练程度远在我之上,在此深表感谢。

要想写好、写对正则表达式,第一步就是分析需求,把模糊的应用要求清楚归纳为几条程序性特征;本例中的正则表达式用于验证“密码字符串”,仔细分解应用场景,可以得到四条明确的要求(一般来说,密码字符串对长度都有要求,但本例中,需要验证的密码字符串已经由其它语句保证了是6-12位长的字符串,所以这里不考虑长度):

1.只能由小写字母、数字和横线(-)组成;
2.开头和结尾不允许是横线;
3.不允许全部是数字;
4.不允许有连续(超过一个)的横线。

下面我们一一解析:

1.只能由小写字母、数字和横线(-)组成
这一条很好办,用字符组『[-a-z0-9]』即可解决,注意我们没有用字符组『\w』,因为一般来说『\w』等价于『[a-z0-9_]』,下划线_也可以匹配;在使用正则表达式时准确限定范围、避免错误匹配,是需要谨记的规矩;

2.开头和结尾不容许是横线
这也很好办,我们知道,在正则表达式中,字符串的开头位置用『^』表示,结束位置用『$』表示(关于『\A』和『\Z』的情况暂不讨论,因为密码字符串中不可能出现换行符),这两个锚点(anchor)只匹配位置,不匹配任何字符;开头不容许出现横线,也就是说,从开头位置向后,不容许出现横线字符,我们可以用否定顺序环视(negative lookahead)功能解决。在本例中,它写作『(?!-)』,其中的『(?!…)』是否定顺序环视的标志符,其中的横线,整个结构表示,在当前位置之后(也就是右边一位),不容许出现横线字符,把它和表示字符串开头的『^』连在一起,得到『^(?!-)』,就表示“从字符串的开始位置,向右边看,不容许马上出现横线”;类似的,我们在表达式的末尾使用否定逆序环视,正则表达式『(?<!-)$』就表示“从字符串的末尾位置,向左边看,不容许马上出现横线”;

3.不容许全部是数字
这个要求得动点脑筋,有人一看到“不容许全部是数字”,就想到否定型字符组『[^0-9]*』,这其实是不对的。我们仔细想想,“不容许全部是数字”就是“必须出现至少一个非数字字符”,而第一条要求字符只能是小写字母、数字和横线,那么这个“非数字字符”只能是小写字母,或者横线。这样一来我们就知道了,在这个正则表达式中,必须出现一个『[-a-z]』匹配的字符;

4.不容许有连续(超过一个)的横线
这种“不容许出现某种连续字符”的情况,是正则表达式中最难处理的地方,因为常见的表示“不容许”的功能,就是排除型字符组『[^…]』,于是,遇到“不容许出现两个连续横线”的情况,许多人就想当然地写下『[^--]』,但这其实大错特错――我们需要谨记,字符组的作用只限于单个字符,所以『[^--]』的意思是“在这个位置,不能匹配横线”。那么要怎么办呢?
一般来说有两个办法,我们可以规定,在一个横线字符匹配之后,不容许继续出现横线,还是应用上面说过的否定顺序环视,『-(?!-)』,就保证了匹配了一个横线之后,不容许继续出现横线,如果在每一个可能匹配横线的地方都加上这个限定,“不容许有连续(超过一个)横线”的要求也就满足了;或者我们也可以在整个正则表达式的最开头,使用否定顺序环视『^(?![-a-z0-9]*-)』,因为表达式『[-a-z0-9]*-』会“尽力寻找可能的匹配”,对它加以否定,就保证了整个字符串中绝对不容许出现两个连续的横线。
在这个例子中,我们观察第一条要求对应的表达式,发现横线一般是与小写字母和数字同时出现在一个字符组『[-a-z0-9]』中,如果采取上述第一种办法,因为字符组中只能出现对单个字符的规定(而无法使用类似环视之类的结构),『[-(?!-)a-z0-9]』的意思完全不对,所以整个字符组就要改成括号,以多选结构表示为『(-(?!-)|[a-z0-9])』,显得很累赘,所以优选第二种方法。

好了,四条要求已经分别解决完毕,现在我们把它们组合起来。

首先,是开头的『^(?!-)』,这就表示“开头不容许出现横线”,在结尾用『(?<!-)$』,表示“结尾不容许出现横线”;
其次,之中的内容都只可能是小写字母、数字和横线,所以用字符组『[-a-z0-9]』,因为长度不确定,所以使用量词『*』,变成『[-a-z0-9]*』;
再次,整个正则表达式中必须出现一个非数字字符,也就是必须让『[-a-z]』匹配一个字符,因为这个非数字字符出现的位置不确定,我们不妨把上面的表达式『[-a-z0-9]*』“切开”,把『[-a-z]』塞进去,得到『[-a-z0-9]*[-a-z][-a-z0-9]*』,这样就保证了“在所有由小写字母、数字和横线构成的字符串中,至少出现了一个非数字字符”;
最后,不容许出现两个连续的横线,我们的解决办法是在字符组的最开始位置,添加一个否定顺序环视,也就是『(?![-a-z0-9]*-)』,我们把它与之前的『^(?!-)』合并起来,得到『^(?!(-|[-a-z0-9]*-))』。

所以,整个正则表达式就是这样:

^(?!(-|[-a-z0-9]*--))[-a-z0-9]*[-a-z][-a-z0-9]*(?<!-)$

看起来完全没有问题,但放到Ruby on Rails框架里运行,却报正则表达式错误――原来是Ruby不支持逆序环视,所以最后的『(?<!-)』无法使用;那么要如何解决呢?
这时候又有两个办法,第一是用字符串函数判断最后一个字符是否横线,来“辅助”正则表达式,许多新手往往会陷入思维的误区,或者追求“漂亮”,非要用一个正则表达式解决所有问题,这其实是不必要的;如果非要用正则表达式,可能要动用一些复杂的结构――不过还好,在本例中,我们可以“取巧”,再添加一个否定顺序环视,『(?![-a-z0-9]*-$)』,表示“不容许出现 横线+字符串结尾 的情况”,也就等于“在字符串结尾之前,不能出现横线”。

我们把这个字符组与之前的『^(?!(-|[-a-z0-9]*-))』合并,就得到『^(?!(-|[-a-z0-9]*-| [-a-z0-9]*-$))』;于是,整个正则表达式就成了:

^(?!(-|[-a-z0-9]*--|[-a-z0-9]*-$))[-a-z0-9]*[-a-z][-a-z0-9]*$

输入这个正则表达式,编译不再报错,运行测试,发现完全符合要求。

建议继续学习

  1. grep 正则表达式选项要记得转义 (累计阅读 6,450)
  2. 统计最近用过的linux命令 (累计阅读 6,405)
  3. 正则表达式基础 (累计阅读 6,163)
  4. 正则表达式的与或非 (累计阅读 5,744)
  5. 学习Grep,Sed中的正则 (累计阅读 5,269)
  6. URL正则表达式 (累计阅读 4,664)
  7. 正则表达式简要入门 (累计阅读 4,366)
  8. 正则转义符汇总 (累计阅读 4,323)
  9. 使用Oracle正则表达式监控应用到数据库的连接情况 (累计阅读 4,271)
  10. PHP 正则里面的两个重要技巧 (累计阅读 4,264)