前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >优秀攻城师必知的正则表达式语法

优秀攻城师必知的正则表达式语法

作者头像
我是攻城师
发布2019-08-05 18:39:30
1.3K0
发布2019-08-05 18:39:30
举报
文章被收录于专栏:我是攻城师

前言

最近公司的一个项目,大量用到了正则来处理文本,以前对正则使用仅限于小打小闹,用的也是一知半解,经过本次的深入使用,发现正则表达式真的是每一位开发者都需要具备的一个基础技能,处理文本的功能异常强大。今天我们就来系统的学习一下它。

关于正则表达式

正则表达式是一种模式匹配引擎,也称为Nondeterministic Finite Automaton(NFA)非确定性有限自动机,为什么叫非确定性呢?因为对于文本搜索可能有多种情况,而正则表达式会尽量穷举所有的可能来找到我们匹配的数据,这种方式也称为回溯,是正则表达式原理里面一个重要的思想。正则表达式是用来处理文本字符串的神器没有之一,如果没有正则表达式,处理一些数据校验和提取,替换工作会变得非常麻烦,例如:验证邮箱是否合法,提取网址,校验日期,校验电话号码,模糊搜索等等。

使用正则表达式来匹配文本,主要有两种直观的反馈结果:

(1)回答我true/false,用来表达是否满足匹配条件

(2)除了回答我true/false外,还要告诉我每一响匹配数据是什么,以及在文本中的起始位置。

在Java里面,关于正则有两个核心类,分别是:

(1)java.util.regex.Pattern

(2)java.util.regex.Matcher

Pattern类用于创建和预编译正则表达式,并能够将自身的规则与文本进行匹配

一个简单的例子:

匹配输入的文本是不是数字

代码语言:javascript
复制
System.out.println( Pattern.matches("\\d+","45a"));//false

Matcher类用于多次匹配文本,可以获取所有的匹配结果的详细信息。一个简单的例子:

找出所有以h开头的单词

代码语言:javascript
复制
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());
       }

输出:

代码语言:javascript
复制
hello  start: 0 end: 6
hadoop  start: 6 end: 13
hive  start: 23 end: 28
hi  start: 28 end: 31

一些元字符如下:

代码语言:javascript
复制
<
>
(
)
[
]
{
}
\
^
-
=
$
!
|
?
*
+
.

上面这些字符,在使用的时候需要转义,注意在Java语言里面转义写两个反斜杠:

代码语言:javascript
复制
\\+

简单解释一下,两个反斜杠表示的是一个反斜杠的意思,所以如果要对某些元字符转义需要使用两个反斜杠才可以。

正则表达式里面常见的符号和语法含义:

代码语言:javascript
复制
\ 转义符号
^ 匹配输入字符串的开始
$ 匹配输入字符串结尾
* 零次或多次匹配前面的字符或子表达式。例如,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"。
<a href="#footnote-xyz"><sup>[xyz]</sup></a> 反向字符集。匹配未包含的任何字符。例如,"[^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官网文档里面的量词表如下图示:

下面我们以实际的例子,来介绍这三种匹配模式的区别和差异:

首先,我们的待匹配的文本是:

代码语言:javascript
复制
<h1>12345@qq.com</h1>   <h1>67890@qq.com</h1>

贪婪模式:

代码语言:javascript
复制
<h1>.*</h1>

输出结果:

代码语言:javascript
复制
<h1>12345@qq.com</h1>   <h1>67890@qq.com</h1>

执行原理:

首先模式可以看成两部分:

代码语言:javascript
复制
p1=(<h1>.*)
p2=(</h1>)

匹配开始时p1部分因为是贪婪模式,会一下吃入整行数据,然后p1成功完成,接着因为p1吃入了整行数据,导致没有剩余数据去匹配p2部分,所以匹配失败。失败之后,p1会从右侧开始,每次吐出一个字符,也称回溯,将p1分成切成两半,分别为s1和s2,那么分别拿s1和s2去匹配p1和p2,知道整体成功或者失败,在上面的例子中,很显然当p1从右侧切分出5个字符时(此时右半部分为\),这个模式就都成功匹配了,然后停止尝试,返回数据。

勉强模式:

代码语言:javascript
复制
<h1>.*?</h1>

(注意在*符号后面,加了后面?) 输出结果:

代码语言:javascript
复制
<h1>12345@qq.com</h1>
<h1>67890@qq.com</h1>

执行原理:

勉强模式也就是最小匹配,同样是

代码语言:javascript
复制
p1=(<h1>.*?)
p2=(</h1>)

两部分,由于p1部分可以是0次或者1次,因此被忽略掉,直接用字符串去匹配p2失败。然后从左边开始进行每遇到一个字符就切分一次,同样分成两半s1和s2,如果s1部分符合,那么就从剩下的s2部分开始1个1个字符读入,直到找到有符合p2部分的数据存在或者失败。当第一个满足的数据找到之后,程序仍然会继续在剩下部分中再次执行,直到遍历结束,所以这个过程是有可能匹配到多条数据的,如上面的输出就找到了两条符合的数据。

占有模式:

(1) 模式:

代码语言:javascript
复制
\d++@

输出:

代码语言:javascript
复制
12345@
67890@

(2) 模式:

代码语言:javascript
复制
\d++3

输出:无匹配结果

(3) 模式:

代码语言:javascript
复制
\d+3

输出:

代码语言:javascript
复制
123

为了说明问题,我上面举了3个例子,其中(1)和(2)是占有模式,第三个是贪婪模式,是为了对比他们的差异而用的。

第一个 \d++@ 是占有模式,基本原理与贪婪模式的执行过程类似,但是唯一的区别就在于占有模式,在匹配不到数据的时候不会发生回溯,如第一个匹配的模式\d++@可以直接匹配到里面存在的两条数据然后输出,第二个同样是占有模式,你会发现匹配不到任何数据,为什么呢?因为\d++直接匹配完所有的数字,不会发生回溯,所以即使3存在也匹配不到。最后为了验证我们的想法,我们使用了贪婪模式的匹配,因为贪婪模式可以回溯,所以最终可以把123匹配到。

捕获组

捕获组是一个非常实用的功能,它能够用来提取我们匹配到数据,如下:

代码语言:javascript
复制
((A)(B(C)))
(A)
(B(C))
(C)

我们通过一段程序来看下结果:

代码语言:javascript
复制
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));
        }
        }

输出:

代码语言:javascript
复制
ABC
==============
ABC
A
BC
C

注意使用括号,代表一种分组获取,按照上面的括号顺序,从左到右,第一对小括号可以代表整个文本的变量,第二对小括号代表捕获A,第三对代表捕获BC,第四对代表捕获C。上面介绍的可能有点抽象,下面我们通过一个例子看下:

输入的文本,还是我们上面的例子,不过我们稍加改动:

代码语言:javascript
复制
<h1>11111111@qq.com</h1>   <h1>2222222@126.com</h1>

现在我们想要提取这里面的邮箱的前缀和后缀,那么如何用捕获组来解决呢?非常简单如下:

代码语言:javascript
复制
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));
        }

输出:

代码语言:javascript
复制
前缀:11111111   后缀:qq.com
前缀:2222222   后缀:126.com

捕获组的第一个作用就是提取各种我们需要的内容,关于捕获组本身还有几种特殊用法,感兴趣的同学可以参考上面的目录里面的详细介绍。

捕获组的第二个作用,可以界定一个范围,如下:

(dog){3} 和 dog{3} 是不同的两个匹配模式:

前者代表精确的匹配dog这个单词3次,后者是精确的匹配g这个字母三次,这一点需要注意

分支逻辑

这个功能也是非常实用的,在正则表达式里面,默认的匹配规则都是隐式的AND,比如我随便写一个匹配模式cat,那么就必须cat才行,如果我想匹配cat或者dog应该怎么表示呢?

这个时候我们可以用|符号,来表示多个分支条件的匹配,如下:

代码语言:javascript
复制
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());
        }

输出:

代码语言:javascript
复制
匹配成功:true
dog

接着再看一例子:

代码语言:javascript
复制
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());
        }

输出:

代码语言:javascript
复制
是否全部相等:true
上海市

注意上面的例子,可以变形为:

代码语言:javascript
复制
北京市|上海市

不可变形为:

代码语言:javascript
复制
[北京|上海]市

[]是针对单个字符的,并不能针对一组词语。

如[abc],代表匹配其中任何一个字符,并不是全部字符,这一点需要牢记,初学者非常容易弄混。

接着再看一个改进的例子:

代码语言:javascript
复制
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());
        }

输出:

代码语言:javascript
复制
是否全部相等:false
北京市
上海市

我们注意一点:

Pattern.matches(regex,input)方法和matcher.matches()方法是相等的,前者的底层其实就是调用的后者。

这个返回结果代表的是匹配的模式串是否和输入的字符串完全相等,如果完全相等就返回true,否则就返回false,如果返回false,只能表示两个字符串并不具有相等关系,但不代表不具有包含关系,如上面的例子中,字符串整体不相等,但目标串里面仍有包含模式串的内容,所以能找到匹配相关的结果,这一点也需要注意。

总结

本文主要介绍了正则表达式的相关概念和原理,并结合例子重点剖析了正则里面常用的三大王牌功能点,分别是:量词匹配,捕获组,和分支逻辑。理解了这些内容我们才算真正的对正则表达式入门了,当然除了这些核心内容之外,还有一些细的语法,鉴于篇幅有限,在这里就不再赘述了,感兴趣的攻城师可自行尝试学习,相信在我们掌握它之后,以后就可以轻松的处理各种复杂的文本匹配了。

历史文章:

如何动手撸一个LRU缓存

如何动手撸一个简单的LFU缓存

在Java里面如何解决进退两难的jar包冲突问题?

Java基本类型的内存分配在栈还是堆

什么是缓存置换算法?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 关于正则表达式
  • 量词匹配及原理
  • 捕获组
  • 分支逻辑
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档