Project1

标题: 【教程】正则表达式学习笔记 [打印本页]

作者: RyanBern    时间: 2014-3-24 13:27
标题: 【教程】正则表达式学习笔记
本帖最后由 RyanBern 于 2015-2-23 20:52 编辑

前言
       多年以来,正则表达式一直是我的一块心病。相信很多人也有类似的体会:看正则表达式就好像“看天书一般”。前几日,我痛下决心准备将正则表达式里里外外弄个清楚。于是将F1翻了个遍,又拿出了Java教程参考了一番,到今天总算有点开窍了。同时也发现了F1里面写的少许错误之处,因此特地拿出来这份“学习笔记”,和那些同样被正则表达式困扰的同学们分享一下。(喂喂喂,你不就是要写个教程么?费话真多……)
•什么是正则表达式
       所谓正则表达式,可以将其理解为有特殊含义的字符串(String),可以作为字符串匹配的模式串(Pattern)来使用,比纯字符串有更高的灵活性。正则表达式最重要的作用就是进行模式匹配,这为Ruby大范围处理文本信息提供了方便。至于匹配的具体过程我们无需去关心,因为毕竟我们走的是面向对象的编程道路,对于那些琐碎的中间过程可以不必考虑,不过有兴趣的同学可以百度一下“模式匹配算法”、“KMP模式匹配”,但是在这里我们不去进行讨论。
•正则表达式的初始化
可以用一对双斜线'//'来初始化一个空的正则表达式,这个和数组用一对方括号'[]'初始化是一样的,也可以像字符串常量那样初始化正则表达式,如
  1. regex = /abc/
复制代码

•正则表达式内部可以写什么
同字符串一样,正则表达式里面几乎可以写任意的字符,但是正则表达式中的字符有很强的特殊性,当然,有些无法解析的正则表达式不能通过编译。
•正则表达式的匹配规则
先了解单字符匹配的概念:正则表达式中的一个字符和目标串中的一个字符(或多个字符)成功匹配。在这里“单”是对于正则表达式而言的,也就是说,正则表达式里面的一个字符可能会对应目标串里面的多个字符,具体的例子我们放到后面来说。
我们称一个正则表达式和一个字符串匹配,如果存在字符串的一个子串str,使得子串满足正则表达式的模式条件。
这样说起来有些难懂,我们举一个例子:
  1. regex = /abc/
  2. str = "aabcd"
  3. p regex =~ str #=> 1 匹配成功
复制代码

将一个正则表达式和一个字符串进行匹配,最常用“=~”符号。
在这里,我们看到,字符串str有一个子串是"abc",而正则表达式/abc/正好和这个子串一一配对,这就是我们所说的满足匹配模式条件。也叫做一次成功匹配。
注意,=~符号有返回值,如果匹配成功,则返回子串首字符的索引(index),如果失败,则返回nil。
另外,正则表达式匹配有一些其他性质:

连续匹配:一个正则表达式只能和目标串的一个子串匹配,而不能和目标串的若干个子串连接之后得到的新串匹配。例如:/ab/能和"abcd"匹配,但不能和"acdb"匹配。(即ab必须是连续的)
整体匹配:匹配必须是正则表达式的整体,而不能是正则表达式的一部分。
首次匹配:如果某正则表达式和某字符串的多个子串匹配,则认为匹配的子串是满足整体匹配的左起第一个子串。
  1. p /ab/ =~ "cabababa" # => 1 而不是3或者5
复制代码

贪婪匹配/懒惰匹配:正则表达式的匹配的特殊原则,这个在后面的数量指定符(Quantifier)里面会提到。
•元字符
正则表达式中,字符分为元字符和普通字符。普通字符只能和它本身匹配,元字符就是有特殊含义的字符。注意元字符和字符串中的转义字符的区别。转义字符属于普通字符,而元字符是某种意义下的“通配符”。
在Ruby的正则表达式中,不带反斜线的字母和数字和带反斜线的符号都是普通字符。而带反斜线的字母和数字和不带反斜线的符号一般都是元字符。这样说起来比较拗口,我们举几个例子吧。
普通字符:a,b,\n,\\
元字符:.,^,\w,\D
•Regexp类
在列出所有元字符之前,先插一句“没用的”。
Regexp类,即正则表达式的类,实际上是对正则表达式的封装。在Ruby中可以直接用一对“//”来初始化一个正则表达式。这和数组类(Array)有点相似,数组可以用一对“[]”来初始化。
类方法:
Regexp.last_match:返回最近一次正则表达式匹配的MatchData对象,这个方法等同于访问内部变量$~。
Regexp.last_match([nth]):若nth=0,则返回匹配的整个字符串$&,否则返回第nth个括号相匹配的字符串$1,$2,$3……这个方法等同于访问$&,$1,$2……
实例方法:
self =~ string:和String对象进行匹配,匹配成功匹配的所在位置,匹配失败返回nil。
self === string:和String对象进行匹配,匹配成功返回true,匹配失败返回false。在这里看到,由于Regexp类以及实现了'==='方法的特殊化,所以应用到case~when表达式中是非常方便的。
self.match(str):和str进行匹配,返回匹配的结果MatchData。
to_s:返回正则表达式的字符串形式。
•MatchData类
储存正则表达式匹配中间过程信息的类,可以通过$~获取。
实例方法:
self[nth]:返回第nth个匹配部分。
post_match:返回匹配部分之后的字符串。
pre_match:返回匹配部分之前的字符串。
to_a:返回$&,$1,$2,……的数组形式。
to_s:返回整个匹配部分。
•几个内部全局变量
和模式匹配有关的内部全局变量已经在Ruby中定义,可以在进行一次匹配之后使用它们。
$~:保存最近一次匹配的信息,是一个MatchData的实例,如果不存在则为nil。
$&:保存最近一次成功匹配的字符串的子串,如果匹配失败,则为nil。
$1,$2,$3……:保存最近一次成功匹配中,和第1、2、3……个括号匹配的字符串。如果没有对应括号或则匹配失败,则为nil。

•各种元字符的解析
在F1中搜索正则表达式,我们可以看到里面列出了所有元字符的用法。这些东西没必要记住,需要的时候可以自行翻阅。不过F1里面对某些元字符描述的不是特别清楚,在这里进行如下补充。
.:匹配除换行符\n以外的任何一个字符,如果使用正则表达式选项m,则可以匹配任意字符。
^:行首,匹配输入字符串头部或者换行符\n之后的位置。
  1. p "\n".gsub(/^/,"o") #=> "o\n"
复制代码

注1:这里\n之后如果什么都不写,不算另起一行。只有形如"\nx"的字符串才算是两行,每一行的行首都满足整体匹配的条件。
注2:元字符“^”匹配的是字符串头部,即字符串每一行开始部分之前的那个地方,而不是字符串每一行的第一个字符。
注3:这里用gsub方法意在指示出整个字符串有多少个可以匹配的子串,并不和我们刚才所说的“首次匹配”相矛盾。
$:行尾,匹配输入字符串尾部或者换行符\n之前的位置。
  1. p "\n".gsub(/$/,"o") #=> "o\no"
复制代码

注:元字符“$”匹配的是字符串中换行符之前的那个位置,而不是换行符本身或者换行符之前的那个字符,另外,“$”也可匹配字符串最后面的那个位置(可以由上一个例子看出)。
\w:匹配任何数字字母字符,包括下划线,等同于[0-9a-zA-Z_]。
注:经过测试表明,\w也可以匹配汉字。(感谢P叔提出的修改意见)
\W:匹配\w以外的字符。
\s:匹配任何空字符,等同于[ \n\r\t\f\\v],注意,空格" "也算空字符。
\S:匹配任何非空字符,即\s以外的字符。
\d:匹配一个数字字符,等同于[0-9]
\D:匹配\d以外的字符。
\A:匹配字符串头部,和^不同,换行对其无影响。
  1. p "\nx".gsub(/\A/,"o") #=>"o\nx"
复制代码

上面的例子中,字符x在第二行里面,但是\A没有匹配到x之前\n之后的位置,这就说明\A只能匹配字符串最前面的位置。
\Z:匹配字符串尾部,字符串以\n结束,则匹配换行符之前的位置。
  1. p "x\n".gsub(/\Z/,"o") #=>"xo\no"
复制代码

注意和$的区别,对于"x\np\n",用\Z匹配得到的是"x\npo\no",用$得到的是"xo\npo\no"。也就是说$能匹配所有\n之前的位置,而\Z只能匹配最后一个\n之前的位置。
\z:匹配字符串尾部,换行符对其无影响。
  1. p "x\np\n".gsub(/\z/,"o") #=>"x\np\no"
复制代码
,也就是说\z只能匹配字符串最后面的那个位置,和\A有点相互对应的意思。
下面的一些元字符带有圆括号结构,如果你是自上而下阅读,建议先跳过这部分,等你了解了正则表达式的群组化之后再来进行这里的阅读。
(?# ):注释,括号中的任意字符将被忽略。
注1:这部分括号不算作后项引用考虑的范围之内。
  1. /(.)(?#zzr)(zzr)/ =~ "azzr"
  2. p $2 #=>"zzr"
复制代码

注2:如果括号的开头不是'?#',则不把它看作注释,而是看作一个数量指定符'?'和普通字符'#'。
(?:):单一群组化,它不具备后项引用功能,不为后项引用提供服务。
(?=):先行(lookahead),使用先行的内部pattern指定匹配的位置,相当于做一个判断。
注1:和先行匹配的部分不会出现在比较结果里面,它的作用只是判断。判断结束之后,如果匹配,则回到先行匹配的开始处进行后面的匹配,否则认为匹配失败。
注2:/(?=zzr)btb/表示的其实是同时和zzr和btb匹配的字符串,而不是"zzrbtb"这样的连写。
(?!):否定先行(negative lookahead),使用否定的pattern指定匹配位置。
注1:和先行匹配同理,否定先行的匹配部分不会出现在比较结果里面。
注2:/(?!zzr)btb/表示的是不和zzr匹配并且和btb匹配的子串,而不是一个不和zzr匹配的子串和一个和btb匹配的子串的连写。
注3:/zzr(?!btb)/表示和子串"zzr"匹配,但是这个子串后面的若干位不能和btb匹配,匹配的结果依然是否定先行以外的部分(即$&的值是"zzr")。
(?ixm-ixm):在正则表达式中临时改变正则表达式的匹配选项ixm。
例:(?i)表示打开i选项,(?-i)表示关闭i选项。
(?ixm:):在括号范围内临时改变正则表达式的匹配选项,括号外恢复正则表达式的初始选项。

•字符簇
用[]来指定字符簇,匹配的时候,字符串中的字符可以匹配出现在[]中字符的任意一个。注意,一个字符簇只能代表一个字符,只是这个代表的字符具有选择性,因此[ab]能匹配字符a或字符b,而不是/ab/。从某种意义上来讲,字符簇也算作是一种“自定义的通配符”。
例如[abc]能匹配a,b,c中的任何一个,/[abc]xx/可以匹配"axx","bxx","cxx"。
[]中可以用^,-,来表示范围,[a-z]表示匹配a到z中任意一个,[^abc]表示匹配除了abc以外的任何一个字符,^不在开头表示其字符本身,-在头尾也表示其字符本身,另外“.”元字符在方括号中只与它本身匹配。
对字符簇内的集合允许取交、并、差。
[a-c[mp]]能和a,b,c,m,p任何一个匹配。
[a-z&&[d-f]]能和d,e,f任何一个匹配,相当于取交集。
[a-f&&[^bc]]能和a,d,e,f任何一个匹配,相当于取差集。
以上三个字符簇都是合法的。
另外不允许使用空的字符簇[]或者是只有^的字符簇[^],字符簇内的字符可以重复(例如[aaa]),能通过编译,但是没必要这样做。
•群组化与后项引用
使用正则表达式中的圆括号()可以将正则表达式群组化,以供后项引用使用。注意,即使是群组化,依然要求目标串和正则表达式完全匹配,括号的作用只是保存部分匹配结果来达到为后项引用使用的目的。这样做能为我们之后使用匹配过程中的信息提供方便。
例如:/((zzr)ZZR)\1\2/等同于/((zzr)ZZR)zzrZZRzzr/。
  1. /((zzr)ZZR)/ =~ "zzr"
  2. p $1 #=> nil
复制代码

部分匹配,但是整体不匹配,返回结果是nil,$1也为nil。
对应的括号必须位于后项引用的左侧,这个道理是显然的,因为只有对这部分进行匹配之后,才能确定\1引用的是什么。\1表示左起第一个左括号包围的群组,由于括号匹配具有唯一性,因此\1的值就会被完全确定。
注:内部变量$n(n是正整数)指的是在匹配成功的情况下,左起第n个左括号(及其对应的右括号)所包围的群组对应匹配的子串的值。由于括号匹配的唯一性,$n的值也可以被完全确定下来。
注:群组化和数量指定符一起使用的时候,后项引用和内部全局变量($1,$2,...)可能会失效(或者达不到预期效果)。
  1. /(ab[cde])+/ =~ "abcabdabe"
  2. p $1,$2,$3 #=> "abe", nil, nil
复制代码

因此对某一群组使用数量指定之后,不要用$1,$2,或者是后项引用。

引入这种群组化的思想有什么用?我们看下面一个例子:
例:写出一个正则表达式,它能匹配连续两个相同字符。
解:/(.)\1/
如果没有群组化和后项引用,似乎会很困难。这也就说明,含有后项引用的正则表达式的值是动态确定的,而不是静态的,也就是说这种正则表达式的值在程序运行过程中才能被确定下来,编译的时候是无法确定其值的。
注:处于括号内的后项引用通常不起作用。
  1. p /(\1)/ =~ "x" # => nil
复制代码

因为这个时候\1所代表的还没有确定。
但是,如果包含后项引用的最外层括号的序号大于后项引用的序号,那么后项引用是有效的。
  1. /(\w\d)(\1)1/ =~ "z1z11"
  2. p $&, $1, $2 # => "z1z11" "z1" "z1"
复制代码

•后项引用和八进制代码
这一部分是对F1相应部分的解读,感觉没必要抠得太深。写程序最忌讳二义性,而这里抠的恰恰就是二义性的问题。因此造出一堆没什么意义的歧义正则表达式是及其无聊的事情。不过既然提到了,就在这里说说,感觉应该可以跳过这一部分。
后项引用超过两位时,和八进制代码容易混淆。Ruby中规定,如果有对应的括号,那么就为后项引用,无对应括号,就解释为八进制代码。
另外,在正则表达式中,如果要使用1位八进制代码,必须以0打头,如\01等。因为后项引用不可能存在类似\0的结构。
容易引起歧义时,要用括号来划分群组。
  1. /(.)(\1)1/ =~ "111" #=> 0
  2. /(.)\11/ = ~ "a\11" #-> 0
复制代码

•字符的数量指定(Quantifier)
用于指定子表达式出现次数的数量指定符,通常为* + ? {m} {m,n}等,它们的作用就是指定紧跟在它们前面的字符(或者字符群组)重复出现的次数。刚才我们说过,一个正则表达式的字符可能会匹配目标串的多个字符。在这里我们略去这些Quantifier的说明,F1里面已经说得很详细了。唯一要注意的问题就是大家要深入理解“尽量匹配短的”和“尽量匹配长的”这两句话的含义,在最前面的匹配性质那里,我们提到了“贪婪匹配/懒惰匹配”,在这里我们就详细说一下。
默认情况下,我们的正则表达式的匹配原则是贪婪匹配,即尽可能匹配较长的字符串。这样说有点不太清楚,说得详细点,就是如果在某次匹配过程中已经找到了匹配的内容(此时字符串计算机还没有“看完”),该次匹配并不会停止,计算机会奢望“既然能匹配到这里,那么有没有更长的呢”。这种匹配的“思想”就是所谓贪婪匹配。注意,贪婪匹配和我们的首次匹配不要弄混了,和正则表达式匹配的子串的位置永远是最靠前的。
  1. /\|\d+\|/ =~ "|123|12345|"
  2. p $& #=> "|123|"
复制代码

在上面例子中,匹配的部分是"|123|"而非"|12345|",所以贪婪匹配不是说匹配一个最长的子串,而是说在子串的起始位置相同时,匹配的部分尽可能长。
那么懒惰匹配的含义就正好相反,如果在匹配过程中,已经找到了匹配的内容,那么匹配结束。这种懒惰的思想就是所谓的懒惰匹配。
懒惰匹配的符号是'?',这个符号通常跟在其他数量指定符后面表示这个是一次懒惰匹配。
例:
  1. str = '<td>abc</td><td>def</td>'
  2. regex1 = /<td>.*<\/td>/  # 贪婪匹配
  3. regex2 = /<td>.*?<\/td>/ # 懒惰匹配
  4. regex1 =~ str
  5. p $& # => "<td>abc</td><td>def</td>"
  6. regex2 =~ str
  7. p $& # => "<td>abc</td>"
复制代码

下面这几个符号都十分常用,别的可以不记,这些最好记住吧:
?等同于{0,1};*等同于{0,};+等同于{1,};??等同于{0,1}?
•正则表达式的选项
初始化正则表达式时,结尾斜线后面紧跟的就是正则表达式的选项,我们可以将它理解成正则表达式的一种自定义可选匹配规则。
i:匹配的时候不区分大小写字母,即大写的A可以和小写的a匹配成功。
m:多行模式,使得”.”元字符能匹配换行符”\n”。
x:忽视正则表达式中的空白字符,相当于删去所有空白字符而后重新连接的正则表达式。
注:空白字符的定义和C语言中的定义是一样的,相当于使得isspace为1的字符。
o:计算正则表达式时,只有第一个内嵌表达式是有效的。
例:初始化一个带有选项的正则表达式
  1. regex = /abc/i # 不区分大小写字母
  2. p regex =~ "Abc" # => 0
复制代码


还记得我们刚才跳过的那部分元字符吗?我们现在可以跳回去看看了。不过如果依然看不懂的话,那就没办法了,我水平有限啊。

•正则表达式在字符串中的重要应用
下面列出的是String类的一些方法,这些方法会用的正则表达式。
self[regexp]:返回与regexp首次匹配的子字符串。
slice(regexp):等同于self[regexp]。
scan(regexp):用regexp对字符串反复进行匹配操作,返回匹配成功之后的所有子串的数组。
gsub(regexp, str):将字符串中和regexp匹配的所有子串用串str代替。
(gsub的意义是g(lobal)sub(stitute),意思为全局替换)
split(regexp):按照正则表达式将字符串分割为子串的数组。
例:
  1. str = 'abc@@def@gh@@@@ijk'
  2. p str.split(/@+/) # => ["abc", "def", "gh", "ijk"]
复制代码

注:scan和gsub成功进行一次匹配时,下一次匹配将从上一次匹配成功的地方进行,也就是说"aaaaa".scan(/aa/)会得到["aa","aa"],而不是["aa","aa","aa","aa"]。
sub(regexp, str):将字符串中和regexp匹配的第一个子串用str代替。除去一次匹配这个特点之外,其余的规则和gsub相同。

有了这些基础,我们就能看懂Window_Message的有关部分和增强对话框的脚本了。

作者: taroxd    时间: 2014-3-25 17:56
其实正则表达式(经常使用的部分)还是很简单的啦,就是看上去不太友好而已

(gsub的意义是g(lobal)sub(stitute),意思为全局替换(大雾))

哪里有大雾?gsub难道不是这意思么??

还有这哪里是伪教程?哪里伪了?

顺便提一个比较不错的网站:http://rubular.com/
作者: 怪蜀黍    时间: 2014-3-28 16:27
用得着的时候吾会来这里查询的,加油!
作者: yagami    时间: 2014-3-31 22:25
连c++11都原生有正则表达式了 看来有时间得研究下这天书
作者: 无脑之人    时间: 2014-4-1 12:54
c++11有了正则好开心=w=以后处理字符串终于不用那么别扭了www
说起来LZ教一下怎么制作正则表达式库吧∑(っ °Д °;)っKleene*完全不会做∑(っ °Д °;)っ
作者: taroxd    时间: 2014-7-6 09:49
slice[regexp]
是小括号()啦~

对了,有兴趣把这一段复制到VA区那个活动么?只要「正则表达式在字符串中的重要应用」这一段稍微多写点就可以了。

你可以用
  1. [fold=标题]内容[/fold]
复制代码
来把东西折叠起来,就像那个活动贴中其他人所做的那样。

感谢你的辛苦付出。
作者: RyanBern    时间: 2015-2-20 17:03
本帖最后由 RyanBern 于 2015-2-20 17:10 编辑

对教程进行了微小的更新,修改的一些错误。
增加内容:
1.群组化数量指定之后,后项引用和$n会失效的问题。
2.贪婪匹配/懒惰匹配详解。
3.String#split的简略用法。
作者: 7408    时间: 2015-2-20 18:29
被LZ说中了、正则表达式的确是心病呐 一直想找时间弄清楚来着~然后就看到LZ的教程..好开心~
多谢LZ精心奉献的教程..66RPG热心的人果然很多~
作者: taroxd    时间: 2015-2-22 09:59
本帖最后由 taroxd 于 2015-2-22 10:02 编辑

RUBY 代码复制
  1. /|\d+|/ =~ "|123|12345|"
  2. p $& #=> "|123|"


这个你真的测试过吗?这个正则表达式匹配 “空”或者“1个或多个数字”或者“空”
“空”匹配了字符串开头,因此 $& 为空字符串 ""
作者: 952193683    时间: 2015-2-24 22:29
有教程文档吗?求!




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1