写点什么

正则表达式(四):正则表达式的与或非

  • 2011-04-04
  • 本文字数:3373 字

    阅读完需:约 11 分钟

我们使用正则表达式,熟练掌握各种功能和结构只是手段,解决实际的问题才是真正的目的。要解决真正的问题,就必须有解决问题的思路,正则表达式的功能,说到底,可以归纳为三种逻辑,为了表述方便,我们分别称为与、或、非。

逻辑关系

说明

在某个位置,某些元素(字符、字符组或者子表达式)必须出现

在某个位置,某个元素或许不出现,或许不出现,或许长度不固定;要出现的,是某几个元素中的一个

在某个位置,某些元素不能出现

一般来说,正则表达式千变万化,总是这三种逻辑的组合。比如匹配双引号字符串

“quoted string”

逻辑关系

分析

首尾的双引号字符必须出现

两个双引号之间的字符个数是不确定的(如果是空字符串””,则两个双引号之间没有字符)

两个双引号之间不能出现双引号字符

再比如匹配 html 中的 open-tag(比如

)和 close-tag(比如

逻辑关系

分析

首尾必须分别是 < 和 >,如果是 close-tag,则 < 之后必须出现 /

< 和 > 之间必须出现至少一个字符(<> 不是一个合法的 tag)

< 之后不能是 / 字符,如果是 open-tag,< 之后不能出现 /

下面我们来讲解三种逻辑的对策。

“与”是正则表达式中最普通的逻辑关系。一般来说,如果正则表达式中的元素没有任何量词(quantifier,比如 *、?、+)修饰,就是“与”关系。比如『<』,就表示“这里必须出现 < 字符”;『cat』,就表示“这里必须依次出现 c、a、t,3 个字符”。

不过“与”的情况并没有这么简单,有时候,“必须出现”的是若干个元素,或者说,几个元素必须同时出现,但它们之间并不相连,这是非常容易犯错的时候,不过现在我们不举具体的例子,稍晚一点再说。

“或”是正则表达式中最灵活的逻辑关系。正则表达式能应对各种不同的文本,“或”功能不可或缺。

如果“或”的意思是,元素可以出现,也可以不出现,或者出现的次数不确定,可以用量词来表示“或”关系。比如表达式『a?』,表示在此处,字符 a 可以出现,也可以不出现;表达式『(ab)+』,表示在此处,字符串 ab 必然要出现 1 次,也可以出现无限多次。

如果“或”的意思是,可以出现的是某几个元素中的一个,则应该使用字符组或者多选结构。当元素都是单个字符时,就应该使用字符组『[…]』:比如匹配单词 cat 或者 cut,除去开头的 a、结尾的 t 是固定的,之中“或许出现 a,或许出现 u”,所以应当使用字符组『[au]』,整个正则表达式就是『c[au]t』。当元素不只单个字符(只要有一个元素不只单个字符)时,就应该使用多选结构『(…|…)』:比如不但要匹配单词 cat 或者 cut,还要匹配单词 chart、conduct 和 court,出去开头的 a、结尾的 t 是固定的,之中“或许出现 a,或许出现 u,或许出现 har,或许出现 onduc,或许出现 our”,这时候就应该使用多选结构『(a|u|har|onduc|our)』,整个正则表达式就是『c(a|u|har|onduc|our)t』。

当然,多选分支也可以表示字符组,比如『[au]』就可以表示为『(a|u)』,两者的功能是完全等价的,但是字符组的效率更高,也更直观(毕竟,大家都习惯了简单的『[au]』,而看到『(a|u)』则多半要想一想。

在实践中,“与”和“或”经常同时出现,而且关系不那么简单,下面举一个例子说明:为了隐藏真实的结构,我们需要用 URL 进行伪装,比如这个 URL pattern:/foo/bar_tmp.php。

在真正的系统里,foo 是模块名,bar 是控制器名,tmp 是方法名。合法的 URL 并不要求 3 个名字每次都出现,可以只出现控制器名(/foo),也可以只出现控制器名和模块名(/foo/bar.php),也可以 3 者都出现(/foo/bar_tmp.php)。

这里的模块名、控制器名、方法名,都可以用『[a-z]+』匹配,这里为说明方便,我们暂且用 foo、bar、tmp 代替对应的表达式。初看起来,这个表达式只包含“与”和“或”两种关系:

逻辑关系

分析

/foo 必须出现

/bar、_tmp、.php 都是可选出现的

所以,正则表达式是『/foo(/bar)?(_tmp)?(\.php)?』。

这个表达式确实可以匹配 /foo、/foo/bar.php 和 /foo/bar_tmp.php,但是,它也可以匹配 /foo_tmp、/foo/bar_tmp 等形式,虽然这些形式并不是合法的。

仔细研究之后,我们发现,“与”和“或”的关系并没有那么简单,而应该是这样的:

逻辑关系

分析

/foo 必须出现

/bar 和.php 是可选出现的,但必须同时出现,或同时不出现(与)

在 /bar 和.php 都出现的前提下,_tmp 才可以出现(或)

/foo 必须出现,这很好表示,暂且不去管它;/bar 和.php 如果出现,必须同时出现,所以它们应该作为一个元素,写作『(/bar.php)』;整个元素可选出现,所以给它添加量词,得到『(/bar.php)?』;最后,在 /bar 和.php 都出现的前提下,_tmp 才可以出现,所以将『(_tmp)?』填充到『(/bar.php)?』,得到『(/bar(_tmp)?.php)?』,最后加上开头的 /foo,整个表达式就是『(/bar(_tmp)?.php)?』。到此,整个关系终于完整表达出来,表达式也不会发生错误匹配。

“非”是正则表达式中最难处理的逻辑关系。因为没有直接对应的结构,“非”的处理比较吃力。

最简单的“非”,意思是此处不能出现某个字符,这一点通常很直观,似乎用排除型字符组『[^…]』就可以解决。比如双引号字符串的匹配,首尾两个双引号很容易匹配,其中的内容肯定不是双引号(暂时不考虑转义的情况),所以可以用『[^"]』表示即可,其长度不确定,所以用 * 来限定,所以整个表达式就是『"[^"]*"』,非常简单。

但是,事情果真都如此简单吗?我们仍然举 cat 和 cut 的例子,如果仍然希望匹配 c 开头、t 结尾的单词,但不希望匹配 cut,可以写成『c[^u]t』,是否就可以了?

这个表达式的意思是:最开头的字母是 c,之后是一个不为 u 的字符,之后是 t。没错,它确实不会匹配 cut,也可以匹配 cat。但是,chart、conduct、court 等等,它也没法匹配,因为 [^u] 的意思是:匹配一个不是 u 的字符。

那么,把『[^u]』改成『[^u]+』好了,这样应该就可以解决问题了。但是真的如此吗?『[^u]+』的意思是,一个或若干(最多到无穷)个字符,但每一个字符都不能是 u。所以,尽管『c[^u]+t』能匹配 cat 和 chart,却不能匹配 conduct 和 court。

看来,“非”真是比较难对付,让人非常纠结。好在,也不是没有办法解决它。回复到与 - 或 - 非的观点,分析要实现的功能:

逻辑关系

分析

以 c 开头,以 t 结尾

c 和 t 之间可以出现的字母必须多于一个,没有上限

c 和 t 之间不能只有一个字符 u

如果只考虑“与”和“或”两个逻辑,表达式很好写,是『c[a-z]+t』,再把剩下的条件附加上去,就可以解决问题了。我们仔细看“非”的条件:c 和 t 之间不能只有一个字符 u。既然『[^u]+』表达的并不是这个意思,我们不妨换一种表述法:在 c 之间的位置向后看,不能出现 cut。这一点,正好对应否定顺序环视(positive look-ahead)功能,『(?!cut)』就是用来进行这种判断的,它判断之后的字符串能不能由 cut 匹配,但并不真正真正进行匹配,也不会移动“当前位置”。所以我们将它放在表达式的最开头,得到『(?!cut)c[a-z]+t』。这个表达式的逻辑是:只有在当前位置右侧字符串不能由 cut 匹配的情况下,才从这里开始,向右尝试用 c[a-z]+t。

如果我们更进一步,需要排除掉 cat 和 cut,可以把否定顺序环视改为『(?!c[au]t)』。这样就能保证,匹配到的肯定不是 cat 或者 cut。

更复杂一点,如果我们要验证这样一个字符串:它全部由小写字母构成,长度不超过 12 位,其中不能包含 unfavored 或者 unwanted。也可以照章处理,先匹配“长度不超过 12 位”的小写字母『[a-z]{,12}』,然后写出匹配“不需要匹配内容”的正则表达式,『(unfavored|unwanted)』,再用否定顺序环视将它“排除”即可,只是这次要注意,不能直接写『(?!(unfavored|unwanted))』,因为它只能排除『(unfavored|unwanted)』出现在字符串开头的情况,为了排除它出现在字符串中的情况,我们要把否定顺序环视改为『(?![a-z]*(unfavored|unwanted))』,这样就确保完整的“排除”,整个表达式就是『(?![a-z]*(unfavored|unwanted))[a-z]{,12}』。

总结一下,正则表达式中的“非”,除去能用排除型字符组直接表示的,复杂一点的“非”逻辑都是按照这样的思路进行的:先用一个正则表达式准确匹配需要“排除”的字符串,再用环视功能排除掉它——“非”确实是正则表达式中,最难处理的逻辑关系,好在它并不复杂,而且,除去一些比较古老的工具(比如 Apache 1.3),现在各种工具和语言,基本都支持这种功能。

2011-04-04 00:0062854

评论 1 条评论

发布
用户头像
(?![a-z]*(unfavored|unwanted))[a-z]{,12}
这个表达式写的有问题,[a-z]{,12} 这种写法 Python 才支持 ,但是如果是 Python 会匹配到 unfavored 或 unwanted 非首个单词后面的部分,感觉断言位置匹配是逐个位置尝试匹配,满足表达式就行,而非先匹配表达式再决定位置。
2019-08-11 23:03
回复
没有更多了
发现更多内容

数据库ACID四大特性到底为了啥,一文带你看通透,java支付宝支付接口教程

Java 程序员 后端

数据源的概念是什么?Springcloud+Mybatis如何使用多数据源

Java 程序员 后端

本科毕业斩获字节offer的我做了什么准备?大厂面试经验试题分享(1)

Java 程序员 后端

数据库中间件MyCat实战笔记(第一步),arm架构linux系统

Java 程序员 后端

数论 - 约数基础 【 试除法求所有约数 + 约数个数和约数之和

Java 程序员 后端

最新版SpringBoot开发实战:阿里技术官从基础到项目综合实战pdf

Java 程序员 后端

月薪60k的Java开发在阿里是什么级别?对技术能力有哪些要求?

Java 程序员 后端

拿捏了!ConcurrentHashMap!,宝塔linux建站教程

Java 程序员 后端

普通程序员就不能有拥有架构师光环?想要建立架构思维,这份京东架构技术精髓一定不能错过

Java 程序员 后端

新人问一般都用哪些 Linux 命令,我把这个扔了过去,linux基础及应用教程课后答案

Java 程序员 后端

是什么Java面试题PDF被Git全面封杀?刷完这套题已经拿完9个Offer!

Java 程序员 后端

如何用内网渗透突破安全策略

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

拼多多3面+余额宝4面+蚂蚁金服5面,Java自学宝典

Java 程序员 后端

排除MySQL中常见错误的实用招术,什么是微服务扩展性和高可用、可扩展性

Java 程序员 后端

推荐一款技术人必备的接口测试神器:Apifox,不愧是大佬

Java 程序员 后端

提升开发效率N倍的20+命令行神器,赶紧收藏了,mybatis原理图

Java 程序员 后端

数据结构系列第六部分:排序,Github爆火的《高并发秒杀顶级教程》

Java 程序员 后端

最新基准测试:Kafka、Pulsar-和-RabbitMQ-哪个最快,阿里Java笔试题目

Java 程序员 后端

服了!阿里资深架构师发布SpringCloud笔记,在GitHub标星已达81

Java 程序员 后端

本科毕业斩获字节offer的我做了什么准备?大厂面试经验试题分享

Java 程序员 后端

来自北京大学NOIP金牌选手yxc的常用代码模板3——搜索与图论

Java 程序员 后端

推荐这款牛掰的 API 敏捷开发工具,java程序设计教程课后题答案

Java 程序员 后端

数据结构的栈和队列(这不进来看一看),计算机java语言入门

Java 程序员 后端

最近被安排搞搜索接口优化,压测了4次,才勉强达到要求

Java 程序员 后端

有个厉害的程序员老婆是什么体验?,mysql性能优化面试题

Java 程序员 后端

拜读!程序员60K+高薪技术,spring整合mybatis原理

Java 程序员 后端

是什么让普通的链表也能达到二分查找的效率,你知道吗?

Java 程序员 后端

普通本科,毕业六年,复盘一个月,mybatis一级缓存和二级缓存面试题

Java 程序员 后端

数据库索引的原理,springcloud视频百度云

Java 程序员 后端

斯坦福高效睡眠法-读书笔记,这可能是目前最全的

Java 程序员 后端

无论你是什么职业,这篇职场生存法则都是你必备的,java微服务架构技术

Java 程序员 后端

正则表达式(四):正则表达式的与或非_Java_余晟_InfoQ精选文章