正则

正则表达式的与或非

今天我的同事老赵 @jeffz_cn 问我,有没有办法用正则表达式匹配“不包含某个字符串”的文本,正好,我在写作的《正则表达式傻瓜书》中也提到了这类问题,就把这一节放出来,给大家参考,也希望大家多提建议(尤其是配图方面)。 正则表达式的与或非 我们都知道,写正则表达式有点像搭积木,复杂的功能总可以拆分开来,由不同的元素(也就是子表达式)对应,再用合适的关系将它们组合起来,就可以完成。在这一节,我们讲解常见的与或非关系的表达。 与 “与”是最简单的关系,它表示若干个元素必须同时相继出现,比如匹配单词cat,其实就是要求字符c、字符a和字符t必须同时连续出现。 正则表达式表达“与”关系非常简单,直接连续写出相继出现的元素就可以,我们可以想象,在各个元素之间,存在看不见的连接操作符·,比如上面匹配单词cat的正则表达式,就是『cat』,我们可以将它想象为『c·a·t』。 “与”关系也不限于字符之间,任何子表达式都可以用它来连接,如果我们把上面单词中的a替换为字符组『[au]』,表达式就变为『c[au]t』,你可以想象为『c·[au]·t』。 或 “或”是正则表达式灵活性的重要体现,我们可以规定某个位置的文本的“多种可能”,比如要匹配cat或是cut,在正则表达式看来,就是“字符c,然后是a或u,然后是t”。 如果“或”的多种可能都是单个字符(一般要求ASCII字符,中文字符等多字节字符的情况,可以参考本书专门论述的章节,此处仅以ASCII字符为例),就可以用字符组来表达“或”的关系,比如上面的cat或者cut的情况,正则表达式写做『c[au]t』,其原理如下: 更复杂的情况是“或”的多种可能,并非都是单个字符,有些可能是多个字符。比如,我们可以看一个更复杂的例子,不仅要匹配cut,还要匹配c开头、t结尾的单词chart、conduct和court。也就是说,在开头的c,结尾的t之间“可能”出现的是:u或har或onduc或our。 遇到这种情况,就不应使用字符组,而应当使用多选分支『(…|…)』,将各个“可能选项”列在多选分支中。于是,正则表达式变为『c(u|har|onduc|our)t』,其原理如下: 关于多选分支,还有两点要补充: 多选分支也可用于“每个选择都是单个字符”的情况,比如『c[au]t』写成『c(a|u)t』是没错的,但字符组的效率要远高于多选分支,所以,在这种情况下,推荐使用字符组『c[au]t』; 默认的多选分支『(…|…)』使用的括号是会捕获文本的,也就是说,括号内的表达式真正匹配成功的文本会记录下来,匹配完成之后可以提取出来,具体到上面的例子,就是我们有办法在匹配完成后“提取”出u或har或onduc或our。但许多时候,我们需要的只是整个表达式的匹配,而不关心“匹配时到底选择的哪种可能情况”,在这种情况下,我们稍加修改,使用“不捕获文本的括号”,可以提高效率。不捕获文本的写法也很简单,只是在开扩号之后加上字符『?:』,也就是『(?:…|…)』,具体到上面的例子,就应该写成『c(?:u|har|onduc|our)t』。这样做虽然繁琐点,但效率有保障,阅读起来也不困难,我推荐养成这种习惯,只要用到了括号,就想想是否真的要捕获括号内表达式匹配的文本,如果不需要,就是用不捕获文本的括号。 非 “非”看起来简单,其实是最复杂的,以下分几种情况讨论。 首先讨论针对字符的“非”:不容许出现某个或某几个字符。这是最简单的情况,直接用排除型字符组就可以对付,仍然用上面的例子,如果要匹配的单词是c开头、t结尾,中间有一个字符,但不能是u(也就是说,整个单词不能是cut),直接用『c[^u]t』就可以了,若中间的字符不能是a或u(也就是说,整个单词不能是cat或cut),则表达式改为『c[^au]t』。…

14 years ago

正则表达式解题经验谈

这两天,我的同事丁宇(@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])』,显得很累赘,所以优选第二种方法。 好了,四条要求已经分别解决完毕,现在我们把它们组合起来。…

14 years ago