我们都知道,正则表达式可以很方便地对字符串进行匹配、查找、分割等操作。但是,面对看似复杂的符号组合,自己就曾被唬过,感觉这是一种难以理解的语法,所以碰到的时候也就查查对应的正则表达式是表示什么意思,并没有尝试了解。人总是被逼的,轮到自己开发有关字符串的匹配功能的时候,发现使用字符串拆解比对的方式,逻辑上实在很繁杂,不简练更不直观,程序的健壮性也不敢想象。可见,即使某种语言是正交完备的,也不一定能很高效地去表述所有逻辑语境——这个论断和我之前写到的《压缩算法引申》是一致的。这么着,自己也给自己一个机会接触新知识,看了一天正则表达式,并用程序实践之,逐渐感觉渐入佳境。于是用自己的思维方式(物理出身),重新整理正则表达式的精要。
这里,我尝试用数学集合的方式去表述正则表达式,希望能够用几乎人尽皆知的集合方式阐述看似复杂的字符串匹配规则。本文,主要参数正则表达式的基本语法要素,即字符组,为了满足集合表述,这里把字符组成为字符集合。具体分析如下:
1. 普通字符
类似只有单个元素的集合组,每个集合只有一个字符,这个字符可以是字母、汉字、数字、下划线、换行符等。单个字符集合只能表示该字符本身,如正则式“A”所匹配的就是左右只含字母“A”的字符串。用集合运算,即
{A} = {A},{^A} != {A} (这里^是非的意思,映射到集合运算,即为补集的概念)
2. 预定义字符
在正则表达式语法要素中,有一些字符集合是预定义好的,我们可以拿来即用。这些字符集合的划分方式很符合我们平时使用到的字符划分方式。这些预定义的字符集一般是成对出现的,也是使用集合补集的概念。具体如下:
预定义字符 |
说明 |
\d和\D |
\d表示0到9之间的一个数字,集合表达式为设D={0,1,2,3…9},总集合N={所有字符(视具体编码而定)},则\d表示为A = {a : a∈D} \D是\d的补集,表示0到9以外的任意一个字符,集合表达式B={b,b∈^D} |
\w和\W |
\w表示字母、数字、下划线中任意的一个字符,即A={a : a∈[a-zA-Z0-9_]} \W是\w的补集,即\W表示除字母、数字、下划线之外的任意一个字符 根据不同的编码,这个集合会有所变动,如下说明 在支持ASCII码的语言中,如JavaScript,“\w”等价于[a-zA-Z0-9_] ; |
\s和\S |
\s表示任意一个空白字符,如空格、制表符、换页符等,\S是\s的补集 |
注意:上面的预定义字符集合的元素个数都是1,和普通字符一样;并且,我们发现不同语言支持的编码不同,预定义字符所对应的具体内容也不同。这样,我们可以很轻松的写出特定区域电话号码的正则表达式:
0592-\d\d\d\d\d\d\d
备注:正则表达式预定义字符不仅仅上面三类,还有其它小类,这里不予罗列。
3.自定义字符集合
集合区间是个重要的概念,用于表述集合元素的范围。从集合的表示法看,集合一般有两种表示法:①枚举法(列举法)②描述法。我们在写正则表达式的时候往往也需要用到集合区间的概念,也同样可以使用枚举法(如上例,电话号码),也可以使用描述法。我们知道,往往描述法会显得简练,不易遗漏。正则表达式同样有一套区间的表示方法,非常直观,就是[#-&],其中#和&都表示字符元素。当然,也可以使用枚举法,同样是用[]表示,如[xyz]表示x/y/z中的任意一个字符,注意,还是单个字符,即x ∈[表达式]——枚举法还可以用|或语句表示,如[x|y|w]。既然有区间表述,也会有补集的定义。正则表达式的补集定义是这样的[^#-&],表示集合[#-&]的补集。
举例,同样是用电话号码,只查询号码第一位的高于5的电话号码集合,表达式如下
0592-[6-9]\d\d\d\d\d\d,或者0592-[^0-5]\d\d\d\d\d\d。
备注:字符集可以通过[]自定义,仅仅匹配多个字符中的一个@H_502_108@。
4. 特殊字符
接下来要讲解的是特殊字符,这些特殊字符也是预定义字符,个人觉得目的是为了向完备性和简练性进发。先列表展示一下这些特殊字符
特殊字符 |
说明 |
. |
匹配换行符以外的任意字符,即[^\n] |
^ |
匹配字符串的开始 |
$ |
匹配字符串的末尾 |
\b |
匹配字符串的开始或者末尾 |
\B |
|
\ |
转义字符,对上述特殊字符,需要通过该转义字符来取消预定义的特殊意义 |
上述^、$、\b、\B这四个字符为了满足定位字符串的前后位置;\转义字符,目的就是为了把已经占用的特殊字符转换为正常字符,是为了满足完备。
顺便罗列出量词定义,以便一并解释这些定义的集合意义,如下
量词:就像是计算集合元素个数的聚合函数,类似COUNT(A)——具体的数学表达式给忘了…
量词 |
说明 |
S? |
表示指定的字符或组S最多出现一次,可以没有 |
S+ |
表示指定的字符或组S至少出现一次 |
S* |
表示指定的字符或组S可以出现任意次,包括不出现 |
{n} |
表示指定的字符或组必须出现n次 |
{m,n} |
表示指定的字符或组最少出现m次,最多出现n次 |
{n,} |
表示指定的字符或组最少出现n次,多了不限 |
有了量词的定义,我们就可以更简练的实现重复性定义的缩写了,这个有点类似数学里的科学计数法。还是那个号码例子,之前我们为了拼接7个数字,需要写7次\b,这个不仅不直观,可能会漏写或者多写,也不便于代码维护。有了量词的定义,就可以用\b{7}表示,这个就非常简练了。再举个例子,有时候我们要匹配的字符串长度是预先未知的,使用*就非常轻松解决这个问题了,比如\w*,表示任意个数的字符拼接的字符串。
回归到字符串的基本定义,一个字符串包含多个字符。使用上述介绍的字符集,基本上是涵盖了所有的字符,即任意字符∈上述定义的集合并集。但是,单纯这样的集合根本不够适用于灵活的正则表达式,即不适合于表示我们想要表述的字符串规则——是的,正则表达式的目的是就是表示一个有规律的字符串。所以,引入量词的目的就是为了满足这个目的。因为,发现没,量词其实有if、for的逻辑。也就说,量词完成了正则表达式的逻辑法则。巧妙的使用量词和巧妙的使用编程语法是一样的,需要一个练习和实战的过程。
具体实战例子会在自定义velocity模板编辑器的自动联想功能中总结。
个人整理的进阶文章
备注:本文参考资料主要有《菜鸟成长之路——Java程序员全攻略》之11.2拿下正则式。