Regular Expression心得体会


前言
Regular Expressions( 以下用 RE 称呼 ) 对小弟来说一直都是神密的地带,看到一些网络上的大大,简单用 RE 就决解了某些文字的问题,小弟便兴起了学一学 RE 的想法,但小弟天生就比较懒一些,总希望看有没有些快速学习的方式,于是小弟又请出 Google 大神,藉由祂的神力,小弟在网络上找到了 Jim Hollenhorst 先生的文章,经过了阅读,小弟觉得真是不错,所以就做个小心得报告,跟 Move-to.Net 的朋友分享,希望能为各位大大带来一丁点在学习 RE 时的帮助。 Jim Hollenhorst 大大文章之网址如下,有需要的大大可直接连结。
The 30 Minute Regex Tutorial By Jim Hollenhorst
** http://www.codeproject.com/useritems/RegexTutorial.asp **

什么是 RE?
想必各位大大在做文件查找的时侯都有使用过万用字符 ”” ,比如说想查找在 Windows 目录下所有的 Word 文件时,你可能就会用 ”.doc” 这样的方式来做查找,因为 ”*” 所代表的是任意的字符。 RE 所做的就是类似这样的功能,但其功能更为强大。
写程序时,常需要比对字符串是否符合特定样式, RE 最主要的功能就是来描述这特定的样式,因此可以将 RE 视为特定样式的描述式,举个例子来说, ”\w+” 所代表的就是任何字母与数字所组成的非空字符串 (non-null string) 。在 .NET framework 中提供了非常强大的类别库,藉此可以很轻易的使用 RE 来做文字的查找与取代、对复杂标头的译码及验证文字等工作。
学习 RE 最好的方式就是藉由例子亲自来做做看。 Jim Hollenhorst 大大也提供了一个工具程序 Expresso( 来杯咖啡吧 ) ,来帮助我们学习 RE ,下载的网址是
** http://www.codeproject.com/useritems/RegexTutorial/ExpressoSetup2_1C.zip ** 。
接下来,就让我们来体验一些例子吧。

一些简单的例子
假设要查找文章中 Elvis 后接有 alive 的文字符串的话,使用 RE 可能会经过下列的过程,括号是所下 RE 的意思:
1. elvis ( 查找 elvis)
上述代表所要查找的字符顺序为 elvis 。在 .NET 中可以设定乎略字符的大小写,所以 ”Elvis” 、 ”ELVIS” 或者是 ”eLvIs” 都是符合 1 所下的 RE 。但因为这只管字符出现的顺序为 elvis ,所以 pelvis 也是符合 1 所下的 RE 。可以用 2 的 RE 来改进。
2. \belvis\b ( 将 elvis 视为一整体的字查找,如 elvis 、 Elvis 乎略字符大小写时 )
“\b” 在 RE 中有特别的意思,在上述的例子中所指的就是字的边界,所以 \belvis\b 用 \b 把 elvis 的前后边界界定出来,也就是要 elvis 这个字。
假设要将同一行里 elvis 后接有 alive 的文字符串找出来,此时就会用到另外二个特别意义的字符 ”.” 及 ”” 。 ”.” 所代表就是除了换行字符的任意字符,而 ”” 所代表的是重复 * 之前项目直到找到符合 RE 的字符串。所以 ”.” 所指的就是除了换行字符外的任意数目的字符数。所以查找同一行里 elvis 后接有 alive 的文字符串找出来,则可下如 3 之 RE 。
3. \belvis\b.
\balive\b ( 查找 elvis 后面接有 alive 的文字符串,如 elvis is alive)
用简单之特别字符就可以组成功能强大的 RE ,但也发现当使用越来越多的特别字符时, RE 就会越来越难看得懂了。

再看看另外的例子

组成有效的电话号码
假使要从网页上收集顾客格式为 xxx-xxxx 的 7 位数字的电话号码,其中 x 是数字, RE 可能会这样写。
4. \b\d\d\d-\d\d\d\d ( 查找七位数字之电话号码,如 123-1234)
每一个 \d 代表一个数字。 ”-” 则是一般的连字符号,为避免太多重复的 \d , RE 可以改写成如 5 的方式。
5. \b\d{3}-\d{4} ( 查找七位数字电话号码较好的方法,如 123-1234)
在 \d 后的 {3} ,代表重复前一个项目三次,也就是相等于 \d\d\d 。

RE 的学习及测试工具 Expresso

因为 RE 不易阅读及使用者容易会下错 RE 的特性, Jim 大大开发了一个工具软件 Expresso ,用来帮助使用者学习及测试 RE ,除了上面所述的网址之外,也可以上 Ultrapico 网站 ( ** http://www.Ultrapico.com ** ) 。安装完 Expresso 后,在 Expression Library 中, Jim 大大把文章的例子都建立在其中,可以边看文章边测试,也可以试着修改范例所下的 RE ,马上可以看到结果,小弟觉得非常好用。各位大大可以试试。

.NET 中 RE 的基础概念
特殊字符
有些字符有特别的意义,比如之前所看到的 ”\b” 、 ”.” 、 ”*” 、 ”\d” 等。 ”\s” 所代表的是任意空格符,比如说 spaces 、 tabs 、 newlines 等 . 。 ”\w” 代表是任意字母或数字字符。

再看一些例子吧
6. \ba\w*\b ( 查找 a 开头的字,如 able)
这 RE 描述要查找一个字的开始边界 (\b) ,再来是字母 ”a” ,再加任意数目的字母数字 (\w*) ,再接结束这个字的结束边界 (\b) 。
7. \d+ ( 查找数字字符串 )
“+” 和 ”*” 非常相似,除了 + 至少要重复前面的项目一次。也就是说至少有一个数字。
8. \b\w{6}\b ( 查找六个字母数字的字,如 ab 123c )
下表为 RE 常用的特殊字符

. 除了换行字符的任意字符
\w 任意字母数字字符
\s 任意空格符
\d 任意数字字符
\b 界定字的边界
^ 文章的开头,如 ”^The'' 用以表示出现于文章开头的字符串为 ”The”
$ 文章的结尾,如 ”End$” 用以表示出现在文章的结尾为 ”End”
特殊字符 ”^” 及 ”$” 是用来查找某些字必需是文章的开头或结尾,这在验证输入是否符合某一样式时特别用有,比如说要验证七位数字的电话号码,可能会输入如下 9 的 RE 。
9. ^\d{3}-\d{4}$ ( 验证七位数字之电话号码 )
这和第 5 个 RE 相同,但其前后都无其它的字符,也就是整串字符串只有这七个数字的电话号码。在 .NET 中如果设定 Multiline 这个选项,则 ”^” 和 ”$” 会每行进行比较,只要某行的开头结尾符合 RE 即可,而不是整个文章字符串做一次比较。
转意字符 (Escaped characters)
有时可能会需要 ”^” 、 ”$” 单纯的字面意义 (literal meaning) 而不要将它们当成特殊字符,此时 ”\” 字符就是用来移除特殊字符特别意义的字符,因此 ”^” 、 ”\.” 、 ”\\” 所代表的就是 ”^” 、 ”.” 、 ”\” 的字面意义。

重复前述项目
在前面看过 ”{3}” 及 ”*” 可以用来重复前述字符,之后我们会看到如何用同样的语法重复整个次描述 (subexpressions) 。下表是使用重复前述项目的一些方式。

  • 重复任意次数
    + 重复至少一次
    ? 重复零次或一次
    {n} 重复 n 次
    {n,m} 重复至少 n 次,但不超过 m 次
    {n,} 重复至少 n 次

再来试一些例子吧
10. \b\w{5,6}\b ( 查找五个或六个字母数字字符的字,如 as25d 、 d58sdf 等 )
11. \b\d{3}\s\d{3}-\d{4} ( 查找十个数字的电话号码,如 800 123-1234)
12. \d{3}-\d{2}-\d{4} ( 查找社会保险号码,如 123-45-6789)
13. ^\w* ( 每行或整篇文章的第一个字 )
在 Espresso 可试试有 Multiline 和没 Multiline 的不同。

匹配某范围的字符
有时需要查找某些特定的字符时怎么辨 ? 这时中括号 ”[]” 就派上了用场。因此 [aeiou] 所要查找的是 ”a” 、 ”e” 、 ”i” 、 ”o” 、 ”u” 这些元音, [.?!] 所要查找的是 ”.” 、 ”?” 、 ”!” 这些符号,在中括号中的特殊字符的特别意义都会被移除,也就是解译成单纯的字面意义。也可以指定某些范围的字符,如 ”[a-z0-9]” ,所指的就是任意小写字母或任意数字。

接下来再看一个比较初复杂查找电话号码的 RE 例子
14. \(?\d{3}[( ] \s?\d{3}[- ]\d{4} ( 查找十位数字之电话号码,如 (080) 333-1234 )
下这样的 RE 可查找出较多种格式的电话号码,如 (080) 123-4567 、 511 254 6654 等。 ”\(?” 代表一个或零个左小括号 ”(“ ,而 ”[( ]” 代表查找一个右小括号 ”)” 或空格符, ”\s?” 指一个或零个空格符组。但这样的 RE 会将类似 ”800) 45 -3321” 这样的电话找出来,也就是括号没有对称平衡的问题,之后会学到择一 (alternatives) 来决解这样的问题。

不包含在某特定字符组里 (Negation)
有时需要查找在包含在某特定字符组里的字符,下表说明如何做类似这样的描述。
\W 不是字母数字的任意字符
\S 不是空格符的任意字符
\D 不是数字字符的任意字符
\B 不在字边界的位置
[^x] 不是 x 的任意字符
[^aeiou] 不是 a 、 e 、 i 、 o 、 u 的任意字符

15. \S+ ( 不包含空格符的字符串 )

择一 (Alternatives)
有时会需要查找几个特定的选择,此时 ”|” 这个特殊字符就派上用场了,举例来说,要查找五个数字及九个数字 ( 有 ”-” 号 ) 的邮政编码。

16. \b\d{5}-\d{4}\b|\b\d{5}\b ( 查找五个数字及九个数字 ( 有 ”-” 号 ) 的邮政编码 )
在使用 Alternatives 时需要注意的是前后的次序,因为 RE 在 Alternatives 中会优先选择符合最左边的项目, 16 中,如果把查找五个数字的项目放在前面,则这 RE 只会找到五个数字的邮政编码。了解了择一,可将 14 做更好的修正。

17. (\(\d{3}\)|\d{3})\s?\d{3}[- ]\d{4} ( 十个数字的电话号码 )

群组 (Grouping)
括号可以用来介定一个次描述,经由次描述的介定,可以针对次描述做重复或及他的处理。

18. (\d{1,3}\.){3}\d{1,3} ( 寻找网络地址的简单 RE)
此 RE 的意思第一个部分 (\d{1,3}\.){3} ,所指的是,数字最小一位最多三位,并且后面接有 ”.” 符号,此类型的共有三个,之后再接一到三位的数字,也就是如 192.72.28.1 这样的数字。
但这样会有个缺点,因为网络地址数字最多只到 255 ,但上述的 RE 只要是一到三位的数字都是符合的,所以这需要让比较的数字小于 256 才行,但只单独使用 RE 并无法做这样的比较。在 19 中使用择一来将地址的限制在所需要的范围内,也就是 0 到 255 。
19. ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?) ( 寻找网络地址 )
有没有发觉 RE 越来越像外星人说的话了 ? 就以简单的寻找网络地址,直接看 RE 都满难理解的哩。

Expresso Analyzer View

Expresso 提供了一个功能,它可以将所下的 RE 变成树状的说明,一组组的分开说明,提供了一个好的除错环境。其它的功能,如部分符合 (Partial Match 只查找反白 RE 的部分 ) 及除外符合 (Exclude Match 只不查找反白 RE 的部分 ) 就留给各位大大试试啰。

当次描述用括号群组起来时,符合次描述的文字可用在之后的程序处理或 RE 本身。在预设的情型下,所符合的群组是由数字命名,由 1 开始,由顺序是由左至右,这自动群组命名,可在 Expresso 中的 skeleton view 或 result view 中看到。

Backreference 是用来查找群组中抓取的符合文字所相同的文字。举例来说 ”\ 1” 所指符合群组 1 所抓取的文字。

20. \b(\w+)\b\s*\1\b ( 寻找重复字,此处说的重复是指同样的字,中间有空白隔开如 dog dog 这样的字 )
(\w+) 会抓取至少一个字符的字母或数字的字,并将它命名为群组 1 ,之后是查找任意空格符,再接和群组 1 相同的文字。

如果不喜欢群组自动命名的 1 ,也可以自行命名,以上述例子为例, (\w+) 改写为 (?

  1<word>\w+)  ,这就是将所抓取的群组命名为  Word  ,  Backreference  就要改写成为  \k<word>
  2
  321\. \b(?<word>\w+)\b\s*\k<word>\b (  使用自行命名群组抓取重复字  ) 
  4
  5使用括号还有许多特别的语法元素,比较通用的列表如下:    
  6抓取  (Captures)   
  7(exp)  符合  exp  并抓取它进自动命名的群组    
  8(?<name>exp)  符合  exp  并抓取它进命名的群组  name   
  9(?:exp)  符合  exp  ,不抓取它    
 10Lookarounds   
 11(?=exp)  符合字尾为  exp  的文字    
 12(?&lt;=exp)  符合前缀为  exp  的文字    
 13(?!exp)  符合后面没接  exp  字尾的文字    
 14(?<!--exp)  符合前面没接  exp  前缀的文字    
 15批注  Comment   
 16(?#comment)  批注 
 17
 18Positive Lookaround   
 19接下来要谈的是  lookahead  及  lookbehind assertions  。它们所查找的是目前符合之前或之后的文字,并不包含目前符合本身。这些就如同  ”^”  及  ”\b”  特殊字符,本身并不会对应任何文字  (  用来界定位置  )  ,也因此称做是  zero-width assertions  ,看些例子也许会清楚些。 
 20
 21(?=exp)  是一个  ”zero-width positive lookahead assertion”  。它指的就是符合字尾为  exp  的文字,但不包含  exp  本身。    
 2222\. \b\w+(?=ing\b) (  字尾为  ing  的字,比如说  filling  所符合的就是  fill)   
 23(?<=exp)  是一个  ”zero-width positive lookbehind assertion”  。它指的就是符合前缀为  exp  的文字,但不包含  exp  本身。    
 2423\. (?<=\bre)\w+\b (  前缀为  re  的字,比如说  repeated  所符合的就是  peated)   
 2524\. (?<=\d)\d{3}\b (  在字尾的三位数字,且之前接一位数字  )   
 2625\. (?<=\s)\w+(?=\s) (  由空格符分隔开的字母数字字符串  ) 
 27
 28Negative Lookaround   
 29之前有提到,如何查找一个非特定或非在特定群组的字符。但如果只是要验证某字符不存在而不要对应这些字符进来呢  ?  举个例子来说,假设要查找一个字,它的字母里有  q  但接下来的字母不是  u  ,可以用下列的  RE  来做。 
 30
 3126\. \b\w*q[^u]\w*\b (  一个字,其字母里有  q  但接下来的字母不是  u)   
 32这样的  RE  会有一个问题,因为  [^u]  要对应一个字符,所以若  q  是字的最后一个字母,  [^u]  这样的下法就会将空格符对应下去,结果就有可能会符合二个字,比如说  ”Iraq haha”  这样的文字。使用  Negative Lookaround  就能解决这样的问题。 
 33
 3427\. \b\w*q(?!u)\w*\b (  一个字,其字母里有  q  但接下来的字母不是  u)   
 35这是  ”zero-width negative lookahead assertion”  。 
 36
 3728\. \d{3}(?!\d) (  三个位的数字,其后不接一个位数字  ) 
 38
 39同样的,可以使用  (?<!exp)  ,  ”zero-width negative lookbehind assertion”  ,来符合前面没接  exp  前缀的文字符串。 
 40
 4129\. (?<![a-z ])\w{7} (  七个字母数字的字符串,其前面没接字母或空格  )   
 4230\. (?<=<(\w+)-->).*(?=&lt;\/\1&gt;) (HTML  卷标间的文字  )   
 43这使用  lookahead  及  lookbehind assertion  来取出  HTML  间的文字,不包括  HTML  卷标。 
 44
 45请批注  (Comments Please)   
 46括号还有个特殊的用途就是用来包住批注,语法为  ”(?#comment)”  ,若设定  ”Ignore Pattern Whitespace”  选项,则  RE  中的空格符当  RE  使用时会乎略。此选项设定时,  ”#”  之后的文字会乎略。 
 47
 4831\. HTML  卷标间的文字,加上批注    
 49(?&lt;= #  查找前缀,但不包含它    
 50&lt;(\w+)&gt; #HTML  标签    
 51) #  结束查找前缀    
 52.* #  符合任何文字    
 53(?= #  查找字尾,但不包含它    
 54&lt;\/\1&gt; #  符合所抓取群组  1  之字符串,也就是前面小括号的  HTML  标签    
 55) #  结束查找字尾 
 56
 57寻找最多字符的字及最少字符的字  (Greedy and Lazy)   
 58当  RE  下要查找一个范围的重复时  (  如  ”.*”)  ,它通常会寻找最多字符的符合字,也就是  Greedy matching  。举例来说。 
 59
 6032\. a.*b (  开始为  a  结束为  b  的最多字符的符合字  )   
 61若有一字符串是  ”aabab”  ,使用上述  RE  所得到的符合字符串就是  ”aabab”  ,因为这是寻找最多字符的字。有时希望是符合最少字符的字也就是  lazy matching  。只要将重复前述项目的表加上问号  (?)  就可以把它们全部变成  lazy matching  。因此  ”*?”  代表的就是重复任意次数,但是使用最少重复的次数来符合。举个例子来说: 
 62
 6333\. a.*?b (  开始为  a  结束为  b  的最少字符的符合字  )   
 64若有一字符串是  ”aabab”  ,使用上述  RE  第一个所得到的符合字符串就是  ”aab”  再来是  ”ab”  ,因为这是寻找最少字符的字。 
 65
 66*?  重复任意次数,最少重复次数为原则    
 67+?  重复至少一次,最少重复次数为原则    
 68??  重复零次或一次,最少重复次数为原则    
 69{n,m}?  重复至少  n  次,但不超过  m  次,最少重复次数为原则    
 70{n,}?  重复至少  n  次,最少重复次数为原则 
 71
 72还有什么没提到呢  ?   
 73到目前为止,已经提到了许多建立  RE  的元素,当然还有许多元素没有提到,下表整理了一些没提到的元素,在最左边的字段的数字是说明在  Expresso  中的例子。    
 74#  语法  说明    
 75\a Bell  字符    
 76\b  通常是指字的边界,在字符组里所代表的就是  backspace   
 77\t Tab   
 7834 \r Carriage return   
 79\v Vertical Tab   
 80\f From feed   
 8135 \n New line   
 82\e Escape   
 8336 \nnn ASCII  八位码为  nnn  的字符    
 8437 \xnn  十六位码为  nn  的字符    
 8538 \unnnn Unicode  为  nnnn  的字符    
 8639 \cN Control N  字符,举例来说  Ctrl-M  是  \cM   
 8740 \A  字符串的开始  (  和  ^  相似,但不需籍由  multiline  选项  )   
 8841 \Z  字符串的结尾    
 89\z  字符串的结尾    
 9042 \G  目前查找的开始    
 9143 \p{name} Unicode  字符组名称为  name  的字符,比如说  \p{Lowercase_Letter}  所指的就是小写字    
 92(?&gt;exp) Greedy  次描述,又称之为  non-backtracking  次描述。这只符合一次且不采  backtracking  。    
 9344 (?<x>-<y>exp)   
 94or (?-<y>exp)  平衡群组。虽复杂但好用。它让已命名的抓取群组可以在堆栈中操作使用。  (  小弟对这个也是不太懂哩  )   
 9545 (?im-nsx:exp)  为次描述  exp  更改  RE  选项,比如  (?-i:Elvis)  就是把  Elvis  大乎略大小写的选项关掉    
 9646 (?im-nsx)  为之后的群组更改  RE  选项。    
 97(?(exp)yes|no)  次描述  exp  视为  zero-width positive lookahead  。若此时有符合,则  yes  次描述为下一个符合标的,若否,则  no  次描述为下一个符合标的。    
 98(?(exp)yes)  和上述相同但无  no  次描述    
 99(?(name)yes|no)  若  name  群组为有效群组名称,则  yes  次描述为下一个符合标的,若否,则  no  次描述为下一个符合标的。    
10047 (?(name)yes)  和上述相同但无  no  次描述 
101
102结论    
103经过了一连串的例子,及  Expresso  的帮忙,相信各位大大对  RE  有个基本的了解,网络上当然有许多有关于  RE  的文章,如果各位大大有兴趣  ** http://www.codeproject.com  ** 还有许多关于  RE  的相关文章。若大大对书有兴趣的话,  Jeffrey Friedl  的  Mastering Regular Expressions  很多大大都有推  (  小弟还没拜读  )  。希望籍由这样的心得报告,能让对  RE  有兴趣的大大能缩短学习曲线,当然这是小弟第一次接触  RE  ,若文章中有什么错误或说明的不好的地方,可要请各位大大体谅,并请各位大大将需要修正的地方  mail  给小弟,小弟会非常感谢各位大大。</y></y></x></name></word></word></word></word>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus