正则表达式基础
来源:互联网 发布:腾讯安全软件管家 编辑:程序博客网 时间: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),并进行相关的处理(比如将匹配到的部分处理之后替换掉原来的部分)。
举个例子,假如我们想:
- 在一个字符串s中搜索World或world或WORLD。
- 如果搜索到了,将搜到的内容加上括号替换掉原内容,比如“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。我们从单个字符说起。
单个字符:
- 普通字符,比如“a”,“b”,“c”,匹配它本身。有些字符有特殊含义,需要用“\”进行转义,例如:“\t”表示“Tab”字符,“\*”表示“*”,“\\”表示“\”本身,等等。
- [...]可以匹配中括号内字符中的任意一个;[^...]可以匹配除指定字符之外的任意字符。连续字符可以简写,比如“[a-z]”代表a到z的任意一个字符。还有一些预定义的字符集:
- \w:任意一个单词字符,相当于[A-Za-z0-9_]。
- \W:任意一个非单词字符,相当于[^\w]。
- \d:任意一个数字字符,相当于[0-9]。
- \D:任意一个非数字字符,相当于[^0-9]。
- \s:任意一个空字符,相当于[ \t\r\n...]。
- \S:任意一个非数字字符,相当于[^\s]。
- “.”可以匹配任意一个字符,在[]内,则匹配“.”本身。
有了单个字符,我们可以在其后面加一个量词,表示这个字符重复出现若干次。有以下量词:
- *:表示重复出现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
阅读全文
0 0
- 正则表达式基础表达式
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式的基础
- 正则表达式基础
- 正则表达式基础
- 正则表达式入门基础
- 正则表达式基础
- Javascript正则表达式基础
- 正则表达式基础
- 正则(正规)表达式基础
- Thinkphp 模板->系统变量输出
- GIS中尽量实例化都放在功能模块里,不要在全局中给实例化
- Java-CGLib动态代理
- 自己实现string<char>类
- Swift--项目模板
- 正则表达式基础
- XML文件 特殊符号处理
- JDBC连接MySql数据库
- 【笔试题】网易2018秋招内推笔试
- 如何通过GZIP来优化你的网站
- 认证机构信息管理软件
- (持续更新)一些黑科技和技巧
- -bash: wget: command not found 解决方法
- postman使用技巧