为了账号安全,请及时绑定邮箱和手机立即绑定

正则表达式深度解析

标签:
Android

我开发了一款正则表达式测试器,想学习正则表达式又苦恼繁琐的测试不妨可以试一下

正则表达式(regular expression)是一种可以在许多现代应用程序和编程语言中使用的特殊形式的代码模式。可以使用它们来验证输入是否符合给定的文本模式,在一大段文字中查找该模式的文本,用其它文本来替换匹配该模式的文本或者重新组织匹配文本的一部分,把一块文本划分成一系列更小的文本

正则表达式通常用于两种任务:1.验证,2.搜索/替换。用于验证时,通常需要在前后分别加上和$,以匹配整个待验证字符串;搜索/替换时是否加上此限定则根据搜索的要求而定,此外,也有可能要在前后加上\b而不是和$。此表所列的常用正则表达式,除个别外均未在前后加上任何限定,请根据需要,自行处理

元字符

正则表达式之所以拥有巨大的魔力,就是因为有12个标点字符才产生的

$ ( ) * + . ? [ \ ^ { |

它们被称作元字符,如果想要在下正则表达式中匹配它们,那么就需要在它们前面用一个反斜杠\来进行转义。特别应该注意的是在这个列表中并不包含右方括号]、连字符-和右花括号},前两个字符只有在它们位于一个没有转义的[之后才成为元字符,而}只有在一个没有转义的{之后才是元字符。在任何时候都没有必要对}进行转义

在Java里面,要匹配反斜杠\,需要使用“\\”,因为在字符串里面不允许出现单个反斜杠\,所以“\”表示一个反斜杠,“\\”才能正确匹配到反斜杠\

使用正则记号<\Q>和<\E>。<\Q>会抑制所有元字符的含义,直到出现<\E>为止。如果漏掉了<\E>,那么在<\Q>之后直到正则表达式结束之前的所有字符都会被当作字符文本来对待。所以上面的正则表达式可以用如下代码匹配

Pattern p = Pattern.compile("\\Q$()*+.?[\\^{|\\E");

Matcher m = p.matcher("$()*+.?[\\^{|");

匹配单个字符

除了元字符之外,匹配单个字符直接使用对应的字符来匹配,当然也有一些特殊的字符,如匹配一个包含ASCII控制字符的字符串:响铃、退出、换页、换行、回车、水平制表符和垂直制表符,对应的地十六进制ASCII分别是:07、1B、0C、0A、0D、09、0B

x
字符 x

\\
反斜线字符

\0n
带有八进制值 0 的字符 n (0 <= n <= 7)

\0nn
带有八进制值 0 的字符 nn (0 <= n <= 7)

\0mnn
带有八进制值 0 的字符 mnn(0 <= m <= 3、0 <= n <= 7)

\xhh
带有十六进制值 0x 的字符 hh

\uhhhh
带有十六进制值 0x 的字符 hhhh

\t
制表符 ('\u0009')

\n
新行(换行)符 ('\u000A')

\r
回车符 ('\u000D')

\f
换页符 ('\u000C')

\a
报警 (bell) 符 ('\u0007')

\e
转义符 ('\u001B')

\cx
对应于 x 的控制符

匹配字符类

使用的方括号[]的表示法被称作是一个字符类(character class)。一个字符类匹配在一个可能的字符列表中的单个字符,预定义字符类

.
任何字符(与行结束符可能匹配也可能不匹配)

\d数字:[0-9]\D非数字: [^0-9]\s空白字符:[ \t\n\x0B\f\r]\S非空白字符:[^\s]\w单词字符:[a-zA-Z_0-9]\W非单词字符:[^\w]

在字符类之外,上面的12个标点字符是元字符。在一个字符类中,只有其中4个字符拥有特殊功能:\、^、-和]。(也就是说,在字符类里面,除了那4个特殊字符,其它的字符都不需要使用转义符。)如果使用的是Java或者是.NET,那么左方括号[在字符类也是一个元字符,所有的其它字符都是字面量,只是把它们自身加入到了字符类中

点号是最古老也是最简单的正则表达式特性之一。它的含义永远是匹配任意单个字符。点号是最经常被滥用的正则表达式特性,最好只有当你确实想要允许出现任意字符是地,才使用点号,而在任何场合,都应当使用一个字符类或者是否定字符类来实现

在.NET、Java、PCRE、Perl、Python中,<(?s)>是用于“点号匹配换行符”模式的模式修饰符

反斜杠总是会对紧跟其后的字符进行转义,这与它在字符类之外的作用一样。被转义的字符可以是单个字符,也可以是一个范围的开始或结束。另外4个元字符只有当它们被放置在特定位置时才拥有特殊含义。在使用中总是对这些元字符进行转义会使你的正则表达式更加容易让人理解

[abc]a、b 或 c(简单类)[^abc]任何字符,除了 a、b 或 c(否定)[a-zA-Z]a 到 z 或 A 到 Z,两头的字母包括在内(范围)[a-d[m-p]]a 到 d 或 m 到 p:[a-dm-p](并集)[a-z&&[def]]d、e 或 f(交集)[a-z&&[^bc]]a 到 z,除了 b 和 c:[ad-z](减去)[a-z&&[^m-p]]a 到 z,而非 m 到 p:[a-lq-z](减去)

字母数字字符则不能使用反斜杠来转义

如果紧跟着左括号后面是一个脱字符(^),那么就会对整个字符类取否。也就是就它会匹配不属于该字符类列表中的任意字符。一个否定字符类会匹配换行符号,除非把换行也加入到否定字符类中

连字符(-)被放在两个字符之间的时候就会创建一个范围。该范围所组成的字符类包含连字符之前的字符、连字符之后的字符,以及按照字母表顺序位于这两个字符之间的所有字符

<\d>和<[\d]>都会匹配单个数字,每个小写的简写都拥有一个相关联的大写简定字符,其含义正好相反。因此<\D>会匹配不是数字的任意字符,所以同<[^\d]>是等价的

<\w>会匹配单个的单词字符(word character),所谓的单词字符指的是能够出现在一个单词中的字符,这包括了字母、数字和下划线。<\W>则会匹配不属于上述字符集合中的任意字符。在Java、JavaScript、PCRE和Ruby中,<\w>总是和<[a-zA-Z0-9_]>的含义完全相同,而在.NET和Perl中,会包含其它字母表(泰语等)的字母和数字

<\s>匹配任意的空白字符,其中包括了空格、制表符和换行符。在.NET、Perl和JavaScript中,<\s>也会匹配杜撰Unicode标准定义这空白号的字符。在JavaScript中对于<\s>使用的是Unicode,对<\d>和<\w>则使用ASCII标准。<\S>会匹配<\s>不能匹配的任意字符

量词

当我们要匹配的正则表达式里面有一部分重复多次时,比如说匹配手机号或固话时,我们可以使用量词来进行匹配固定次数或不定次数的重复

如下面的例子:匹配一个10位的十进制数,可以使用如下正则表达式

Matcher m = Pattern.compile("\\d{10}").matcher("0123456789");while (m.find()) {
    System.out.println(m.group());
}

运行结果为

0123456789

正则表达式中的数量词有Greedy (贪心)量词、Reluctant(懒惰)量词和Possessive(占有)量词三种

首先来看一个贪心量词

X?
X,一次或一次也没有

X*
X,零次或多次

X+
X,一次或多次

X{n}
X,恰好 n 次

X{n,}
X,至少 n 次

X{n,m}
X,至少 n 次,但是不超过 m 次

量词<{n}>,其中n是一个正整数,用来重复之前的正则记号n次

对于固定次数的重复,使用量词<{n}>。<{1}>这样和没有任何量词是等价的,<ab{1}c>和<abc>是等价的,<{0}>是重复之前的记号0次,<ab{0}c>和<ac>是同样的正则表达式

对于可变次数重复,我们使用量词<{n,m}>,其中n是一个正整数,并且m大于n,至少 n 次,但是不超过 m 次。对于可变次数重复的情形,其中所有选择分析重复的顺序就会产生影响。如果n和m是相等的,那就是固定次数的重复

量词<{n, }>,其中n是一个正整数,支持无限次数重复。<\d{1,}>匹配一个或多个数字,<\d+>也一样,在一个不是量词的正则记号之后添加一个“+”,意味着一次或多次;<\d{0,}>匹配零个或多个数字,<\d>也一样,“”意味着0次或多次。<h?>与<h{0,1}>的效果是一样的,在一个合法和完整的非量词正则记号之后的“?”意味着0或1次

量词还可以嵌套。<(e\d+)?>会匹配一个e之后跟着一个或是多个数字,或者是一个长度为0的匹配

Reluctant(懒惰)量词和Possessive(占有)量词与Greedy (贪心)量词基本语法类似

Reluctant 数量词

X??
X,一次或一次也没有

X*?
X,零次或多次

X+?
X,一次或多次

X{n}?
X,恰好 n 次

X{n,}?
X,至少 n 次

X{n,m}?
X,至少 n 次,但是不超过 m 次

Possessive 数量词

X?+
X,一次或一次也没有

X*+
X,零次或多次

X++
X,一次或多次

X{n}+
X,恰好 n 次

X{n,}+
X,至少 n 次

X{n,m}+
X,至少 n 次,但是不超过 m 次

在贪心量词后面加上一个问号“?”可以使任何量词变为懒惰量词;同理在贪心量词后面加上一个加号“+”可以使任何量词变为占有量词。下面来讲一下几种量词的区别:

greedy量词是最常用的,被看作“贪婪的”,因为它第一次就读入整个被模糊匹配的字符串。如果第一个匹配尝试(整个输入字符串)失败,匹配器就会在被匹配字符串中的最后一位后退 一个字符并且再次尝试,重复这个过程,直到找到匹配或者没有更多剩下的字符可以后退为止。根据表达式中使用的量词,它最后试图匹配的内容是1 个或者0个字符。因为总是从最大匹配开始匹配,故称贪婪

reluctant量词采取相反的方式:它们从被匹配字符串的开头开始,然后逐步地一次读取一个字符搜索匹配,直到找到匹配或将整个字符串吞入。因为总是从最小匹配开始,故称懒惰

possessive量词总是读完整个输入字符串,尝试一次(而且只有一次)匹配。和greedy量词不同,possessive从不后退

贪心量词会找到最长的可能匹配,懒惰量词则会找到最短的可能匹配,两者都会进行回退,但是占有量词不进行回退

使用如下代码进行验证

System.out.println("Greedy贪心量词");
Matcher m = Pattern.compile("1.*a").matcher("12a34abc");while (m.find()) {
    System.out.println(m.group());
}

System.out.println("Reluctant懒惰量词");
Matcher m1 = Pattern.compile("1.*?a").matcher("12a34abc");while (m1.find()) {
    System.out.println(m1.group());
}

System.out.println("Possessive占有量词");
Matcher m2 = Pattern.compile("1.*+a").matcher("12a34abc");while (m2.find()) {
    System.out.println(m2.group());
}

得到的结果为

Greedy贪心量词
12a34a
Reluctant懒惰量词
12a
Possessive占有量词

分组与边界匹配器

首先我们来看一个问题,匹配My cat is brown中的cat,但是不会匹配category或是bobcat,看下面的正则表达式

Matcher m = Pattern.compile("\\bcat\\b").matcher("My cat is brown");while (m.find()) {
    System.out.println(m.group());
}

运行结果为

cat

在上面的正则表达式里面,我们使用到了单词边界匹配器

^
行的开头

$
行的结尾

\b
单词边界

\B
非单词边界

\A
输入的开头

\G
上一个匹配的结尾

\Z
输入的结尾,仅用于最后的结束符(如果有的话)

\z
输入的结尾

正则表达式记号<\b>被称作是一个单词边界,它会匹配一个单词的开始或结束,就它自身而言,所产生的一个长度为0的匹配。

严格来讲,<\b>会匹配如下3种位置:

  • 在目标文本的第一个字符之前,如果第一个字符是单词字符

  • 在目标文本的最后一个字符之后,如果最后一个字符是单词字符

  • 在目标文本的两个字符之间,其中一个是单词字符,而另外一个不是单词字符

<\B>会匹配在目标文本中的<\b>不匹配的第一个位置。换句话说,<\B>会匹配不属于单词开始或结束的每一个位置。

单词字符就是可以在单词中出现的字符。

JavaScript、PCRE和Ruby只把ASCII字符看做是单词字符。<\w>因此与<[a-zA-z0-9]>是完全等同的;.NET和Perl把所有语言字母表中的字母和数字都当作单词字符;Python则为你提供了一个选项,只有在创建正则表达式时传递了UNICODE或是U选项,非ASCII的字符才会被包括起来;Java则表现得不是很一致,<\w>只匹配ASCII字符,但是<\b>则是支持Unicode的,因此可以支持任何字母表。

正则表达式中的记号<^>,<$>,<\A>,<\Z>和<\z>被称为定位符(anchor),它们并不匹配任意字符。事实上,它们匹配的特定的位置,也就是说把正则表达式这些位置来进行匹配。

JavaScript不支持<\A>。定位符<>和<\A>是等价的,前提是不能打开“和$匹配换行处”这个选项。对于除了Ruby之外的所有其它正则表达式流派来说,该选项都是默认关闭的。除非使用JavaScript,一般都推荐使用<\A>。<\A>的含义问题保持不变的,因此可以避免由于正则选项设置而造成的混淆或错误。

.NET、Java、PCRE、Perl、Ruby同时支持<\Z>和<\z>,Python只支持<\Z>,JavaScript则根本不提供对<\Z>和<\z>的支持。<\Z>和<\z>的唯一区别是当目标文本的最后一个字符是换行符的时候,在这种情形下,<\Z>可以匹配到目标文本的最结尾处,也就是在最后的换行符之后 的位置,同时也可以匹配紧跟这个换行符之前的位置;<\z>则只会匹配目标文本的最末尾处,因此如果存在一个多余的换行符,那么它无法匹配。

定位符<$>和<\Z>是等价的,前提是不能打开“^和$匹配换行处”这个选项。对于除了Ruby之外的所有其它正则表达式流派来说,该选项都是默认关闭的

逻辑运算符

当匹配多个选择分支时,如匹配Mary,Jane and Sue went to Mary’s house中的Mary,Jane或Sue,使用的正则表达式为

Matcher m = Pattern.compile("Mary|Jane|Sue").matcher("Mary,Jane and Sue went to Mary’s house ");while (m.find()) {
    System.out.println(m.group());
}

执行结果为

Mary

Jane

Sue

Mary

竖线,或是称作管道符号,会把正则表达式拆分成多个选择分支,每个只会匹配一个名字,但是每次却可以匹配不同的名字

正则表达式里面的逻辑运算符如下表表示

XY
X  后跟 Y

X|Y
X  或 Y

(X)
X,作为捕获组

上面的正则表达式还有以下问题:在匹配的过程中会匹配DJanet中的Jane。如下面的的正则表达式所示

Matcher m = Pattern.compile("Mary|Jane|Sue").matcher("Mary,Jane and Sue went to Mary’s house DJanet");while (m.find()) {
    System.out.println(m.group());
}

执行结果为

Mary

Jane

Sue

Mary

Jane

这个时候,可能会想到之前使用到的单词边界匹配器,把正则表达式修改一下,添加单词边界匹配

Matcher m = Pattern.compile("\\bMary|Jane|Sue\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");while (m.find()) {
    System.out.println(m.group());
}

执行结果为

Mary

Jane

Sue

Mary

Jane

执行结果有也不是我们想要的答案,上面的正则表达式写法上面有问题,应该写成下面这样

Matcher m = Pattern.compile("\\bMary\\b|\\bJane\\b|\\b Sue\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");

“|”在所有正则操作符中拥有最低的优先级。如果想要正则表达式中的一些内容不受替代操作影响的话,那么就需要把这些选择分支进行分组,分组是通过圆括号来实现的,括号拥有在所有正则操作符中的最高优先级

Matcher m = Pattern.compile("\\b(Mary|Jane|Sue)\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");while (m.find()) {
    System.out.println(m.group());
}

执行结果为

Mary

Jane

Sue

Mary

一组圆括号不仅仅是一个分组,它还是一个捕获分组,正则表达式\b(\d\d\d\d)-(\d\d)-(\d\d)\b拥有三个捕获分组,分组是按照左括号的顺序从左到右进行编号的,(\d\d\d\d)、(\d\d)、(\d\d)分别为3个分组

分组分为捕获性分组和非捕获性分组,简单的说捕获性分组就是捕获分组所匹配的内容暂且存储在某个地方,以便下次使用,捕获性分组以(...)表示,有些地方将取得捕获性分组所匹配结果的过程称之为"反向引用",非捕获性分组不捕获分组所匹配的内容,当然也就得不到匹配的结果,非捕获性分组以(?:...)表示,在一些只需要分组匹配但是并不需要得到各个分组匹配的结果时,使用非捕获性分组可以提高匹配速度

在匹配过程中,当正则表达式引擎到达右括号而退出分组的时候,它会把该捕获分组所匹配到的文本的子串存储起来。当我们匹配2008-08-05时,2008被保存到第一个捕获中,08在第2个捕获中,而05则在第3个捕获中

使用\b\d\d(\d\d)-\1-\1\b可以匹配像2008-08-08这样的日期(年减去世纪、月份和该月的天数都是相同的数字),在这个正则表达式中,我们使用反向引用来在该正则表达式中的任何地方匹配相同的文本。可以使用反斜杠之后跟一个单个数字(19)来引用前9个捕获分组,而第(1099)则要使用\10~\99

注意,不要使用\01。它或者是一个八进制的转义,或者会产生一个错误。在JavaScript中此正则表达式还会匹配12—34。因为在JavaScript中,对一个还没有参与匹配的分组的反向引用总是会匹配成功,这同捕获了长度为0的匹配的分组的反向引用是一样的

特殊字符

^
匹配输入字符串开始处的位置,但在中括号表达式中使用的情况除外,在那种情况下它对字符集求反。若要匹配 ^ 字符本身,请使用 \^

$
匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,那么 $ 还匹配 \n 或 \r 前面的位置。若要匹配 $ 字符本身,请使用 \$

( )
标记子表达式的开始和结束。可以捕获子表达式以供以后使用。若要匹配这两个字符,请使用 \( 和 \)

*
零次或多次匹配前面的字符或子表达式。若要匹配 * 字符,请使用 \*

+
一次或多次匹配前面的字符或子表达式。若要匹配 + 字符,请使用 \+

.
匹配除换行符 \n 之外的任何单个字符。若要匹配 .,请使用 \.

[ ]
标记中括号表达式的开始。若要匹配这些字符,请使用 \[ 和 \]

?
零次或一次匹配前面的字符或子表达式,或指示“非贪心”限定符。若要匹配 ? 字符,请使用 \?

\
将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如,字符 n 匹配字符 n。\n 匹配换行符。序列 \\ 匹配 \,序列 \( 匹配 (

/
表示文本正则表达式的开始或结束。若要匹配 / 字符,请使用 \/

{ }
标记限定符表达式的开始。若要匹配这些字符,请使用 \{ 和 \}

|
指出在两个项之间进行选择。要匹配 |,请使用 \|

优先级

\
转义符

(), (?:), (?=), []
括号和中括号

*, +, ?, {n}, {n,}, {n,m}
限定符

^, $, \任何元字符、任何字符
定位点和序列|
替换

匹配

//String类中的匹配public boolean matches(String regex)//Pattern类中的匹配public static boolean matches(String regex, CharSequence input)

切割

//普通切割,如果没有匹配的则返回第一条数据,为原来的数据public String[] split(String regex)

//切割并限制个数public String[] split(String regex, int limit)

替换

//替换匹配到的第一次public String replaceFirst(String regex, String replacement)

//替换匹配的全部内容public String replaceAll(String regex, String replacement)



作者:Android轮子哥
链接:https://www.jianshu.com/p/bcb45b20822d


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消