时间2013.12.29
地点:软件大楼211
——————————————————————————————————————————
一、前言
2013年就那么可心里默数的几十个小时留下,没想默数着让它过完期待那么光明的一天,黑夜给了我黑色的眼睛,我用来寻找光明,等天明,太阳升起,若在是舞池中央最好。
这篇文章是一篇正则表达式入门级别的科普,若真的想入门,此处无门。
在材料院的设备改造项目里,我们已经用到牛逼哄哄的正则表达式,主要用于实验记录的查询,如下所示:
平时我们在文本编辑器里比如word,text等的查找与替换功能也恰是正则表达式的功劳,玩Linux的大神们应该知道grep命令,全称是Global Regular Expression Print,是一个强大的文本搜索工具,凭借正则表达式搜索文本,并把匹配行打印出来,正则表达式概念的普及正是始于此,当然这点应用只是皮毛。有些问题不深究往往被小看,有些问题深究却是死胡同,世界因为有问题才留给我们机会,不是吗?瞎闹与折腾,现在发现傻子其实最幸福,智商越高脑壳越疼,情商越高心越疼。
——————————————————————————————————————————
二、问世间正则表达式为何物,直叫人生死相许
在很多场合,我们需要查找出符合某一特定规则的字符串,这种需求有很多很多,比如Google浏览器的查找功能,在比如计算机的搜索查找功能,而正则表达式正是用于描述这一特定规则的工具。我们说某个字符串匹配某个正则表达式,即该字符串的部分满足于正则表达式给出得条件。是不是,因为这个工具,许多事情变得便捷,世界因为正则表达式而变得美丽,有阳光或许感觉到冷。
——————————————————————————————————————————
三、破门而入
我在前面说想入门,此处无门,但我说的是如果你真想入门的话此处无门。有些事就像钞票我们难以辨别真假,收到过100元假钞的同志们自我安慰下吧,学好正则表达式你的身价又涨上去了,丢100是为了明天不丢1000。
学习最好的方式是动手从例子从模仿开始,比如林平之练碧血剑谱,若只是天天在闺房里看剑谱永远都只是剑谱,编程有四境界:无意识无能力——不知道自己不知道(平民),有意识无能力——知道自己不知道(码农),有意识有能力——知道自己知道(攻城师),无意识有能力——不知道自己知道(码神)。
假设你想在你的word编辑器里查找hi的单词,可以简单地使用hi正则表达式,它由两个字符组成,前一个字符是h,后一个字符是i,但是这样做我们会把hi,himself,history,high都找出来,于是我们还可以加更多的约束,使之更严格地匹配,用 \bhi\b 可以实现,其中 \b 表示字符串的开始与结束,即词的分界,叫做元字符。这样的元字符还有很多,比如:
——————————————————————————————————————————
. (通配符,表示除换行符以外的任何字符,注意是一个字符,而不是字符串,举例 a.b——可匹配 abc a5c a6c a#c,若是ab5c ac这是不可匹配的,我们说了,只是任意一个字符对不对)
^ (匹配字符串的开头位置 举例 ^liu——可匹配liuze liuzekun
$ (匹配字符串的结尾位置 举例 $liu——可匹配 zeliu kunzeliu
结合以上两者 ^liu$ 则只能匹配以liu开始且以liu结尾的字符串了,那就是精确匹配liu本身
* (匹配零次或多次之前的部分,举例 a*b——可匹配 b ab aab aaab aaaab aaaaab
+ (匹配一次或多次之前的部分, 举例 a+b——可匹配 ab aab aaab aaaaab aaaaab 但不匹配 b ,我觉得*像是乘号,将前面部分可乘 0 次,1次,2次......都匹配,而 +像是加号,将前面部分加0次,1次,2次......都匹配,大概就是为什么 *匹配的是零次或多次前面部分,而+就是匹配一次或多次前面部分,纯属个人理解)
? (匹配零次或一次之前的部分,举例 a?b——只可匹配 b ab 这里 ?像编程语言里的双目表达式里的?,取值有而,要么true选前者,要么false选后者,只是这不涉及真假选择,但要么有要么没有大概一样的道理。
——————————————————————————————————————————
这里先总结这么几个掌握,更多在后面有总结,有了上面这些,我们来看一个复杂的正则表达式:\bHello\b.*\bWorld\b
首先 \bHello\b ——表示匹配一个精确的 hi 字符串
再有 .* ——表示零个或多个 任意一个字符(擦,绕口了)
最后 \bWorld\b ——表示匹配一个精确的World字符串
上述联合起来就是匹配 Hello.......World 的字符串的。(......表示任意个数任意字符)
再来看一个:0\d\d-\d\d\d\d\d\d\d 匹配的是诸如 0731-6633936 等电话号码,这样写看起来看烦躁,偷懒可表示如下: 0\d{2}-\d{7} 这样看起来是不是就雅观了。
——————————————————————————————————————————
四、常用元字符
现在我们已经知道好多有用的元字符了,但还远不够用啊,世界太复杂了。我把它们放在表格里也许更直观,没必要去背记,这不是要去参加闭卷考试,用得多了自然就熟了。
. (点号,12K的火眼金睛才看得清) | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符,包括制表符、空格 |
\d | 匹配数字 |
\b |
匹配单词的开始或结束 |
^ |
匹配字符串的开始 |
$ |
匹配字符串的结束 |
* |
重复零次或更多次 |
+ |
重复一次或更多次 |
? |
重复零次或一次 |
{n} |
重复n次 |
{n,} |
重复n次或更多次 |
{n,m} |
重复n到m次 |
下面还给个反义表
\W |
匹配任意不是字母,数字,下划线,汉字的字符 |
\S |
匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B |
匹配不是单词开头或结束的位置 |
[^x] |
匹配除了x以外的任意字符 |
[^aeIoU] |
匹配除了aeIoU这几个字母以外的任意字符 |
举例:
\ba\w*\b ——匹配步骤:1.开始处以字母a开头,2.跟任意的字母、数字、下划线或汉字字符,3.字符数的目任意,4.结束。
连起来说就是:我们想要匹配以a开始,后面紧跟数目任意的某字符这样一个字符串。
\d+ ——匹配步骤:1.数字一个,2.数目为一个或一个以上
连起来说就是:我们想要匹配某个数字的任意个数串(当然至少1个)
^\d{6,10}$ ——匹配步骤:1.一个数字字符开始,2.个数为6到12个
所以有些网上要你填写身份证号码或者电话号码或者QQ号码,当位数不对时马上就会给出提示,就是这样来得。
那么我们现在还来说说^\d{6,10}$与单纯\d{6,10}的区别:
^\d{6,10}$ 匹配的是以一个数字开始,整个字符串是6到10个数字组成的字符串,比如:12345678
而\d{6,10} 匹配的是字符串中含有6到10个连续数字组成的串即OK,比如: hu12345678nan
前者是标记有开始和结束,要求严格,后者是只要包含即可。
所以我们可知道,正则表达式所说的单词即连续个字符组成的字符串。不一定有含义,只要连续。
五、转义
和编程语言一样,元字符都有特殊的含义,如果我们要匹配元字符,这就需要转义,我们一如既往地选择\来取消这些字符的特殊含义,比如: \. \* \\ 等,这就不多描述了,业界通用,但还要记住一点,如果在编程语言中实现,我们还要转义一次,因为这些符号在编程语言里又有特殊含义,又要取消一下特殊含义,现在麻烦来了,拿 *来说: 1.先是在正则表达式里我们要转义: \* 2.然后在编程语言里为了让上面这个\是普通含义,我们给他一个\ 于是成了 \\* 然而到这一步我们还只是让它在程序里是个 * 3.为了然 *不具备特殊含义我们还要一个 \ 于是整个普通*的实现就是: \\\* 若是\的表示为: \\\\ 看得好揪心 好在C++11(别的语言我不知道)里有原始字符串变量一说,于是转义一次就OK,比如我要匹配 空格、换行符、回车符、和反斜杠,按往常要这么写: ( |\\n|\\r|\\\\) ——说明:1.(后紧跟一个空格字符,2.晓得为什么是\\n而不是\\\n吗? 在C++11里,我们可以这样写:R"~( |\n|\r|\\)~" ——漂亮吧,~是边界符,可不要,也可换成其它,小括号是必须的,里边包含原始字符串。 六、多字符匹配
说道这,我们已经能解决好多问题了,给定目标的查找都不是问题了,但还是不够用,社会太复杂了,比如你想找出某个文本里的所有元音字母:a e i o u ,办法很简单,把它们列在方括号里即可:[aeIoU] ,这样我们就能匹配任何一个这样的元音字母,注意一个的含义,apple并不是它能匹配得到的,要么a 要么e 要么i等等。 也可以指定范围[0-9]匹配0到9的一个数字,同\d,同理注意只是一位的数字,来个123连续数位的无能为力。 再比如[a-zA-Z] 能匹配a到z和A到Z范围内的所有字母 再看个复杂的\(?0\d{2}[) -]?\d{8} 1. \(?说明右括号可有可无 2.然后是数字0 3.d{2}说明跟了2个数字 4.[) -]?说明)或者空格或者-可有可无 5.\d{8}说明后面是8个数字 于是乎可匹配 (010)88886666 010-888886666 010 88886666 (010-88886666(这个匹配并不是我们想要的,下面会讨论如何解决)之流的电话号码 要说明的是(和)也是元字符,所以也用\消除特殊含义,使之正常。 七、分支条件
上面匹配得到了一个不合理的匹配,用分支条件可解决这个问题,即使用 | 把不同的几部分分离开来 0\d{2}-\d{8}|0\d{3}-\d{7} 匹配: 0XX-XXXXXXXX 或者 0XXX-XXXXXXX \d{5}-\d{4}|\d{5} 匹配: XXXXX-XXXX 或者 XXXXX 这里要注意的是分支条件的顺序,如果满足前一条件了,后面就不管了,比如这里写成\d{5}|\d{5}-\d{4} 就有问题了。 八、分组
括号()用于标记子表达式,子表达式也称捕捉组,好诡异的名字,括号括起来之后括号里的部分就是一个整体,你可视为单个字符,而括号里边我们也可以进行局部操作。比如 (\d{1,3}\.){3}\d{1,3} 1.(\d{1,3}\.) 说明是一个1到3位的数字,后面跟一个.(刚开始我一直这样理解,认为\b是一位数字,然后再重复1到3次,如此便是2,22,222之辈,后面想通了应该这样理解,{1,3}是作用于\d的,而不是作用于\d的一个实例,也就是说,展开后有1到3个\d,嗯,展开,先展开表达式,这样就好理解多了是不是) 2.(\d{1,3}\.){3} 即如果把小括号里的部分看成一个整体,那么我们在这里重复这样操作3次,比如可得到49.123.82. 3.(\d{1,3} 在前面基础上加上一个三位数。 擦,这就是传说中的IP匹配,但 256.257.455.520等不合理的IP也匹配进去了,所以我们还要继续加约束条件: ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。 九、贪婪与懒惰
贪婪匹配即尽可能多的执行字符匹配,比如a.*b,意义上是匹配a开始,b结尾的字符串,当碰到aabab时,贪婪匹配会匹配完整即aabab,可不是aab(懒惰匹配),那么怎么进行懒惰匹配呢,那就是在重复符号(* + ? {...})后追加?,即a.*?b,如此为尽可能少地匹配字符重复。 ——————————————————————————————————————————
十、葬礼
那一刻,
不等花鸟鱼闲,
那一天,
那一夜,
不知南星疲倦,
那一月,
不畏风雨,
那一年,
不寻罗绮逅风情啊,
那一世,
不问今生是何年,
那一瞬,
不觉梦里绕魂牵,
静默默念,