一时半刻谈正则(上篇)
一时半刻谈正则(上篇) 前言
------------------------------------------------------------------------------------------------------------- 说说本文目标 半刻让你明白正则表达式是什么?用处在哪儿。一小时让你能够"会用"正则表达式。(我不相信5分钟快速入门,10分钟基本掌握之类的超级快餐式教程,一小时,是我自己能接受的时间,恕我愚钝)。 ----------------------------------------------------------------------------------------------------------------------------------- 如何使用教程以及其余参考书目 “纸上得来终觉,浅绝知此事要躬行”。请相信这句话吧。 ------------------------------------------------------------------------------------------------------------- 正则表达式是什么?
while($html =~ m{<as+([^>]+)>(.*?)</a>}ig){ $link = $1; $name = $2; if($link =~ m{^http://([^/:]+)(:(d+))?(/.*)?$}i){ $host = $1; $port = $3 || 80; $path = $4 || "/" print "HOST:$hostn"; print "PORT:$portn"; print "PATH:$pathn" }else{ print "非法HTTP URL!"; } print "NAME:$namen"; } ---------------------------------------------------------------------------------------------------------------------------------- 用什么工具编写并测试我的正则表达式?
------------------------------------------------------------------------------------------------------------ 正式开始
完整的正则表达式由两种字符组成。一种被称为"元字符",比如在上述例子中出现的^,*等,另一种就是普通字符,比如匹配HTML超链接时的<a>。与编程语言做类比的话元字符就相当于关键字,普通字符就相当于程序中其余的部分。当然正则中的元字符也可以在某种情况下视为普通字符,在后面我会重点说到。 首先认识两个元字符:脱字符 ^与美元符号$,这两个元字符非常容易理解。在检查一行文本时^代表一行的开始,$代表一行的结束。例如整则表达式 dog可以匹配一行文本中任意位置的dog这个词。但是如果我用 ^dog的话,就只能匹配行首的dog。^dog匹配的是以d作为一行文本的第一个字符,而后紧挨着是一个o,在往后就是一个g。(请按照这种基于字符的匹配方式来思考,^dog完全可以理解为“匹配以dog开头的行”。两种思维都正确,但是第一种更为符合正则匹配原理)如果我用 ^dog$ 的话,就只能匹配一行文本只含有一个单词 dog 的行了。你或许会有疑问,脱字符以及美元符号并没有匹配字符啊?对,请记住,这两个字符只是匹配一个位置,而不是具体的字符。暂且先记住这两个字符值匹配行首和行尾这两个位置。到这里我想你肯定已经了解这两个字符了。那么思考一个问题:正则表达式dog$会匹配什么? ^$又能匹配什么呢?(dog$只匹配行尾字符是先是一个d然后是o然后是g,即以dog结尾的行;^$匹配一个空行) 继续认识一个字符:点号.,点号用来匹配任意字符。(这种说法只是通常情况下,有时点号并不能匹配换行符,不过现在你没必要研究如此深刻)例如对于这个单词 separate,我总是记不住应该是separate还是seperate还是separete,当我需要在文本中查找这个词的时候我可以这样写 sep...te这个时候不管这个词中间是什么,都可以匹配。当然也会匹配到 sepxyzte,sep123te等无意义的单词。稍后你会看到有更好的解决方案。再次说明:如果我们需要在表达式中使用一个“匹配任意字符”的占位符,点号就非常方便。 好了,元字符入门就到这里。告诉你这三个字符,其实是想说在以下表格中我列出的元字符,如果讲的话也会以上两种形式来讲。认识更多常用的元字符:
现在我们说说正则表达式里的单词是什么意思:就是不少于一个的连续的w。另外对中文/汉字的特殊处理具体情况请查看所使用的开发工具以及编程语言对此的说明。元字符的个数远不止此,我就不一一列举出来了,因为重点是教会大家会用。 --------------------------------------------------------------------------------------------------------------------------------- 2.量词以及可选项 如果要想匹配多个字符,或者多个重复类似字符怎么办? 比方说验证你的QQ号,QQ号可能从5位到10位。你完全可以用ddddd 来验证5位的QQ号,但是如果是10位呢?dddddddddd 实在是麻烦了些。放心吧,正则表达式肯定有非常好的解决方案。 认识一个元字符星号: *,它表示出现任意多次,就是[0,∞)。用法是这样的,例如 w*此表达式表示匹配任意多w可以匹配的字符,d*可以匹配任意多次数字,再例如 .*这两个符号的组合就可以匹配任意多字符。量词一般放在某个字符或元字符后面,修饰其容许出现的次数。 下面看加号: +,它表示出现只少一次,就是[1,∞)。用法和 * 一样,放在被修饰字符后面。 如果我想指定某个范围或者某个特定的次数怎么办? 当然可以了,你可以使用{min,max}来修饰范围,例如QQ号 d{5,10}此表达式就能够匹配5-10位的数字组成的字符串。是不是非常容易? 还可以这样d{8}说明d只能重复八次。d{5,}说明d至少重复5次以上。{min,max}你可以称之为区间量词。 最后在认识一个元字符:? 问号表示出现0次或者1次。所以?又被称之为可选项。举个例子,匹配整数。整数包括正整数、负整数以及0。我们可以这样写表达式-?d+能够明白这个式子的含义么?对于 -123,123,-5,6等这样的数字都可以匹配。因为整数前面的负号-被?修饰,是可有可无的。当然这个式子还能够匹配000000这样的式子。随着学习的深入,这样的问题都是可以解决的。 到目前为止,基本的量词我想你已经明白了。现在总结如下:
----------------------------------------------------------------------------------------------------------- 3.字符组 回想我在介绍点号的时候的separate,我只是用sep...te来进行匹配。这可能会匹配到很多无意义的单词。我只是希望在三个点号处出现e、a、 r这三个字符。要是有一种语法,能够让我们在某处列出期望匹配的字符就好了。想必肯定是有。没错,正则表达式结构体:[ ]的存在就是为了解决这个问题,通常它连同被它包围的所有字符这个整体被称之为字符组。为了解决我的问题,表达式可以这么写sep[ae]r[ae]te,你只需要在[ ]里列出期望匹配的字符就行。字符组中的每个字符之间是或的关系。[a] 匹配一个a, [b] 匹配一个b, [ab]匹配a或者b 。在字符组中可以列举任意多个字符例如[1234567] [abcdefghijk] [abcd1234,.@#%!$]等。在匹配HTML标签<H1><H2><H3>...<H6>时可以这么写:<[Hh][123456]> 当然还有更简便的方式就是 <[Hh][1-6]>,这也是我接下来需要说明的重点。在字符组内部,可以通过连字符-来表示范围,例如[a-z]表示26个英文字母,[0-9]表示10个数字[a-zA-z0-9_]基本相当于w。但是如果是这样 [-az]或者[-a-z]的话表示什么呢? [-az]能够匹配-、a、z三个字符中任何一个;[-a-z]能够匹配-,以及26个小写英文字母。 !!!请注意:只 有在字符组内部连字符- 才是元字符,否则它就只是能匹配连字符的普通字符。其实在字符组内部它也不一定就是元字符,如果连字符出现在字符组开头,它表示的就是一个普通字符,而不 是一个范围。同样的道理,问号?和点号. 通常作为元字符有其特殊的含义,但是在字符组内部他们就是普通字符。例如[0-9A-Z_!.?] 里面,这个式子能匹配 0~9,A~Z以及下划线_、叹号! 、和问号?。不妨把字符组看做独立的微型语言,在字符组内部和外部,对于元字符的规定是不同的。 接下来认识一个基于字符组的新名词:排除型字符组。用[^ ]来代替[ ]这个字符组就会匹配任何未列出的字符。例如[^0-9]它会匹配任意一个不是数字的字符。在[^ ]里面列出的是不希望匹配的字符,与[ ]正好相反。你肯定注意到了^不就是我提过的了匹配行首的元字符么?没错,但是在字符组内部,它的含义就变了。关于排除型字符组,需要注意的是:排除型字符组表示“匹配一个未列出的字符”而不是“不要匹配列出的字符”。这两种说法看起来一样。但是还是有非常细微的差别的,请看例子:对于几个单词 : Iraq Iraqa Qantas miqra qintar zaqqum% 如果我使用 q[^u] 来 进行匹配的话 Iraq Qantas 这两个词都是匹配不到的。因为 Qantas无法匹配的原因是Q是大写的。而Iraq的例子比较令人迷惑。正则表达式要求q之后紧跟一个不是u的字符,但是并不是说不跟任何字符。抱歉我 没有说明 Iraq后有一个换行符,我们假设没有。请务必记住:一个字符组,即便是排除型字符组也要匹配一个字符。 ---------------------------------------------------------------------------------------------------------- 4.多选结构 在 上一节字符组部分,你肯定学会了用字符组的方式来列出候选字符。但是事实情况是,字符组所在位置只能匹配一个字符。如果我想要匹配多种情况,那就不得不使 用许多字符组,这实在是不够便捷。一项发明做成什么样才能够称得上是伟大呢?正则表达式被称之为伟大是却有原因的。来学习另一种结构。叫做多选结构。其中最重要的字符,相信大家都很熟悉,就是 |这个竖线它的含义就是 或 。依靠它,我们能把不同的自表达式组合成为一个总的表达式,而这个总表达式又能够匹配任意的子表达式。例如 Bob 和 Robert是两个子表达式 。但是Bob|Robert 就是一个复合的表达式,此表达式可以匹配 Bob或者Robert。在这样的组合中子表达式我们称之为“多选分支”,顾名思义,就是具有多种候选方式的其中的一个分支.。 来看一个例子,对于英文单词 prepare,我也分不清应该是 prepera 还是就是prepare。我想如果在一篇文档中匹配这个词,到现在你应该有可以有不止一种解决方案了。例如prep[ae]r[ae]或者prep[are]{3} (这个是非常不够精确的) 我们用多选结构试试:prepera|prepare 这样也是非常好的,当然这个式子还可以继续精简。往后你会知道还可以这样写 prep(era|are) 这样的式子我会在后面用一整节进行说明。 一个字符组只能匹配目标文本中单个字符,而每个多选结构自身都可能是完整的正则表达式,都可以匹配任意长度的文本,这两者都非常有用。同样,在一个包含多选结构的表达式中使用脱字符^和美元符号时也需要小心。我会在下面进行说明,你只需要跟着我的思路往下走。 使用多选结构注意事项:多选结构中多选分支的先后顺序会在某些情况下对正则匹配结果产生影响。这个某些情况,我只能说涉及到正则引擎这个词。就是正则表达式匹配原理。一般情况下,Java,Perl,.NET,PHP,Python,Ruby,Emacs,vim等 都会因为多选分支排列顺序不同对匹配结果产生影响。因为这些平台、语言使用的正则引擎类型是基本相同的。对于现在的你,没有必要理解很深刻。上述平台的正 则引擎在遇到多选分支时是按照从左至右的顺序进行匹配的,因为多选结构表示或的关系,遇到一个能够匹配成功的,此多选结构就被标记为匹配成功。例如 ^d{10}|d{5}$ 对于0123456789这个序列,它会匹配所有数字。然而对于 ^d{5}|d{10}$ 而言,它只会匹配01234这5个数字。所以多选分支的顺序会对结果产生一定影响。此刻你只需要知道并有印象即可,如果想要深入理解原理,我强烈建议你花点儿时间认真学习一下。不要只看我的教程。 ----------------------------------------------------------------------------------------------------------- 5.括号以及反向引用 又是一个难啃的骨头!在上一节我写了这么个式子 prep(era|are),其中括号是什么意思呢?在正则表达式的王国里,括号()有着非常重要的作用。猜测括号都会有哪些作用呢?通常情况下,我们会把括号括起来的部分当做一个整体,例如四则运算2*(3+4),我们会把3+4的结果视为一个整体。一个括号内部就是一个基本的单元。同样的,在正则表达式里,括号的作用也是如此。括号有两种基本用途:限制多选分支的范围;将若干字符组合为一个单元,受问号或者星号之类量词的作用。 首先看限制多选分支的范围。例如prep(era|are),括号里面的内容会被视作一个独立的单元。假如我们去掉括号的话就变成了prepera|are那么这个表达式表示的是匹配prepera或者are这两个单词了。在这个例子中括号的作用就是限制多选分支的范围。为了使你更明白,我再列举几个例子:Chin(a|ese)这个表达式匹配China或者Chinese; th(at|ey|ere),它匹配that或者they或者there;来个复杂点儿的,sep(a|e)r(a|e)te (看起来是不是和sep[ae]r[ae]挺像?仔细思考不一样的地方)。不知你是否已经明白了括号的这一项用途?如果还不太理解,就请把括号当做分组,一个括号就是一个组。这一组字符在外界看来就是一个独立的单元。其实括号的第三个用途就是分组并记住组里面的内容。 再来看括号的基本用途二。 我们在学习量词时知道了用星号*容许被修饰元字符出现任意多次,用加号+说明至少出现一次,区间量词限定出现范围等。但是这些量词修饰的只是单个字符。如 果我想修饰一个单词,或者一个句子怎么办?这个也可以用括号来解决。例如(hello){3} 能够匹配一行文本中连续出现三个hello的部分,也就是说对于hello world hello hello hellohellohello这个句子,它会匹配最后连续的三个hello。 学到了这里,我想你肯定觉得我给出的例子都太简单了。那么我来给个问题。匹配24小时制的所有时间,例如 08:00am 23:30pm等。想想这个问题,用你已经学到的知识可以怎么解决? 解决问题首先要分析问题,解决正则表达式问题首先要分析待匹配的字符串的结构特点。先用dd:dd[ap]m 或者[0-9][0-9]:[0-9][0-9](am|pm)看是否能解决?通常情况下这个表达式就可以解决了。但是此式子能够匹配到 25:99am这样毫无意义的式子。我们下一步需要的是限定一下范围。对于分钟而言,只能是00~59首先看这样是否可行呢?[0-5][0-9] 用来匹配分钟,这是可以的,还可以这样[0-5]d,也还可以(0|1|2|3|4|5)d,也还可以[012345]d不过后两个是在不怎么好看。分钟解决了,请你思考小时部分应该如何解决呢? 我们知道最大只能是23最小是00,这个问题有点儿复杂。不过也是可以很容易解决的。我先把解决方案写出来:(0d|1d|2[0-3]):[0-5]d[ap]m这个式子是否可行呢? 我们还可以提出问题,假如容许出现 9:15am这样的形式呢? 就是小时部分可以是单个字符,这完全是合法的时间表示。想想,小时部分既能出现一个字符也可以是两个字符,用什么方法可以解决?((0?[0-9]|1[0-9]|2[0-3]):[0-5][0-9])[ap]m看这个式子是不是很晕?但是它确实是可以工作的,且不说效率问题。因为正则表达式效率问题内容很深。打造高效的正则表达式是一项非常困难的任务。好在现在只是让你学会用它。好了,24小时这个例子就到这里。 留一个问题如何匹配IP地址? 例如 10.186.144.100, 255.255.255.0, 100.86.92.55,请你试着解决这个问题。相信会有非常大的收获的。(d{1,3}.){3}d{1,3}是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:d{1,3}匹配1到3位的数字,(d{1,3}.){3} 匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字(d{1,3})。不幸的是,它也将匹配999.999.999.999这种不可能存在的IP地址。如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:(([01]?dd?|2[0-4]d|25[0-5]).){3}([01]?dd?|2[0-4]d|25[0-5])。理解这个表达式的关键是理解[01]?dd?|2[0-4]d|25[0-5],这里我就不细说了,你自己应该能分析得出来它的意义。 学到这里,你会发现,正则表达式非常灵活,对于同一个问题可能会有非常多的不同解决方式,写出来的表达式也是各有千秋。记住一个准则就可以了:使用正则表达式往往需要在待匹配文本和选取正则表达式之间做出衡量。首先你先要了解带匹配的数据,然后根据数据形式,设计合适的正则表达式。没有最好,只有适合。 我们来学习括号的第三种用途:分组与反向引用。 在许多开发平台中正则表达式中的括号能够记住它所匹配到的内容。先来解决一个问题。如何寻找一句话中重复出现的单词? 比方说 How do you do? 或者 I can because i think i can.我们将带着这个问题来学习括号的这部分知识。如果我们确切的可以知道重复的单词是什么,比方说是do那么我们就可以简单的这么写:do.*do不过它会匹配到 do .... doing 这样的部分。同时也会有其他五花八门的匹配结果。这显然不是我们所希望的。这样匹配的前提还是,我们知道可能重复的单词是什么,假如我们不知道重复的单词是什么呢?穷举所有可能出现的重复单词显然是不可能完成的任务。如果我们先匹配一个单词,接下来检查“后面的单词是否和它一样”,这就好办多了。这就是反向引用的作用。如果你的正则表达式支持反向引用,我们就可以借助于它来完成这个任务。 首先我们定义这样一个匹配单词的式子:[a-zA-Z]+ 然后用括号将它括起来,作为一个分组也就是一个单元([a-zA-Z]+)然后这个单元后可以有多个其余字符。([a-zA-Z]+).*为了检验重复,我们要这样写([a-zA-Z]+).*1,你肯定看到了1。没错1的意思就是第一个分组。同理2表示第二个分组,3表示第三个分组。前面的括号用于分组并记住其所匹配到的内容,后面通过1等来引用前面分组的内容,这就是反向引用。那么分组是怎么划分的呢? 括号是允许嵌套的,例如:([a-z])([0-9])12这个你可以理解1引用的是([a-z]),2引用的是([0-9]),那么([a-z]([0-9])(A-Z))(#|@)这样的式子呢?(虽然找个式子意义不大)。记住括号是按照开括号 ( 从左至右出现的顺序进行的,分组编号是根据开括号的顺序编排的。在([a-z]([0-9])(A-Z))(#|@)个式子中1引用过的是([a-z]([0-9])(A-Z)),2引用的是([0-9]),3引用的是(A-Z),4引用的是(#|@)。明白了么? 你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?<name> )(或者把尖括号换成单引号' 也行:(?'name' ), 这样就把此括号的组名指定为name了。要反向引用这个分组捕获的内容,你可以使用k<name>即可。 到这里我们学习了字符组,多选结构以及括号的作用和使用方法,累了么?先看过下面的重要总结。然后再休息一下,马上进入下半部分。剩下的部分相对于前面这些来说可能稍微难以理解一些。不过坚持一下,你离会用正则表达式不远了。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- cocos2d-x之Android平台的Java与JS互调
- Let’s Build |> 使用Elixir,Phoenix和React打造
- 无法识别apacer CF(compact flash)
- c# – 如何测试流畅的NHibernate的PersistenceSp
- Flex与.NET互操作 基于WebService的数据访问
- 【Sqlite】Can't upgrade read-only databas
- 关于XML字符串和XML Document之间的转换《转》
- 正则表达式-理论基础篇
- Ruby语法问题:Rational(a,b)和Rational.new!(a
- 保存Excel工作簿中所有的嵌入文件