正则表达式中元素的三种逻辑
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
正则表达式中元素的三种逻辑
正则表达式提供了一整套描述文本特征的方法,它的匹配其实也就是查找符合所描述特征的文本的过程。
按照元素(单个字符、字符组、多选分支等等)的出现情况,这些特征分为三类,也可称为三种逻辑:必须出现、可能出现、不能出现;具体解释如表1。
表1 关于元素的三种逻辑
不管正则表达式多么复杂,总是这三种逻辑的组合。
比如匹配双引号字符串的任务,可以按三种逻辑分析如下。
再比如,匹配HTML中的open tag(如<h1>)和close tag(如</h1>),按三种逻辑分析如下。
必须出现
“必须出现”是正则表达式中最普通的逻辑关系,它表示某个元素必须出现。
通常,这些元素是所要匹配文本的最重要特征:查找tag,则必须出现的是字符<和>;查找E-mail地址,必须出现的元素是@。
如果某个元素必须出现,通常不会(也不应该)用量词(*、?)来限制,也不出现在多选结构中(如果把普通的字符串也看作正则表达式,那么其中每个字符都是必须出现的)。
所以,如果在匹配tag 的正则表达式中,<和>出现在某个多选结构内,或者之后跟有?、*之类的量词,那么这个表达式多半有问题;同样,在匹配E-mail地址的正则表达式中的@字符,也不应该有量词限定,或者出现在多选结构内部。
这条要求看起来简单,但是在日常使用中却容易犯错误。
经常遇到的情况是,为了考虑更多的可能情况而修改正则表达式,增加量词和多选结构,最后正则表达式中没有任何必须出现的元素。
比如匹配数字字符串的正则表达式,一个数字字符串可能包含三个部分:开头的+或-、整数部分、小数点和小数部分,然后分别列出匹配这三个部分的正则表达式。
单独来看,这三个部分都不是必须出现的:数字字符串可以没有符号,比如3.14;也可以没有整数部分,比如.14;还可以没有小数点和小数部分,比如-3。
所以,有些人就把对应的表达式元素加上量词,最终得到[-+]?(\d+)?(\.\d+)?。
初看起来,这样并没有错,仔细看看却发现,这个表达式中所有元素都不是必须出现的,换句话说,这个表达式匹配成功时,可以不匹配任何文本!
简单去掉量词并不能解决问题,这样又会错过许多本应该匹配的文本。
真正要做的,是理清表达式各个部分的关系,尤其是“可能出现”和“必须出现”之间的关系。
可能出现
与普通字符串处理相比,“可能出现”可以算作正则表达式最明显的特征,也是最常用的逻辑。
虽然它看起来很直观,但细说起来,它其实分为两种情况:从元素外来看,元素可能出现也可能不出现,或者出现次数不确定;从元素内来看,元素可能表现为一种形态,也可能表现为另一种形态。
第一种情况需要用量词。
在匹配双引号字符串的正则表达式中,两个双引号字符是必须出现的,它们之间的文本可能出现,也可能不出现,如果出现,长度没有限制,所以用*来限制;在匹配tag的正则表达式中,<和>之内的tag名,必须包括至少一个字符,所以用+来限制。
第二种情况需要使用字符组或多选结构。
如果各种可能形态都是单个字符,则使用字符组,比如上一节匹配数字字符串正则表达式中的[-+],它说明“此处可能出现+号,也可能出现-号”。
如果某一种可能形态不只是一个字符,比如this或that,则应该使用多选分支(this|that)。
回过头来看数字字符串的匹配,根据上一节的分析,符号部分、整数部分、小数部分(包括小数点)都是可能出现的,但这种“可能”其实是需要细分的。
符号部分的“可能出现”其实是“可能出现也可能不出现的”;整数和小数部分的可能出现情况要复杂一点,需要研究可能出现的几种形态:只出现整数部分;只出现小数部分;整数部分和小数部分都出现。
使用多选结构将这三种形态统一起来,得到(\d+|\.\d+|\d+\.\d+)。
最后得到的正则表达式就是[-+]?(\d+|\.\d+|\d+\.\d+)。
有些人会觉得(\d+|\.\d+|\d\.\d+)麻烦,所以会把多选结构的各个分支合并,得到(\d+)?\.?\d+。
这两个正则表达式能匹配的文本的确相同,但我并不推荐这样写正则表达式,因为它的逻辑不够清晰:对表达式(\d+)?\.?\d+来说,最后的\d+是必须出现的。
如果文本只包含整数部分,比如3,\d+匹配的是整数部分,但如果文本包含小数部分,比如 3.14,则\d+匹配的是小数部分的数字。
表达式(\d+|\.\d+|\d+\.\d+)虽然麻烦一点,却是一目了然。
如果使用表达式[-+]?(\d+|\.\d+|\d+\.\d+)仍然不够完美,它可能错误匹配-.14。
此时,[-+]?中真正匹配的是-,
(\d+|\.\d+|\d+\.\d+)中真正匹配的是\.\d+。
要解决这个问题,可以将符号-和+分情况对待:如果是+,则之后的表达式有三种可能:只有整数部分;有整数和小数部分只有小数部分,所以用表达式(\d+|\.\d+|\d+\.\d+)匹配;如果是-,则之后的表达式只有两种可能,不可能出现“只有小数部分”的情况,所以用表达式(\d+|\d+\.\d+)匹配。
综合这些情况,最终得到的表达式就是(+?(\d+|\.\d+|\d+\.\d+) |-?(\d+|\d+\. \d+))。
不能出现
“不能出现”是正则表达式中最难处理的。
最简单的“不能出现”可以直接使用排除型字符组,它表示“此处必须出现一个字符,但不能是某些字符”。
比如匹配双引号字符串,首尾两个双引号之间的字符,都不能出现双引号字符,用[^"]表示,上一节说到,这部分长度可以为零,应当使用量词*,所以整个表达式就是”[^"]*”。
再比如匹配html tag,在<和>之间“不能出现”>,用[^>]表示,应当使用量词+,所以整个表达式就是<[^>]+>,这个表达式仍然可能错误匹配</>。
为解决这个问题,还需要细分两种可能:如果<之后是/,则还必须出现至少一个不为>的字符,比如</a>,应当使用表达式/[^>]+;如果<之后出现的字符不是/,则在这个字符之后,>之前,还可能出现>之外的字符,并且可能不出现,如果出现,长度没有限制,比如在<a>中,在[^>]匹配之后,>之前,就没有任何字符了,所以应当使用表达式[^/][^>]*。
不过,这只是最简单的情况;更复杂的情况是,在某个字符串中不容许出现某个字符串,比如E-mail地址中的用户名(username)就是如此。
点号和横线不能出现在开头的情况比较好满足,可以用\w匹配“非点号非横线字符”,既然整个用户名不能超过64个字符,那么之后的字符串长度不超过63个字符。
综合起来,得到[\w][\w.]{0,63}。
可是,不容许出现连续两个点号的要求则很难满足,所以下面集中讨
论“不超过63个字符”部分的匹配。
要求不能出现两个连续点号,许多人的直观反应是[^.][^.],所以整个表达式改为[\w.]{0,63}[^.][^.][\w.] {0,63}。
但是这样行不通,原因有两点:首先,在[^.][^.]前后的两个[\w.]{0,63}能匹配的文本,总长度其实在0~126之间,无论我们如何修改量词,也不能在两个量词之间建立联系,保证总长度在0~63之内;其次也是最重要的,[^.][^.]的真正意思是“找到连续的两个非点号字符”,正则表达式匹配时会尽力满足这一要求,即便是类似123..456这样明显包含两个连续点号的文本,[^.][^.]仍然可以在其中找到四处匹配:12、23、45、56,同时左右两侧的[-\w.]{0,63}仍然可以成功匹配。
还有人想到的是效仿排除型字符组,用(^\.\.)来表示“不能出现两个连续点号”,遗憾的是,这样也行不通,因为正则表达式只有排除型字符组,没有“排除型括号”。
那么,不妨反过来思考:不能出现两个连续点号,意思是这段文本中的所有字符的右侧都不能是两个连续点号,用环视表示就是[-\w.](?!\.\.),这样的字符最多有63个。
所以,就得到了表达式([-\w.](?!\.\.)){0,63}。
请注意,因为[-\w.](?!\.\.)不是单个字符也不是字符组,所以用量词限定时,必须使用括号将整个子表达式分为一组;再在表达式的最开头加上之前提到的\w,保证第一个字符的正确性。
从例1可以看到,这个表达式完全可以保证字符串不会以点号开头,不会包含连续点号,也不会超出规定长度。
例1 使用环视实现“不能出现”的逻辑
#注意验证时要在首尾加上\A和\Z
usernameRegex = r”\A\w([-
\w.](?!\.\.)){0,63}\Z”
#合法的用户名
re.search(usernameRegex, ”abc123_
”) != None # => True
re.search(usernameRegex, ”abc1-
2.3_”) != None # => True
#包含连续点号
re.search(usernameRegex, ”abc1-
2..3_”) != None # => False
#开头字符不合法
re.search(usernameRegex, ”.abc1-
2.3_”) != None # => False
re.search(usernameRegex, ”-abc1-
2.3″) != None # => False
#长度超过限制
re.search(usernameRegex, ”0″*65) !=
None # => False
也可以更进一步,把“第一个字符不能是点号或横线”也用环视表达,整个表达式就是(?![-.])([-\w.](?!\.\.)){1,64}。
实际上,这个表达式确实更直观、更容易理解,只是注意量词的下限应当改为1,因为用户名不能是空字符串。
总结一下:正则表达式中的“不能匹配”,最简单的情况可以用排除型字符组直接表示,但它只能表示“某个字符不能出现”;如果要表示“某个字符串不能出现”,一般都要用到否定环视,其逻辑是:在文本中的每个位置,都用环视否定“不能出现”的字符串(除此之外,还有另一种逻辑,下面讲解验证操作时会看到)。
不过,一些比较古老的工具(比如Apache 1.3,以及Linux/UNIX下的某些工具)并不支持否定环视,所以使用时必须留意。