正则表达式基础

来源:互联网 发布:腾讯安全软件管家 编辑:程序博客网 时间:2024/06/13 07:41
原文链接:http://www.taoshengxu.com/space/article/view/1500894694

正则表达式是学习技术的过程中“投资回报率”相当高的一块知识,属于越早学越不亏的东西。
各个平台对正则表达式的支持可以分为三大类:
  • POSIX基本(BRE,Basic Regular Expression),如:grep,sed,vi。
  • POSIX扩展(ERE,Extended Regular Expression),如:grep -E,egrep,awk。
  • Perl兼容(PCRE,Perl Compatible Regular Expression),如:Perl,Java,.NET,Python。
当然,这只是一个大致的划分,每个平台上对正则表达的支持都不相同,具体可以参考Jeffrey Friedl的《Mastering Regular Expressions》(好吧,几乎每一篇NB的关于正则表达式的文章都会提到它,所以我也提一下,以假装这是一篇NB的文章...)。
早期的正则表达式的实现可以追溯到Ken Thompson在UNIX上实现ed的相关工作。后来人们发现这个功能十分好用,以致于ed的命令“g/re/p”单独发展成grep,后来又有sed,awk等工具的出现。当时这些工具对正则表达式的支持各不相同,于是POSIX试图为其建立标准。但这些已有的实现中,有两大类不可调和的矛盾(主要是因为元字符不一样),所以最后实际形成了两个标准,即BRE和ERE。当前Linux下的工具对正则表达式的支持大多为这两者。
另一方面,Perl语言单独发展出一套正则表达式,最后人们发现这一套规则甚至比ERE更好用,以致于后面的主流编程语言对正则表达式的支持都不同程度上是Perl兼容的。PCRE最后成为实际上最流行的正则表达式,甚至很多版本的grep也提供grep -P来使用PCRE。
本文主要以Java,C#和Python为例讲述正则表达式(PCRE)的基本使用,后面会提到BRE和ERE。
基本框架
正则表达式是匹配某种Pattern的表达式。一个正则表达式代表了一个字符串集合。比如:
abc       --匹配->    abcabc|def   --匹配->    abc,defab*c      --匹配->    ac,abc,abbc,...
正则表达式最直观的用途是搜索和替换。一般地:给定一个正则表达式和待处理的字符串s,我们希望在s中搜索到匹配的部分。如果找到匹配,我们还希望知道具体匹配了什么(比如ab*c匹配的是abc还是abbc),并进行相关的处理(比如将匹配到的部分处理之后替换掉原来的部分)。
举个例子,假如我们想:
  1. 在一个字符串s中搜索World或world或WORLD。
  2. 如果搜索到了,将搜到的内容加上括号替换掉原内容,比如“Hello, World!”将被替换成“Hello, (World)!”。
以下是各语言的实现:
Java:
12345678910String s = "Hello, World!";String reExp = "(world|World|WORLD)";Pattern p = Pattern.compile(reExp);Matcher m = p.matcher(s);if (m.find()) {    System.out.println("matched part: " + m.group());    System.out.println("matched part start index: " + m.start());    System.out.println("replaced string: " + m.replaceAll("($1)"));}
C#:
12345678910string s = "Hello, World!";string pattern = @"(world|World|WORLD)";Regex regex = new Regex(pattern);Match match = regex.Match(s);if (match.Success) {    Console.WriteLine("matched part: " + match.Value);    Console.WriteLine("matched part start index: " + match.Index);    Console.WriteLine("replaced string: " + Regex.Replace(s, pattern, "($1)"));}
Python:
123456789s = "Hello, World!"re_exp = r"(world|World|WORLD)"pattern = re.compile(re_exp)matcher = pattern.search(s)if matcher:    print("matched part: " + str(matcher.group()))    print("matched part start index: " + str(matcher.start()))    print("replaced string: " + pattern.sub(r"(\1)", s))
以上程序输出都是:
matched part: Worldmatched part start index: 7replaced string: Hello, (World)!
以上例子中,“(world|World|WORLD)”匹配world或World或WORLD,并把匹配结果分组(见下文),分组之后就可以在替换的时候通过类似“$1”的方式引用匹配到的结果。
如果觉得这么写程序太冗长了,也可以使用类库提供的简单版的函数,或者自己实现工具函数,比如:
Java:
12System.out.println(Pattern.matches(".*(world|World|WORLD).*", "Hello, World!"));System.out.println("Hello, World!".replaceAll("(world|World|WORLD)", "($1)"));
C#:
12Console.WriteLine(Regex.IsMatch("Hello, World!", @"(world|World|WORLD)"));Console.WriteLine(Regex.Replace("Hello, World!", @"(world|World|WORLD)", "($1)"));
Python:
12print(re.search(r"(world|World|WORLD)", "Hello, World!") is not None)print(re.sub(r"(world|World|WORLD)", r"(\1)", "Hello, World!"))
以上程序输出都是:
true/TrueHello, (World)!
注意Java里的Pattern.matches是严格匹配,所以正则表达式前后加上.*相当于搜索。
C#和Python还提供lambda/回调方法,方便进行更加“自定义”的替换。比如我们想把匹配到的部分的第一个字母去掉:
C#
123Console.WriteLine(Regex.Replace("Hello, World!", @"(world|World|WORLD)", (m) => {    return m.ToString().Substring(1);}));
Python:
1print(re.sub(r"(world|World|WORLD)", lambda m: m.group(1)[1:], "Hello, World!"))
以上程序输出为:
Hello, orld!
匹配规则
显然,正则表达式的核心问题在于如何指定匹配规则。本节内容若非特殊说明,适用于Java,C#和Python。我们从单个字符说起。
单个字符
  1. 普通字符,比如“a”,“b”,“c”,匹配它本身。有些字符有特殊含义,需要用“\”进行转义,例如:“\t”表示“Tab”字符,“\*”表示“*”,“\\”表示“\”本身,等等。
  2. [...]可以匹配中括号内字符中的任意一个;[^...]可以匹配除指定字符之外的任意字符。连续字符可以简写,比如“[a-z]”代表a到z的任意一个字符。还有一些预定义的字符集:
    • \w:任意一个单词字符,相当于[A-Za-z0-9_]。
    • \W:任意一个非单词字符,相当于[^\w]。
    • \d:任意一个数字字符,相当于[0-9]。
    • \D:任意一个非数字字符,相当于[^0-9]。
    • \s:任意一个空字符,相当于[ \t\r\n...]。
    • \S:任意一个非数字字符,相当于[^\s]。
  3. “.”可以匹配任意一个字符,在[]内,则匹配“.”本身。
有了单个字符,我们可以在其后面加一个量词,表示这个字符重复出现若干次。有以下量词:
  • *:表示重复出现0到无数次。比如“a*”可以匹配“”,“a”,“aa”,...;“.*”可以匹配任意字符串。
  • +:表示重复出现1到无数次。比如“a+”可以匹配“a”,“aa”,...。
  • ?:表示出现0次或1次。比如“a?”可以匹配“”和“a”。
  • {m}:表示重复出现m次。比如“a{3}”匹配“aaa”,“.{3}”可以匹配“abc”,“XXX”等。
  • {m,n}:表示重复出现m到n次。比如“a{1,3}”可以匹配“a”,“aa”,“aaa”。
  • {m,}:表示重复出现m到无数次。
说到这里,细心的读者可能会发现一个问题,比如这个正则表达式:
.*a
是“匹配任意字符串外加一个a”,那么用它来匹配:
bcabcabc
结果是bca还是bcabca呢?多数语言里,量词匹配默认用的是贪婪模式,即匹配尽量多的字符,所以这个例子中会匹配bcabca。另一种模式称之为惰性模式,即匹配尽量少的字符。在量词之后加上“?”即可指定使用惰性模式。比如用:
.*?a
来匹配上面的字符串,结果就是bca。
有的时候,我们还希望做边界匹配,比如只匹配一行开头的某个字符串。给定一行字符串,我们可以把“行首”和“行尾”这些位置想像成特殊的占位符:
  • ^:表示行首,多行模式(详见后文)匹配每一行的行首
  • $:表示行尾,多行模式匹配每一行的行尾
  • \A:表示字符串开头
  • \Z:表示字符串结尾
  • \b:表示单词的边界
  • \B:表示单词的内部
这样:
^Hello.*  -> 匹配:“Hello, World!”而不匹配:“Friend, Hello, World!”。o\B       -> 匹配:“Hello, World!”中划线的o。
以上我们讨论了单个字符,单个字符可以直接拼接,然而有的时候我们希望把某个组合视为一个整体,这就是分组,用“()”括起来表示,后面可以跟量词,表示分组整体重复出现n次。例如正则表达式:
(abc){2}  -> 匹配:“abcabc”
分组可以用位置N进行向前引用。第一个分组位置为1,依次递增。为了方便引用,可以通过:
(?P<grp_name>...)     -> Python(?<grp_name>...)      -> C#,Java7+
给分组起一个名字。随后的部分就可以通过“\N”和:
(?P=grp_name)         -> Python(?=grp_name)          -> C#,Java7+
来引用之前的分组,例如:
(\d)abc\1(?P<id>\d)abc(?P=id)  -> Python(?<id>\d)abc(?=id)    -> C#,Java7+
都可以匹配:
1abc23abc4...
分组还可以在匹配结果中通过位置来引用和处理,如本文开头的例子所示。注意在结果引用中,Python用的是“\N”,而Java和C#用的是“$N”。
有的时候我们还希望“匹配abc或def”,这称之为选择匹配,用“reg_a|reg_b”表示,意思是匹配reg_a或reg_b中的任意一个。若“|”出现在分组中,则选择只针对分组内部有效,比如:
(abc|def){2}|ghi
匹配:
abcabcabcdefdefabcdefdefghi
有的时候我们可能希望匹配“后面不跟数字的a”,这时我们不但要匹配某个模式本身,还会在乎它的前后是不是某种模式,有这几种情况:
  • reg_a(?=reg_b):reg_a跟着reg_b的时候才匹配,注意这里并没有“消耗”后面的字符,后面的匹配还是从紧跟reg_a的地方开始。
  • reg_a(?!reg_b):reg_a不跟着reg_b的时候才匹配。
  • (?<=reg_a)reg_b:reg_b前面是reg_a的时候才匹配。注意前置检查中,reg_a必须是固定长度的模式,比如“abc”或“abc|def”。
  • (?<!reg_a)reg_b:reg_b前面不是reg_a的时候才匹配。
例如:
o(?!r)    -> 匹配:“Hello, World!”中划线的o。(?<=l)o   -> 匹配:“Hello, World!”中划线的o。
C#和Python还提供if-else匹配来方便实现一些更“变态”的需求,比如匹配“<user@host.com>”或“user@host.com”,但不匹配“<user@host.com”和“user@host.com>”。我们可以用:
(?(分组位置/分组名称)reg_yes|reg_no)
意思是:如果“分组位置/分组名称”引用的分组在前面出现了,则使用reg_yes进行匹配,否则用reg_no进行匹配。上述需求可以使用这个正则表达式:
(<)?\w+@\w+\.\w+(?(1)>|$)
另外,还有一些特殊的结构:
  • (?:...):(...)的不分组版本,仅用来使用“|”和量词。
  • (?#...):作为嵌入的注释被忽略,Java不支持。
匹配模式
正则表达式的匹配有多种模式,比如要不要忽略大小写,如何处理换行等。Java,C#,Python的类库中都有相关的常数指定模式。常用的模式有这些:
  • 忽略大小写模式,通过Pattern.CASE_INSENSITIVE/RegexOptions.IgnoreCase/re.I指定。
  • “Multi-Line”模式(ML模式),也称为多行模式,通过Pattern.MULTILINE/RegexOptions.Multiline/re.M指定。
  • “Dot-Match-All”模式(DMA模式),有时也被称为单行模式,但这么称会带来理解上的混乱,通过Pattern.DOTALL/RegexOptions.Singleline/re.S:指定。
  • 注释模式,通过Pattern.COMMENTS/RegexOptions.IgnorePatternWhitespace/re.X指定。在此模式下正则表达式里未被转义的空字符和以#开始的注释会被忽略。
模式可以在创建正则表达式对象的时候以参数的形式指定,也可以通过在正则表达式前加上:(?imsx),其中每一个字母代表一个模式。以Python为例:
re.compile('pattern', re.I | re.M)
re.compile('(?im)pattern')
是等价的。
关于单行模式多行模式,历史的发展有点混乱。早期的正则表达式的工作模式是每次只处理一行文本,后来的混乱主要是围绕“.”是否匹配<换行>以及“^”和“$”如何匹配<换行>的问题。演变的结果是:
  • 默认情况:“.”不匹配<换行>;“^”和“$”只匹配字符串开关和结尾,不匹配内部<换行>的附近。
  • 打开DMA模式:“.”匹配<换行>,其它不变。
  • 打开ML模式:“^”和“$”同时匹配内部<换行>的附近,其它不变。
  • DMA模式和ML模式可以同时打开。因此,如果称两者为“单行模式”和“多行模式”,言下之意就是说“单行模式”和“多行模式”可以同时存在,这听起来似乎有点难以接受,所以实际上这是一场由名字引发的混乱。
以上是大多数编程语言的情况,即默认情况下DMA模式和ML模式是关着的,但在大多数编辑器里,ML模式默认是开的,DMA模式默认是关的。
作为注释模式的一个(Python的)例子:
a = re.compile(r"""\d +  # the integral part                   \.    # the decimal point                   \d *  # some fractional digits""", re.X)
a = re.compile(r"\d+\.\d*")
是等价的。
BRE和ERE的一些区别
BRE有一些元字符区别于ERE和PCRE:
  • BRE不支持+和?,如果要用,需要通过量词来指定。
  • BRE中,“()”,“{}”和“|”是普通字符,指定分组和量词需要用“\(...\)”和“\{...\}”,选择匹配则不支持。
BRE和ERE里没有定义\s,\w,\s,\S,\W,\S(虽然一些工具不同程度地实现了这些),而是预定义了字符集:
[:upper:]     -> [A-Z][:lower:]     -> [a-z][:alpha:]     -> [A-Za-z][:digit:]     -> [0-9][:xdigit:]    -> [0-9A-Fa-f][:alnum:]     -> [A-Za-z0-9][:punct:]     -> [!"\#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~],标点符号[:blank:]     -> [ \t][:space:]     -> [ \t\n\r\f\v][:cntrl:]     -> [\x00-\x1F\x7F],控制字符[:graph:]     -> [^ [:cntrl:]][:print:]     -> [^[:cntrl:]]
这些字符集使用的时候要放在[]里,例如“a[[:digit:]]b”匹配“a0b”,“a1b”等,但“a[:digit:]b”则是语法错误的。
BRE和ERE里并没有规定“向前引用”(据说是因为这种方式在数学上不够“正则”...),即在正则表达式里通过\N(N为数字)来引用前面的分组,但Linux下几乎所有工具都支持。
原文链接:http://www.taoshengxu.com/space/article/view/1500894694
原创粉丝点击