前言
最近公司的一个项目,大量用到了正则来处理文本,以前对正则使用仅限于小打小闹,用的也是一知半解,经过本次的深入使用,发现正则表达式真的是每一位开发者都需要具备的一个基础技能,处理文本的功能异常强大。今天我们就来系统的学习一下它。
关于正则表达式
正则表达式是一种模式匹配引擎,也称为Nondeterministic Finite Automaton(NFA)非确定性有限自动机,为什么叫非确定性呢?因为对于文本搜索可能有多种情况,而正则表达式会尽量穷举所有的可能来找到我们匹配的数据,这种方式也称为回溯,是正则表达式原理里面一个重要的思想。正则表达式是用来处理文本字符串的神器没有之一,如果没有正则表达式,处理一些数据校验和提取,替换工作会变得非常麻烦,例如:验证邮箱是否合法,提取网址,校验日期,校验电话号码,模糊搜索等等。
使用正则表达式来匹配文本,主要有两种直观的反馈结果:
(1)回答我true/false,用来表达是否满足匹配条件
(2)除了回答我true/false外,还要告诉我每一响匹配数据是什么,以及在文本中的起始位置。
在Java里面,关于正则有两个核心类,分别是:
(1)java.util.regex.Pattern
(2)java.util.regex.Matcher
Pattern类用于创建和预编译正则表达式,并能够将自身的规则与文本进行匹配
一个简单的例子:
匹配输入的文本是不是数字:
System.out.println( Pattern.matches("\\d+","45a"));//false
Matcher类用于多次匹配文本,可以获取所有的匹配结果的详细信息。一个简单的例子:
找出所有以h开头的单词:
public static void main(String[] args) {
Pattern pattern= Pattern.compile("h.*? ");
String input="hello hadoop spark tez hive hi ";
Matcher matcher=pattern.matcher(input);
while (matcher.find()){
System.out.println(matcher.group()+" start: "+matcher.start()+" end: "+matcher.end());
}
}
输出:
hello start: 0 end: 6
hadoop start: 6 end: 13
hive start: 23 end: 28
hi start: 28 end: 31
一些元字符如下:
<
>
(
)
[
]
{
}
\
^
-
=
$
!
|
?
*
+
.
上面这些字符,在使用的时候需要转义,注意在Java语言里面转义写两个反斜杠:
\\+
简单解释一下,两个反斜杠表示的是一个反斜杠的意思,所以如果要对某些元字符转义需要使用两个反斜杠才可以。
正则表达式里面常见的符号和语法含义:
\ 转义符号
^ 匹配输入字符串的开始
$ 匹配输入字符串结尾
* 零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,}。
+ 一次或多次匹配前面的字符或子表达式。例如,"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
? 零次或一次匹配前面的字符或子表达式。当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是"非贪心的"。"非贪心的"模式匹配搜索到的、尽可能短的字符串,而默认的"贪心的"模式匹配搜索到的、尽可能长的字符串。例如,在字符串"oooo"中,"o+?"只匹配单个"o",而"o+"匹配所有"o"。
{n} n 是非负整数。正好匹配 n 次
{n,} n 是非负整数。至少匹配 n 次
{n,m} m 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次
. 匹配除"\r\n"之外的任何单个字符。若要匹配包括"\r\n"在内的任意字符,请使用诸如"[\s\S]"之类的模式。
(pattern) 捕获组,匹配 pattern 并捕获该匹配的子表达式
(?:pattern) 匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用"or"字符 (|) 组合模式部件的情况很有用。例如,'industr(?:y|ies) 是比 'industry|industries' 更经济的表达式。
(?=pattern) 执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?=95|98|NT|2000)' 匹配"Windows 2000"中的"Windows",但不匹配"Windows 3.1"中的"Windows"。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
(?!pattern) 执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?!95|98|NT|2000)' 匹配"Windows 3.1"中的 "Windows",但不匹配"Windows 2000"中的"Windows"。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
x|y 匹配 x 或 y。例如,'z|food' 匹配"z"或"food"。'(z|f)ood' 匹配"zood"或"food"。
[xyz] 字符集。匹配包含的任一字符。例如,"[abc]"匹配"plain"中的"a"。
[^xyz]反向字符集。匹配未包含的任何字符。例如,"[^abc]"匹配"plain"中"p","l","i","n"。
[a-z] 字符范围。匹配指定范围内的任何字符。例如,"[a-z]"匹配"a"到"z"范围内的任何小写字母。
[^a-z] 反向范围字符。匹配不在指定的范围内的任何字符。例如,"[^a-z]"匹配任何不在"a"到"z"范围内的任何字符。
\b 匹配一个字边界,即字与空格间的位置。例如,"er\b"匹配"never"中的"er",但不匹配"verb"中的"er"。
\B 非字边界匹配。"er\B"匹配"verb"中的"er",但不匹配"never"中的"er"。
\cx 匹配 x 指示的控制字符。例如,\cM 匹配 Control-M 或回车符。x 的值必须在 A-Z 或 a-z 之间。如果不是这样,则假定 c 就是"c"字符本身。
\d 数字字符匹配。等效于 [0-9]。
\D 非数字字符匹配。等效于 [^0-9]。
\f 换页符匹配。等效于 \x0c 和 \cL。
\n 换行符匹配。等效于 \x0a 和 \cJ
\r 匹配一个回车符。等效于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S 匹配任何非空白字符。与 [^ \f\n\r\t\v] 等效。
\t 制表符匹配。与 \x09 和 \cI 等效。
\v 垂直制表符匹配。与 \x0b 和 \cK 等效。
\w 匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。
\W 与任何非单词字符匹配。与"[^A-Za-z0-9_]"等效。
上面描述了正则中的大部分符号的功能,感兴趣的同学,可以自己一一尝试下,接下来重点介绍正则表达式里面比较重要的几个功能,分别是量词匹配,捕获组,和分支逻辑
量词匹配及原理
量词匹配主要有三种,分别是:贪婪匹配,勉强匹配,占有匹配,量词的符号基本组成就是*(零个或多个),+(一个或多个),?(零个或一个)相关组合。Java官网文档里面的量词表如下图示:
下面我们以实际的例子,来介绍这三种匹配模式的区别和差异:
首先,我们的待匹配的文本是:
<h1>12345@qq.com</h1> <h1>67890@qq.com</h1>
贪婪模式:
<h1>.*</h1>
输出结果:
<h1>12345@qq.com</h1> <h1>67890@qq.com</h1>
执行原理:
首先模式可以看成两部分:
p1=(<h1>.*)
p2=(</h1>)
匹配开始时p1部分因为是贪婪模式,会一下吃入整行数据,然后p1成功完成,接着因为p1吃入了整行数据,导致没有剩余数据去匹配p2部分,所以匹配失败。失败之后,p1会从右侧开始,每次吐出一个字符,也称回溯,将p1分成切成两半,分别为s1和s2,那么分别拿s1和s2去匹配p1和p2,知道整体成功或者失败,在上面的例子中,很显然当p1从右侧切分出5个字符时(此时右半部分为\),这个模式就都成功匹配了,然后停止尝试,返回数据。
勉强模式:
<h1>.*?</h1>
(注意在*符号后面,加了后面?) 输出结果:
<h1>12345@qq.com</h1>
<h1>67890@qq.com</h1>
执行原理:
勉强模式也就是最小匹配,同样是:
p1=(<h1>.*?)
p2=(</h1>)
两部分,由于p1部分可以是0次或者1次,因此被忽略掉,直接用字符串去匹配p2失败。然后从左边开始进行每遇到一个字符就切分一次,同样分成两半s1和s2,如果s1部分符合,那么就从剩下的s2部分开始1个1个字符读入,直到找到有符合p2部分的数据存在或者失败。当第一个满足的数据找到之后,程序仍然会继续在剩下部分中再次执行,直到遍历结束,所以这个过程是有可能匹配到多条数据的,如上面的输出就找到了两条符合的数据。
占有模式:
(1) 模式:
\d++@
输出:
12345@
67890@
(2) 模式:
\d++3
输出:无匹配结果
(3) 模式:
\d+3
输出:
123
为了说明问题,我上面举了3个例子,其中(1)和(2)是占有模式,第三个是贪婪模式,是为了对比他们的差异而用的。
第一个 \d++@ 是占有模式,基本原理与贪婪模式的执行过程类似,但是唯一的区别就在于占有模式,在匹配不到数据的时候不会发生回溯,如第一个匹配的模式\d++@可以直接匹配到里面存在的两条数据然后输出,第二个同样是占有模式,你会发现匹配不到任何数据,为什么呢?因为\d++直接匹配完所有的数字,不会发生回溯,所以即使3存在也匹配不到。最后为了验证我们的想法,我们使用了贪婪模式的匹配,因为贪婪模式可以回溯,所以最终可以把123匹配到。
捕获组
捕获组是一个非常实用的功能,它能够用来提取我们匹配到数据,如下:
((A)(B(C)))
(A)
(B(C))
(C)
我们通过一段程序来看下结果:
String input="ABC";
String regex="((A)(B(C)))";
Pattern pattern=Pattern.compile(regex);//编译正则
Matcher matcher=pattern.matcher(input);//获取匹配
while (matcher.find()){//
System.out.println(matcher.group());//
System.out.println("==============");
for (int i = 1; i <= matcher.groupCount(); i++) {
System.out.println(matcher.group(i));
}
}
输出:
ABC
==============
ABC
A
BC
C
注意使用括号,代表一种分组获取,按照上面的括号顺序,从左到右,第一对小括号可以代表整个文本的变量,第二对小括号代表捕获A,第三对代表捕获BC,第四对代表捕获C。上面介绍的可能有点抽象,下面我们通过一个例子看下:
输入的文本,还是我们上面的例子,不过我们稍加改动:
<h1>11111111@qq.com</h1> <h1>2222222@126.com</h1>
现在我们想要提取这里面的邮箱的前缀和后缀,那么如何用捕获组来解决呢?非常简单如下:
String regex="<h1>(.*?)@(.*?)</h1>";
String input ="<h1>11111111@qq.com</h1> <h1>2222222@126.com</h1>";
Pattern pattern=Pattern.compile(regex);
Matcher matcher=pattern.matcher(input);
while (matcher.find()){
System.out.println("前缀:"+matcher.group(1)+" 后缀:"+matcher.group(2));
}
输出:
前缀:11111111 后缀:qq.com
前缀:2222222 后缀:126.com
捕获组的第一个作用就是提取各种我们需要的内容,关于捕获组本身还有几种特殊用法,感兴趣的同学可以参考上面的目录里面的详细介绍。
捕获组的第二个作用,可以界定一个范围,如下:
(dog){3} 和 dog{3} 是不同的两个匹配模式:
前者代表精确的匹配dog这个单词3次,后者是精确的匹配g这个字母三次,这一点需要注意
分支逻辑
这个功能也是非常实用的,在正则表达式里面,默认的匹配规则都是隐式的AND,比如我随便写一个匹配模式cat,那么就必须cat才行,如果我想匹配cat或者dog应该怎么表示呢?
这个时候我们可以用|符号,来表示多个分支条件的匹配,如下:
String regex="cat|dog";
String input ="dog";
Pattern pattern=Pattern.compile(regex);
Matcher matcher=pattern.matcher(input);
System.out.println("匹配成功:"+Pattern.matches(regex,input));
while (matcher.find()){
System.out.println(matcher.group());
}
输出:
匹配成功:true
dog
接着再看一例子:
String regex="(北京|上海)市";
String input ="上海市";
Pattern pattern=Pattern.compile(regex);
Matcher matcher=pattern.matcher(input);
System.out.println("是否全部相等:"+Pattern.matches(regex,input));
while (matcher.find()){
System.out.println(matcher.group());
}
输出:
是否全部相等:true
上海市
注意上面的例子,可以变形为:
北京市|上海市
不可变形为:
[北京|上海]市
[]是针对单个字符的,并不能针对一组词语。
如[abc],代表匹配其中任何一个字符,并不是全部字符,这一点需要牢记,初学者非常容易弄混。
接着再看一个改进的例子:
String regex="(北京|上海)市";
String input ="我们上周去了北京市,下周打算去上海市,下个月打算去深圳市";
//String input ="上海市";
Pattern pattern=Pattern.compile(regex);
Matcher matcher=pattern.matcher(input);
System.out.println("是否全部相等:"+Pattern.matches(regex,input));
while (matcher.find()){
System.out.println(matcher.group());
}
输出:
是否全部相等:false
北京市
上海市
我们注意一点:
Pattern.matches(regex,input)方法和matcher.matches()方法是相等的,前者的底层其实就是调用的后者。
这个返回结果代表的是匹配的模式串是否和输入的字符串完全相等,如果完全相等就返回true,否则就返回false,如果返回false,只能表示两个字符串并不具有相等关系,但不代表不具有包含关系,如上面的例子中,字符串整体不相等,但目标串里面仍有包含模式串的内容,所以能找到匹配相关的结果,这一点也需要注意。
总结
本文主要介绍了正则表达式的相关概念和原理,并结合例子重点剖析了正则里面常用的三大王牌功能点,分别是:量词匹配,捕获组,和分支逻辑。理解了这些内容我们才算真正的对正则表达式入门了,当然除了这些核心内容之外,还有一些细的语法,鉴于篇幅有限,在这里就不再赘述了,感兴趣的攻城师可自行尝试学习,相信在我们掌握它之后,以后就可以轻松的处理各种复杂的文本匹配了。