admin管理员组

文章数量:1579086

正则表达式(Regular Expression)

1.正则表达式的介绍

​ 是强大、便捷、高效的文本处理工具。正则表达式本身,如同一门袖珍编程语言的通用模式表示法,赋予使用者描述和分析文本的能力。配合上特定工具提供的额外支持,正则表达式能够添加、删除、分离、叠加、插入和修整各种类型的文本和数据。

​ 正则表达式是由普通字符(例如字符 a 到 z)以及特殊字符(称为元字符)组成的文字模式。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

2.正则表达式语法

1)非打印字符

非打印字符也可以是正则表达式的组成部分。下表列出了表示非打印字符的转义序列:

字符描述
\cx匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\f匹配一个换页符。等价于 \x0c 和 \cL。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t匹配一个制表符。等价于 \x09 和 \cI。
\v匹配一个垂直制表符。等价于 \x0b 和 \cK。

2)特殊字符

所谓特殊字符,就是一些有特殊含义的字符,如上面说的 runoo*b 中的 *****,简单的说就是表示任何字符串的意思。如果要查找字符串中的 ***** 符号,则需要对 ***** 进行转义,即在其前加一个 \: runo \*ob 匹配 runoob。

许多元字符要求在试图匹配它们时特别对待。若要匹配这些特殊字符,必须首先使字符"转义",即,将反斜杠字符*\* 放在它们前面。下表列出了正则表达式中的特殊字符:

特别字符描述
$匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 \$。
( )标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用\ ( 和 \)。
*匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*。
+匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
.匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用\\ . 。
[标记一个中括号表达式的开始。要匹配 [,请使用 \[。
?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 ?。
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。’\n’ 匹配换行符。序列 ‘\\’ 匹配 “\”,而 ‘\(’ 则匹配 “(”。
^匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配 ^ 字符本身,请使用 ^。
{标记限定符表达式的开始。要匹配 {,请使用 \{。
|指明两项之间的一个选择。要匹配 |,请使用\

3)限定符

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。有 ***** 或 +?{n}{n,}{n,m} 共6种。

正则表达式的限定符有:

字符描述
*匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,‘zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。例如,“do(es)?” 可以匹配 “do” 、 “does” 中的 “does” 、 “doxy” 中的 “do” 。? 等价于 {0,1}。
{n}n 是一个非负整数。匹配确定的 n 次。例如,‘o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,}n 是一个非负整数。至少匹配n 次。例如,‘o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。‘o{1,}’ 等价于 ‘o+’。‘o{0,}’ 则等价于 ‘o*’。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,“o{1,3}” 将匹配 “fooooood” 中的前三个 o。‘o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。

4)定位符

定位符使您能够将正则表达式固定到行首或行尾。它们还使您能够创建这样的正则表达式,这些正则表达式出现在一个单词内、在一个单词的开头或者一个单词的结尾。

定位符用来描述字符串或单词的边界,^$ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界。

正则表达式的定位符有:

字符描述
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
\b匹配一个单词边界,即字与空格间的位置。
\B非单词边界匹配。|a|b|c| ,|a|, b

5)运算符优先级

运算符描述
\转义符
(), (?\:), (?=), []圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}限定符
^, $, \任何元字符、任何字符定位点和序列(即:位置和顺序)
|替换,“或"操作 字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food”。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"。

3.正则表达式的基本原理

1)正则引擎的分类

正则引擎主要可以分为两大类:一种是DFA,一种是NFA。这两种引擎都有了很久的历史(至今二十多年),当中也由这两种引擎产生了很多变体!于是POSIX的出台产生规范了不必要变体的继续产生。这样一来,目前的主流正则引擎又分为3类:一、DFA,二、传统型NFA,三、POSIX NFA。
NFA与DFA是正则表达式引擎所使用的两种基本技术:
NFA(电动机):非确定型有穷自动机
DFA(汽油机):确定型有穷自动机
判断是否是传统型NFA
用nfa|nfa not 来匹配 “nfa not” 字符串 
1. 如果只有 nfa 匹配 ,则是传统型nfa。 
2. 如果整个nfa not 都能匹配,则要么是POSIX NFA ,要么是 DFA。

是DFA还是POSIX NFA
用X(.+)+X来匹配“=XX==========={256}”。 
1. 如果执行花费很长时间,就是NFA(如果上一项测试显示不是传统型NFA,则肯定是POSIX NFA)。 
2. 如果时间很短则是DFA。
DFA 引擎在线性时状态下执行,因为它们不要求回溯(并因此它们永远不测试相同的字符两次)。DFA 引擎还可以确保匹配最长的可能的字符串。但是,因为 DFA 引擎只包含有限的状态,所以它不能匹配具有反向引用的模式;并且因为它不构造显示扩展,所以它不可以捕获子表达式。

传统的 NFA 引擎运行所谓的“贪婪的”匹配回溯算法,以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。因为传统的 NFA 构造正则表达式的特定扩展以获得成功的匹配,所以它可以捕获子表达式匹配和匹配的反向引用。但是,因为传统的 NFA 回溯,所以它可以访问完全相同的状态多次(如果通过不同的路径到达该状态)。因此,在最坏情况下,它的执行速度可能非常慢。因为传统的 NFA 接受它找到的第一个匹配,所以它还可能会导致其他(可能更长)匹配未被发现。

POSIX NFA 引擎与传统的 NFA 引擎类似,不同的一点在于:
在它们可以确保已找到了可能的最长的匹配之前,它们将继续回溯。因此,POSIX NFA 引擎的速度慢于传统的 NFA 引擎;并且在使用 POSIX NFA 时,您恐怕不会愿意在更改回溯搜索的顺序的情况下来支持较短的匹配搜索,而非较长的匹配搜索。
目前使用DFA引擎的程序主要有:awk,egrep,flex,lex,MySQL,Procmail等;
使用传统型NFA引擎的程序主要有:GNU Emacs,Java,ergp,less,more,.NET语言,PCRE library,Perl,PHP,Python,Ruby,sed,vi;
使用POSIX NFA引擎的程序主要有:mawk,Mortice Kern Systems’ utilities,GNU Emacs(使用时可以明确指定);
使用DFA/NFA混合的引擎:GNU awk,GNU grep/egrep,Tcl;

2)NFA中的回溯

NFA引擎最重要的性质是,它会依次处理各个子表达式或组成元素,遇到需要在两个可能成功的可能中进行选择的时候,它会选择其一,同时记住另一个,以备稍后可能的需要。

需要做出选择的情形包括量词(决定是否尝试另一次匹配)和多选结构(决定选择哪个多选分支,留下哪个稍后尝试)。

不论选择那一种途径,如果它能匹配成功,而且正则表达式的余下部分也成功了,匹配即告完成。如果正则表达式中余下的部分最终匹配失败,引擎会知道需要回溯到之前做出选择的地方,选择其他的备用分支继续尝试。这样,引擎最终会尝试表达式的所有可能途径(或者是匹配完成之前需要的所有途径)。

类似于面包屑:在每个道路分叉口留下面包屑,走了死路,可以原路返回,继续尝试另外的道路,走不通继续返回,直至找到出路或者尝试完所有道路。

回溯的要点:

回溯机制的基本原理并不难理解,还是有些细节对实际应用很重要。面对众多选择时,哪个分支应当首先选择?回溯进行时,应该选取哪个保存的状态?
答案是下面这条重要原则:
如果需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”而对于忽略优先量词,会选择“跳过尝试”。

有助于解释为什么匹配优先的量词是“匹配优先”的,但还不完整。要想彻底弄清楚这一点,我们需要了解回溯时使用的是哪个(或者是哪些个)之前保存的分支,

答案是:距离当前最近储存的选项就是当本地失败强制回溯时返回的。使用的原则是LIFO(last in first out,后进先出)

3)匹配的基础

规则1 :优先选择最左端的匹配结果

起始位置最靠左的匹配结果总是优先于其他可能的匹配结果。这条规则并没有规定优先的匹配结果的长度(稍后将会讨论),而只是规定,在所有可能的匹配结果中,优先选择开始位置最左端的。

例子:

正则字符:fat|cat|belly|your
待测字符:The dragging belly,indicates your cat is too fat
匹配的第一个结果不是“fat ”,而是“belly ”。

概念:

传动装置(transmission)和驱动过程(bump-along)

或许汽车变速箱(译注1)的例子有助于理解这条规则,驾驶员在换档时,变速箱负责连接引擎和动力系统。引擎是真正产生动力的地方(它驱动曲轴),而变速箱把动力传送到车轮。

传动装置的主要功能:驱动

如果引擎不能在字符串开始的位置找到匹配的结果,传动装置就会推动引擎,从字符串的下一个位置开始尝试,然后是下一个,再下一个,如此继续。不过,如果某个正则表达式是以“字符串起始位置锚点(start-of-string anchor)”开头的,传动装置就会知道,不需要更多的尝试,因为如果能够匹配,结果肯定是从字符串的头部开始的。

引擎的构造:

文字文本,字符组,点号,Unicode属性,捕获型括号,锚点([^],[?,=],[$],[\G],[\b])

规则2 : 匹配优先与忽略优先

1.匹配优先:
我们来看‘^\w+\d\d’匹配‘1a2b2c2de’的过程。首先‘\w+’匹配整个字符串之后,然后为了匹配‘\d’要求‘\w+’释放一个字符‘e’(最后的字符)。看是否能让‘\d’匹配,如果不能匹配,‘\w+’必须继续“交还”字符,直到能匹配‘\d’为止,如果所有的字符都交还完后还是不能匹配,则匹配失败。
2.忽略优先:
而对于忽略优先,总是匹配尽可能少的字符,先用‘\w’来匹配‘1a2b2c2de’中的‘1’,此时,‘+?’又必须选择,是继续进行匹配,还是忽略?因为它是忽略优先的,会首先选择忽略。接下来的‘\d’不能匹配后面的‘a’,所以‘\w+?’会继续尝试未匹配的‘a’,然后看后面的字符能否匹配‘\d’,直到能匹配‘\d’或尝试所有可能后报告匹配失败。

规则3 :标准量词是匹配优先

标准量词(?,*,+,以及{min,max}等)
用这些量词来约束某个表达时,尝试的次数是存在上下限的.

先来先服务

正则字符:^.*([0-9]+)
待测字符:Copyright 2003.
这个表达式的本意是捕获整个数字‘2003’,但结果并非如此。
为了满足“[0-9]+”的匹配,“.*”必须交还一些字符。在这个例子中,释放的字符是“3.”(即最后的“3”和点号 ),之后“3”能够由“[0-9]”匹配。

过度的匹配优先

正则字符:^.*([0-9][0-9])
待测字符:about.24.char.long.1test.2zy.zxh.
这个表达式的本意是捕获整个数字‘2’和‘4’,但结果并非如此。
为了满足“[0-9]”的匹配,“.*”必须交还一些字符。在这个例子中,第一个匹配的字符要求交还字符h,但是不足以让“[0-9]”匹配,继续交还字符,直到满足第一个“[0-9]”时并且满足第二个“[0-9]”时结束匹配

规则4:三种匹配量词

?(重复0次或1次)、*(重复0次或多次)、+(重复1次或多次)。

Greedy Quantifiers 贪婪Reluctant Quantifiers 勉强Possessive Quantifiers 独占
X?X??X?+
X*X*?X*+
X+X+?X++
X{n}X{n}?X{n}+
X{n,}X{n,}?X{n,}+
X{n,m}X{n,m}?X{n,m}+

Greedy :贪婪=匹配量词

匹配最长。在贪婪量词模式下,正则表达式会尽可能长地去匹配符合规则的字符串,且会回溯

Reluctant :勉强=忽略量词

匹配最短。在勉强量词模式下,正则表达式会匹配尽可能短的字符串。

**Possessive 😗*独占

同贪婪一样匹配最长。不过在独占量词模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。占有优先量词与匹配优先量词很相似,只是它们从来不会交还已经匹配的字符

规则5:引擎中的表达式主导与文本主导

表达式主导:
如用正则表达式:to(nite|knite|night) 来匹配文本:tonight,正则表达式中第一个是‘t’,他会重复尝试,知道在匹配文本中找到‘t’为止,之后检查后续文本是否匹配‘o’,如果能,会接着检查选择分支,NFA引擎会依次尝试‘nite’,‘knite’,‘night’这三种可能,直到匹配成功或全部尝试完后匹配失败,在匹配‘nite’时,会想上面一样一次匹配‘n’,‘i’,‘t’,‘e’,如果失败,在尝试另一种可能。表达式中的控制权在不同的元素之间转换,所以称之为“表达式主导”。

文本主导:
DFA引擎在扫描字符串时,会记录当前有效的所有可能匹配,如上例中,当扫描到匹配文本的‘i’字符时,它会记录下两个可能匹配的位置‘nite’中的‘i’和‘night’中的‘ni’,从而把knight淘汰,再扫描匹配文本中的‘g’,这时只有‘night’能匹配,只剩下一种可能,然后依次匹配‘h’和‘t’,引擎发现匹配已经完成,报告成功。它是扫描的字符串中的每个字符都对引擎进行了控制,所以称为“文本主导”。

会发现,NFA效率会低于DFA,因为DFA每个字符只会扫描一次,而NFA对同一字符会扫描多次。

DFA特征:匹配很迅速,匹配很一致,谈论DFA匹配很烦恼。
为何用NFA:表达式主导,为创造性思维提供了丰富的施展空间。所以重点来了。

4)备用状态

a.未进行回溯的匹配

用「ab?c」匹配abc。
「a」匹配之后,匹配的当前状态如下:
‘a▲bc’	「a▲b?c」

现在该到「b?」了,正则引擎需要决定:是需要尝试「b」呢,还是跳过?因为「?」是匹配优先的,它会尝试匹配。但是,为了确保在这个尝试最终失败之后能够恢复,引擎会把:
‘a▲bc’	「ab?▲c」

添加到备用状态序列中。也就是说,稍后引擎可以从下面的位置继续匹配:从正则表达式中的「b?」之后,字符串的c之前(也就是当前的位置)匹配。这实际上就是跳过「b」的匹配,而问号容许这样做。
引擎放下面包屑之后,就会继续向前,检查「b」。在示例文本中,他能够匹配,所以新的当前状态变为:
‘ab▲c’	「ab?▲c」
最终的「c」也能成功匹配,所以整个匹配完成。备用状态不再需要了,所以不再保存它们。

b.进行了回溯的匹配

如果需要匹配的文本是‘ac’,在尝试「b」之前,一切都与之前的过程相同。显然,这次「b」无法匹配。也就是说,对「…?」进行尝试的路走不通。因为有一个备用状态,这个“局部匹配失败”并不会导致整体匹配失败。引擎会进行回溯,也就是说,把“当前状态”切换为最近保存的状态。在本例中,情况就是:

‘a▲c’	「ab?▲c」

在「b」尝试之前保存的尚未尝试的选项。这时候,「c」可以匹配“c”,所以整个匹配宣告完成。

c.不成功的匹配

现在,我们用同样的表达式匹配‘abX’。在尝试「b」以前,因为存在问号,保存了这个备用状态:

‘a▲bX’	「ab?▲c」

「b」能够匹配,但这条路往下却走不通了,因为「c」无法匹配X。于是引擎会回溯到之前的状态,“交还”b给「c」来匹配。显然,这次测试也失败了。如果还有其他保存的状态,回溯会继续进行,但是此时不存在其他状态,在字符串中当前位置开始的整个匹配也就宣告失败。

d.忽略优先的匹配

现在来看最开始的例子,使用忽略优先匹配量词,用「ab??c」来匹配‘abc’。「a」匹配之后的状态如下:
‘a▲bc’	「a▲b??c」
接下来轮到「b??」,引擎需要进行选择:尝试匹配「b」,还是忽略?因为“??”是忽略优先的,它会首先尝试忽略,但是,为了能够从失败的分支中恢复,引擎会保存下面的状态:

‘a▲bc’	「a▲bc」
到备用状态列表中。于是,引擎稍后能够用正则表达式中的「b」来尝试匹配文本中的“b”(我们知道这能够匹配,但是正则引擎不知道,他甚至都不知道是否会要用到这个备用状态)。状态保存之后,他会继续向前,沿着忽略匹配的路走下去:
‘a▲bc’	「ab?? ▲c」
「c」无法匹配‘b’,所以引擎必须回溯到之前保存的状态:

‘a▲bc’	「a▲bc」
显然,此时匹配可以成功,接下来的「c」匹配‘c’。于是我们得到了与使用匹配优先的「ab?c」同样的结果,虽然两者所走的路不相同。

NFA和DFA都具备许多匹配优先相关的特性。(DFA不支持忽略优先,现在只关注匹配优先,目前只用NFA来讲解)

DFA 匹配优先很好用,介绍很乏味

NFA除了忽略优先还提供了许多特性,比如环视,条件判断,反向引用,固话分组,最重要的是让使用者直接操控匹配的过程,运用得当,这会带来很大的方便,也可能有某些性能问题.

匹配速度:DFA的速度和正则表达式无关,而NFA中两者直接相关。

能力差异:NFA引擎能提供一些DFA不支持的功能.

4.正则表达式的适用场景

平衡法则

1、只匹配期望的文本,排除不期望的文本。

2、易于控制和理解。

3、使用NFA,注意效率问题,灵活使用(分组,捕获,条件判断,控制)。能够匹配,尽快匹配并返回。不能匹配,尽快报告匹配失败。

1)匹配连续行

    public static void main(String[] args) {
        String str="This is \r\n test string";
        testa(str);
    }

    /**
     * 包含回车换行符的处理
     * Pattern.DOTALL	        在这种模式中,表达式 .可以匹配任何字符,包括行结束符。默认情况下,此表达式不匹配行结束符。
             通过嵌入式标志表达式 (?s) 也可以启用此种模式(s 是 “single-line” 模式的助记符,在 Perl 中也使用它)。
       Pattern.CASE_INSENSITIVE	        默认情况下,大小写不敏感的匹配只适用于US-ASCII字符集。这个标志能让表达式忽略大小写进行匹配
     */
    public static void testa(String teststr){
        Pattern wp = Pattern.compile(".*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
        Matcher m = wp.matcher(teststr);
        String result = m.replaceAll("");
        System.out.println("result:" + result);
    }

2)匹配ip

    public static Boolean isIP(String teststr) {
        // String regex = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";
        String regex = "[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*";//三个点号也可以匹配
        String regex01 = "\\d+\\.\\d+\\.\\d+\\.\\d+";//1234.1234.12345.12 也可以匹配
        String regex02 = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";//999.888.1.2 也可以匹配
        String regex03 = "\\d|\\d\\d|[012]\\d\\d.\\d|\\d\\d|[012]\\d\\d\\.\\d|\\d\\d|[012]\\d\\d\\.\\d|\\d\\d|[012]\\d\\d";//256.256257.1.2 也可以匹配
        String regex04 = "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])";//254.12.1.2 正确匹配ip
        Pattern pattern = Pattern.compile(regex04);
        Matcher m = pattern.matcher(teststr);
        return m.find();
    }

3)匹配电话号码

   /**
     * 电话号码包括固定电话和手机号码
     * @param phone
     * @return
     */
    public static Boolean isPhone(String phone) {
        String regex = "^(\\d{7}|\\d{11})$";
        //固定电话是由区号和号码组成,
        //区号是以 0 开头的,后面是 2~3 位数 0\d{2,3}
        //固定电话号码由 7~8 位数字组成 \d{7,8}
        //固定电话的组合方式可能是“区号-号码”或者是“区号号码” 0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}

        //手机号码是 11 位数,并且以数字 1 开头。考虑到手机号码的特殊性 考虑到手机号码的特殊性,这里使用“13[0-9]\\d{8}|15[1089]\\d{8}”表达式进行匹配
        //验证以 13 或 15 开头的,貌似还有16,18开头的 19(值班电话)
        //还要考虑手机号码前面有0的情况
        //还要考虑手机号码中间是否有“-”
        String regex01 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|1[35689]\\d{9}";
        String regex02 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|0?1[35689]\\d{9}";
        String regex03 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|0?1[35689]\\d{1}[-]?\\d{4}[-]?\\d{4}";
        String regex04 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|0?1[35689]\\d[-]?\\d{4}[-]?\\d{4}";
        Pattern pattern = Pattern.compile(regex04);
        Matcher m = pattern.matcher(phone);
        return m.find();
    }

4)匹配对称的扩号

/**
     * 匹配对称的括号中内容
     * (ab)
     * (a((b))
     */
    public static Boolean bracketsContent(String phone) {
        String regex01 = "\\(.*\\)";//括号及括号内部的任意字符 \(.*\)
        String regex02 = "\\([^\\)]*\\)";//从第一个开括号到最近的闭括号 \([^)]*\)
        String regex03 = "\\([^\\(\\)]*\\)";//从一个开括号到最近的闭括号,中间不容许有开括号 \([^()]*\)
        String regex04 = "\\([^\\(\\)]*(\\([^\\(\\)]*\\)[^\\(\\)]*)*\\)";//指定括号层级2个层级 \([^()]*\([^()]*)*)
        Pattern pattern = Pattern.compile(regex03);
        Matcher m = pattern.matcher(phone);
        return m.find();
    }

有兴趣的思考下以下情况的匹配过程

5)匹配邮箱

\w[-\w.+]*@([A-Za-z0-9-][-A-Za-z0-9]+\.)+[A-Za-z]{2,14} 

6)匹配网址url

^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+

7)匹配身份证号码

\d{17}[\d|x]|\d{15}

8)匹配用户名

[A-Za-z0-9_\-\u4e00-\u9fa5]+

5.Java使用正则表达式

    public static void simpleRegexTest(){
        //\w匹配字母或数字或下划线
        //\d匹配数字
        String test = "this is my 1st test string";
        String myRegex = "\\d+\\w+";
        Pattern pattern = Pattern.compile(myRegex);
        //Pattern pattern = Patternpile(myRegex);
        Matcher matcher = pattern.matcher(test);
        if(matcher.find() ){
            String matchText = matcher.group();
            int start = matcher.start();
            int end = matcher.end();
            System.out.println("matchText:"+matchText+";"+"from:"+start+";to:"+end);
        }else {
            System.out.println("not match");
        }
    }

Pattern 概述

声明:public final class Pattern implements java.io.Serializable

Pattern 类有final修饰,可知他不能被子类继承。

含义:模式类,正则表达式的编译表示形式。

Pattern 匹配模式(Pattern flags)和Matcher类


compile( )方法有一个版本,它需要一个控制正则表达式的匹配行为的参数:

Pattern Patternpile(String regex, int flag)
	Pattern.CASE_INSENSITIVE
    默认情况下,大小写不敏感的匹配只适用于US-ASCII字符集。这个标志能让表达式忽略大小写进行匹配。要想对Unicode字符进行大小不明感的匹配,只要将UNICODE_CASE与这个标志合起来就行了。
    通过嵌入式标志表达式(?i)也可以启用不区分大小写的匹配。
    指定此标志可能对性能产生一些影响。
        
    Pattern.MULTILINE
    默认情况下,输入的字符串被看作是一行,即便是这一行中包好了换行符也被看作一行。当匹配“^”到“$”之间的内容的时候,整个输入被看成一个一行。启用多行模式之后,包含换行符的输入将被自动转换成多行,然后进行匹配。
    通过嵌入式标志表达式 (?m) 也可以启用多行模式。
        
    Pattern.DOTALL
    在这种模式中,表达式 .可以匹配任何字符,包括行结束符。默认情况下,此表达式不匹配行结束符。
    通过嵌入式标志表达式 (?s) 也可以启用此种模式(s 是 “single-line” 模式的助记符,在 Perl 中也使用它)。
        
    Pattern.COMMENTS
    这种模式下,匹配时会忽略(正则表达式里的)空格字符(不是指表达式里的”//s”,而是指表达式里的空格,tab,回车之类)和注释(从#开始,一直到这行结束)。
    通过嵌入式标志表达式(?x) 也可以启用注释模式。

1.Pattern.split(CharSequence input)

Pattern有一个split(CharSequence input)方法,用于分隔字符串,并返回一个String[],估计String.split(String regex)就是通过Pattern.split(CharSequence input)来实现的.
Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
String[] str=p.split("我的QQ是:456456我的电话是:0532214我的邮箱是:aaa@aaa"); 
//结果:str[0]="我的QQ是:" str[1]="我的电话是:" str[2]="我的邮箱是:aaa@aaa" 

2.Pattern.matcher(CharSequence input)

Matcher类的构造方法也是私有的,不能随意创建,只能通过Pattern.matcher(CharSequence input)方法得到该类的实例.
Pattern类只能做一些简单的匹配操作,要想得到更强更便捷的正则匹配操作,那就需要将Pattern与Matcher一起合作.Matcher类提供了对正则表达式的分组支持,以及对正则表达式的多次匹配支持.
Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("b22bb23"); 
m.pattern();//返回p 也就是返回该Matcher对象是由哪个Pattern对象的创建的 

3.Matcher.matches()/ Matcher.lookingAt()/ Matcher.find()

Matcher类提供三个匹配操作方法,三个方法均返回boolean类型,当匹配到时返回true,没匹配到则返回false
matches()对整个字符串进行匹配,只有整个字符串都匹配了才返回true
matches 和 lookingAt 方法都用来尝试匹配一个输入序列模式。它们的不同是 matches 要求整个序列都匹配,而lookingAt 不要求。lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。

Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("22bb23"); 
m.matches();//返回false,因为bb不能被\d+匹配,导致整个字符串匹配未成功. 
Matcher m2=p.matcher("2223"); 
m2.matches();//返回true,因为\d+匹配到了整个字符串

pattern = Pattern.compile(REGEX);
matcher = pattern.matcher(INPUT);
matcher2 = pattern.matcher(INPUT2);
System.out.println("Current REGEX is: "+REGEX);
System.out.println("Current INPUT is: "+INPUT);
System.out.println("Current INPUT2 is: "+INPUT2);
System.out.println("lookingAt(): "+matcher.lookingAt());
System.out.println("matches(): "+matcher.matches());
System.out.println("lookingAt(): "+matcher2.lookingAt());

//Current REGEX is: foo
//Current INPUT is: fooooooooooooooooo
//Current INPUT2 is: ooooofoooooooooooo
//lookingAt(): true
//matches(): false
//lookingAt(): false

4.Mathcer.start()/ Matcher.end()/ Matcher.group()

当使用matches(),lookingAt(),find()执行匹配操作后,就可以利用以上三个方法得到更详细的信息.
start()返回匹配到的子字符串在字符串中的索引位置.
end()返回匹配到的子字符串的最后一个字符在字符串中的索引位置.
group()返回匹配到的子字符串
Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("aaa2223bb"); 
m.find();//匹配2223 
m.start();//返回3 
m.end();//返回7,返回的是2223后的索引号 
m.group();//返回2223 

Mathcer m2=m.matcher("2223bb"); 
m.lookingAt();   //匹配2223 
m.start();   //返回0,由于lookingAt()只能匹配前面的字符串,所以当使用lookingAt()匹配时,start()方法总是返回0 
m.end();   //返回4 
m.group();   //返回2223 

Matcher m3=m.matcher("2223bb"); 
m.matches();   //匹配整个字符串 
m.start();   //返回0,原因相信大家也清楚了 
m.end();   //返回6,原因相信大家也清楚了,因为matches()需要匹配所有字符串 
m.group();   //返回2223bb 

5.replaceFirst 和 replaceAll

replaceFirst 替换首次匹配,replaceAll 替换所有匹配

	private static String REGEX = "dog";
    private static String INPUT = "The dog says meow. " +
                                    "All dogs say meow.";
    private static String REPLACE = "cat";
 
    public static void main(String[] args) {
       Pattern p = Pattern.compile(REGEX);
       // get a matcher object
       Matcher m = p.matcher(INPUT); 
       INPUT = m.replaceAll(REPLACE);
       System.out.println(INPUT);
       //The cat says meow. All cats say meow.
   }

6.appendReplacement 和 appendTail 方法

appendReplacement方法:sb是一个StringBuffer,replaceContext待替换的字符串,这个方法会把匹配到的内容替换为replaceContext,并且把从上次替换的位置到这次替换位置之间的字符串也拿到,然后,加上这次替换后的结果一起追加到StringBuffer里(假如这次替换是第一次替换,那就是只追加替换后的字符串啦)。

appendTail方法:sb是一个StringBuffer,这个方法是把最后一次匹配到内容之后的字符串追加到StringBuffer中。

   private static String REGEX = "a*b";
   private static String INPUT = "aabfooaabfooabfoobkkk";
   private static String REPLACE = "-";
   public static void main(String[] args) {
      Pattern p = Pattern.compile(REGEX);
      // 获取 matcher 对象
      Matcher m = p.matcher(INPUT);
      StringBuffer sb = new StringBuffer();
      while(m.find()){
         m.appendReplacement(sb,REPLACE);
      }
      m.appendTail(sb);
      System.out.println(sb.toString());
   }

7.regionStartregionEnd,region方法

regionStart:报告此匹配器区域的开始索引。
regionEnd:报告此匹配器区域的结束索引(不包括)。
region:设置此匹配器的区域限制。重置匹配器,然后设置区域,使其从 start 参数指定的索引开始,到 end 参数指定的索引结束(不包括end索引处的字符)。

Pattern p = Pattern.compile("(\\w+)%(\\d+)");
Matcher m = p.matcher("ab%12-cd%34");
m.region(0, 4);    
while (m.find()) {
    System.out.println("group():" + m.group());
    System.out.println("regionStart():" + m.regionStart());
    System.out.println("regionEnd():" + m.regionEnd());
}
//group():ab%1
//regionStart():0
//regionEnd():4

8.reset()

重置匹配器。可将Matcher 对象状态初始化。将匹配位置指向文件的开头

reset(CharSequence input)

重置此具有新输入序列的匹配器。

Pattern p = Pattern.compile("(\\w+)%(\\d+)");
Matcher m = p.matcher("ab%12-cd%34");
m.reset("ef%56-gh%78");
while (m.find()) {
System.out.println("group():" + m.group());
}

//group():ef%56
//group():gh%78

6.问题答疑]

7.结束语

学以致用,知行合一

谢谢大家

7.评价

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Pu9VZHp-1595408804925)(d:\user\01380195\Application Data\Typora\typora-user-images\image-20200708111719956.png)]

正则表达式(Regular Expression)

报名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BqoZQ1K-1595408806167)(d:\user\01380195\Application Data\Typora\typora-user-images\image-20200708111315399.png)]

签到

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r9uHY6rN-1595408806168)(d:\user\01380195\Application Data\Typora\typora-user-images\image-20200708111252026.png)]

1.正则表达式的介绍

​ 是强大、便捷、高效的文本处理工具。正则表达式本身,如同一门袖珍编程语言的通用模式表示法,赋予使用者描述和分析文本的能力。配合上特定工具提供的额外支持,正则表达式能够添加、删除、分离、叠加、插入和修整各种类型的文本和数据。

​ 正则表达式是由普通字符(例如字符 a 到 z)以及特殊字符(称为元字符)组成的文字模式。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

2.正则表达式语法

1)非打印字符

非打印字符也可以是正则表达式的组成部分。下表列出了表示非打印字符的转义序列:

字符描述
\cx匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\f匹配一个换页符。等价于 \x0c 和 \cL。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t匹配一个制表符。等价于 \x09 和 \cI。
\v匹配一个垂直制表符。等价于 \x0b 和 \cK。

2)特殊字符

所谓特殊字符,就是一些有特殊含义的字符,如上面说的 runoo*b 中的 *****,简单的说就是表示任何字符串的意思。如果要查找字符串中的 ***** 符号,则需要对 ***** 进行转义,即在其前加一个 \: runo \*ob 匹配 runoob。

许多元字符要求在试图匹配它们时特别对待。若要匹配这些特殊字符,必须首先使字符"转义",即,将反斜杠字符*\* 放在它们前面。下表列出了正则表达式中的特殊字符:

特别字符描述
$匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 \$。
( )标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用\ ( 和 \)。
*匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*。
+匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
.匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用\\ . 。
[标记一个中括号表达式的开始。要匹配 [,请使用 \[。
?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 ?。
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。’\n’ 匹配换行符。序列 ‘\\’ 匹配 “\”,而 ‘\(’ 则匹配 “(”。
^匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配 ^ 字符本身,请使用 ^。
{标记限定符表达式的开始。要匹配 {,请使用 \{。
|指明两项之间的一个选择。要匹配 |,请使用\

3)限定符

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。有 ***** 或 +?{n}{n,}{n,m} 共6种。

正则表达式的限定符有:

字符描述
*匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,‘zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。例如,“do(es)?” 可以匹配 “do” 、 “does” 中的 “does” 、 “doxy” 中的 “do” 。? 等价于 {0,1}。
{n}n 是一个非负整数。匹配确定的 n 次。例如,‘o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,}n 是一个非负整数。至少匹配n 次。例如,‘o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。‘o{1,}’ 等价于 ‘o+’。‘o{0,}’ 则等价于 ‘o*’。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,“o{1,3}” 将匹配 “fooooood” 中的前三个 o。‘o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。

4)定位符

定位符使您能够将正则表达式固定到行首或行尾。它们还使您能够创建这样的正则表达式,这些正则表达式出现在一个单词内、在一个单词的开头或者一个单词的结尾。

定位符用来描述字符串或单词的边界,^$ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界。

正则表达式的定位符有:

字符描述
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
\b匹配一个单词边界,即字与空格间的位置。
\B非单词边界匹配。|a|b|c| ,|a|, b

5)运算符优先级

运算符描述
\转义符
(), (?😃, (?=), []圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}限定符
^, $, \任何元字符、任何字符定位点和序列(即:位置和顺序)
|替换,“或"操作 字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food”。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"。

3.正则表达式的基本原理

1)正则引擎的分类

正则引擎主要可以分为两大类:一种是DFA,一种是NFA。这两种引擎都有了很久的历史(至今二十多年),当中也由这两种引擎产生了很多变体!于是POSIX的出台产生规范了不必要变体的继续产生。这样一来,目前的主流正则引擎又分为3类:一、DFA,二、传统型NFA,三、POSIX NFA。
NFA与DFA是正则表达式引擎所使用的两种基本技术:
NFA(电动机):非确定型有穷自动机
DFA(汽油机):确定型有穷自动机
判断是否是传统型NFA
用nfa|nfa not 来匹配 “nfa not” 字符串 
1. 如果只有 nfa 匹配 ,则是传统型nfa。 
2. 如果整个nfa not 都能匹配,则要么是POSIX NFA ,要么是 DFA。

是DFA还是POSIX NFA
用X(.+)+X来匹配“=XX==========={256}”。 
1. 如果执行花费很长时间,就是NFA(如果上一项测试显示不是传统型NFA,则肯定是POSIX NFA)。 
2. 如果时间很短则是DFA。
DFA 引擎在线性时状态下执行,因为它们不要求回溯(并因此它们永远不测试相同的字符两次)。DFA 引擎还可以确保匹配最长的可能的字符串。但是,因为 DFA 引擎只包含有限的状态,所以它不能匹配具有反向引用的模式;并且因为它不构造显示扩展,所以它不可以捕获子表达式。

传统的 NFA 引擎运行所谓的“贪婪的”匹配回溯算法,以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。因为传统的 NFA 构造正则表达式的特定扩展以获得成功的匹配,所以它可以捕获子表达式匹配和匹配的反向引用。但是,因为传统的 NFA 回溯,所以它可以访问完全相同的状态多次(如果通过不同的路径到达该状态)。因此,在最坏情况下,它的执行速度可能非常慢。因为传统的 NFA 接受它找到的第一个匹配,所以它还可能会导致其他(可能更长)匹配未被发现。

POSIX NFA 引擎与传统的 NFA 引擎类似,不同的一点在于:
在它们可以确保已找到了可能的最长的匹配之前,它们将继续回溯。因此,POSIX NFA 引擎的速度慢于传统的 NFA 引擎;并且在使用 POSIX NFA 时,您恐怕不会愿意在更改回溯搜索的顺序的情况下来支持较短的匹配搜索,而非较长的匹配搜索。
目前使用DFA引擎的程序主要有:awk,egrep,flex,lex,MySQL,Procmail等;
使用传统型NFA引擎的程序主要有:GNU Emacs,Java,ergp,less,more,.NET语言,PCRE library,Perl,PHP,Python,Ruby,sed,vi;
使用POSIX NFA引擎的程序主要有:mawk,Mortice Kern Systems’ utilities,GNU Emacs(使用时可以明确指定);
使用DFA/NFA混合的引擎:GNU awk,GNU grep/egrep,Tcl;

2)NFA中的回溯

NFA引擎最重要的性质是,它会依次处理各个子表达式或组成元素,遇到需要在两个可能成功的可能中进行选择的时候,它会选择其一,同时记住另一个,以备稍后可能的需要。

需要做出选择的情形包括量词(决定是否尝试另一次匹配)和多选结构(决定选择哪个多选分支,留下哪个稍后尝试)。

不论选择那一种途径,如果它能匹配成功,而且正则表达式的余下部分也成功了,匹配即告完成。如果正则表达式中余下的部分最终匹配失败,引擎会知道需要回溯到之前做出选择的地方,选择其他的备用分支继续尝试。这样,引擎最终会尝试表达式的所有可能途径(或者是匹配完成之前需要的所有途径)。

类似于面包屑:在每个道路分叉口留下面包屑,走了死路,可以原路返回,继续尝试另外的道路,走不通继续返回,直至找到出路或者尝试完所有道路。

回溯的要点:

回溯机制的基本原理并不难理解,还是有些细节对实际应用很重要。面对众多选择时,哪个分支应当首先选择?回溯进行时,应该选取哪个保存的状态?
答案是下面这条重要原则:
如果需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”而对于忽略优先量词,会选择“跳过尝试”。

有助于解释为什么匹配优先的量词是“匹配优先”的,但还不完整。要想彻底弄清楚这一点,我们需要了解回溯时使用的是哪个(或者是哪些个)之前保存的分支,

答案是:距离当前最近储存的选项就是当本地失败强制回溯时返回的。使用的原则是LIFO(last in first out,后进先出)

3)匹配的基础

规则1 :优先选择最左端的匹配结果

起始位置最靠左的匹配结果总是优先于其他可能的匹配结果。这条规则并没有规定优先的匹配结果的长度(稍后将会讨论),而只是规定,在所有可能的匹配结果中,优先选择开始位置最左端的。

例子:

正则字符:fat|cat|belly|your
待测字符:The dragging belly,indicates your cat is too fat
匹配的第一个结果不是“fat ”,而是“belly ”。

概念:

传动装置(transmission)和驱动过程(bump-along)

或许汽车变速箱(译注1)的例子有助于理解这条规则,驾驶员在换档时,变速箱负责连接引擎和动力系统。引擎是真正产生动力的地方(它驱动曲轴),而变速箱把动力传送到车轮。

传动装置的主要功能:驱动

如果引擎不能在字符串开始的位置找到匹配的结果,传动装置就会推动引擎,从字符串的下一个位置开始尝试,然后是下一个,再下一个,如此继续。不过,如果某个正则表达式是以“字符串起始位置锚点(start-of-string anchor)”开头的,传动装置就会知道,不需要更多的尝试,因为如果能够匹配,结果肯定是从字符串的头部开始的。

引擎的构造:

文字文本,字符组,点号,Unicode属性,捕获型括号,锚点([^],[?,=],[$],[\G],[\b])

规则2 : 匹配优先与忽略优先

1.匹配优先:
我们来看‘^\w+\d\d’匹配‘1a2b2c2de’的过程。首先‘\w+’匹配整个字符串之后,然后为了匹配‘\d’要求‘\w+’释放一个字符‘e’(最后的字符)。看是否能让‘\d’匹配,如果不能匹配,‘\w+’必须继续“交还”字符,直到能匹配‘\d’为止,如果所有的字符都交还完后还是不能匹配,则匹配失败。
2.忽略优先:
而对于忽略优先,总是匹配尽可能少的字符,先用‘\w’来匹配‘1a2b2c2de’中的‘1’,此时,‘+?’又必须选择,是继续进行匹配,还是忽略?因为它是忽略优先的,会首先选择忽略。接下来的‘\d’不能匹配后面的‘a’,所以‘\w+?’会继续尝试未匹配的‘a’,然后看后面的字符能否匹配‘\d’,直到能匹配‘\d’或尝试所有可能后报告匹配失败。

规则3 :标准量词是匹配优先

标准量词(?,*,+,以及{min,max}等)
用这些量词来约束某个表达时,尝试的次数是存在上下限的.

先来先服务

正则字符:^.*([0-9]+)
待测字符:Copyright 2003.
这个表达式的本意是捕获整个数字‘2003’,但结果并非如此。
为了满足“[0-9]+”的匹配,“.*”必须交还一些字符。在这个例子中,释放的字符是“3.”(即最后的“3”和点号 ),之后“3”能够由“[0-9]”匹配。

过度的匹配优先

正则字符:^.*([0-9][0-9])
待测字符:about.24.char.long.1test.2zy.zxh.
这个表达式的本意是捕获整个数字‘2’和‘4’,但结果并非如此。
为了满足“[0-9]”的匹配,“.*”必须交还一些字符。在这个例子中,第一个匹配的字符要求交还字符h,但是不足以让“[0-9]”匹配,继续交还字符,直到满足第一个“[0-9]”时并且满足第二个“[0-9]”时结束匹配

规则4:三种匹配量词

?(重复0次或1次)、*(重复0次或多次)、+(重复1次或多次)。

Greedy Quantifiers 贪婪Reluctant Quantifiers 勉强Possessive Quantifiers 独占
X?X??X?+
X*X*?X*+
X+X+?X++
X{n}X{n}?X{n}+
X{n,}X{n,}?X{n,}+
X{n,m}X{n,m}?X{n,m}+

Greedy :贪婪=匹配量词

匹配最长。在贪婪量词模式下,正则表达式会尽可能长地去匹配符合规则的字符串,且会回溯

Reluctant :勉强=忽略量词

匹配最短。在勉强量词模式下,正则表达式会匹配尽可能短的字符串。

**Possessive 😗*独占

同贪婪一样匹配最长。不过在独占量词模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。占有优先量词与匹配优先量词很相似,只是它们从来不会交还已经匹配的字符

规则5:引擎中的表达式主导与文本主导

表达式主导:
如用正则表达式:to(nite|knite|night) 来匹配文本:tonight,正则表达式中第一个是‘t’,他会重复尝试,知道在匹配文本中找到‘t’为止,之后检查后续文本是否匹配‘o’,如果能,会接着检查选择分支,NFA引擎会依次尝试‘nite’,‘knite’,‘night’这三种可能,直到匹配成功或全部尝试完后匹配失败,在匹配‘nite’时,会想上面一样一次匹配‘n’,‘i’,‘t’,‘e’,如果失败,在尝试另一种可能。表达式中的控制权在不同的元素之间转换,所以称之为“表达式主导”。

文本主导:
DFA引擎在扫描字符串时,会记录当前有效的所有可能匹配,如上例中,当扫描到匹配文本的‘i’字符时,它会记录下两个可能匹配的位置‘nite’中的‘i’和‘night’中的‘ni’,从而把knight淘汰,再扫描匹配文本中的‘g’,这时只有‘night’能匹配,只剩下一种可能,然后依次匹配‘h’和‘t’,引擎发现匹配已经完成,报告成功。它是扫描的字符串中的每个字符都对引擎进行了控制,所以称为“文本主导”。

会发现,NFA效率会低于DFA,因为DFA每个字符只会扫描一次,而NFA对同一字符会扫描多次。

DFA特征:匹配很迅速,匹配很一致,谈论DFA匹配很烦恼。
为何用NFA:表达式主导,为创造性思维提供了丰富的施展空间。所以重点来了。

4)备用状态

a.未进行回溯的匹配

用「ab?c」匹配abc。
「a」匹配之后,匹配的当前状态如下:
‘a▲bc’	「a▲b?c」

现在该到「b?」了,正则引擎需要决定:是需要尝试「b」呢,还是跳过?因为「?」是匹配优先的,它会尝试匹配。但是,为了确保在这个尝试最终失败之后能够恢复,引擎会把:
‘a▲bc’	「ab?▲c」

添加到备用状态序列中。也就是说,稍后引擎可以从下面的位置继续匹配:从正则表达式中的「b?」之后,字符串的c之前(也就是当前的位置)匹配。这实际上就是跳过「b」的匹配,而问号容许这样做。
引擎放下面包屑之后,就会继续向前,检查「b」。在示例文本中,他能够匹配,所以新的当前状态变为:
‘ab▲c’	「ab?▲c」
最终的「c」也能成功匹配,所以整个匹配完成。备用状态不再需要了,所以不再保存它们。

b.进行了回溯的匹配

如果需要匹配的文本是‘ac’,在尝试「b」之前,一切都与之前的过程相同。显然,这次「b」无法匹配。也就是说,对「…?」进行尝试的路走不通。因为有一个备用状态,这个“局部匹配失败”并不会导致整体匹配失败。引擎会进行回溯,也就是说,把“当前状态”切换为最近保存的状态。在本例中,情况就是:

‘a▲c’	「ab?▲c」

在「b」尝试之前保存的尚未尝试的选项。这时候,「c」可以匹配“c”,所以整个匹配宣告完成。

c.不成功的匹配

现在,我们用同样的表达式匹配‘abX’。在尝试「b」以前,因为存在问号,保存了这个备用状态:

‘a▲bX’	「ab?▲c」

「b」能够匹配,但这条路往下却走不通了,因为「c」无法匹配X。于是引擎会回溯到之前的状态,“交还”b给「c」来匹配。显然,这次测试也失败了。如果还有其他保存的状态,回溯会继续进行,但是此时不存在其他状态,在字符串中当前位置开始的整个匹配也就宣告失败。

d.忽略优先的匹配

现在来看最开始的例子,使用忽略优先匹配量词,用「ab??c」来匹配‘abc’。「a」匹配之后的状态如下:
‘a▲bc’	「a▲b??c」
接下来轮到「b??」,引擎需要进行选择:尝试匹配「b」,还是忽略?因为“??”是忽略优先的,它会首先尝试忽略,但是,为了能够从失败的分支中恢复,引擎会保存下面的状态:

‘a▲bc’	「a▲bc」
到备用状态列表中。于是,引擎稍后能够用正则表达式中的「b」来尝试匹配文本中的“b”(我们知道这能够匹配,但是正则引擎不知道,他甚至都不知道是否会要用到这个备用状态)。状态保存之后,他会继续向前,沿着忽略匹配的路走下去:
‘a▲bc’	「ab?? ▲c」
「c」无法匹配‘b’,所以引擎必须回溯到之前保存的状态:

‘a▲bc’	「a▲bc」
显然,此时匹配可以成功,接下来的「c」匹配‘c’。于是我们得到了与使用匹配优先的「ab?c」同样的结果,虽然两者所走的路不相同。

NFA和DFA都具备许多匹配优先相关的特性。(DFA不支持忽略优先,现在只关注匹配优先,目前只用NFA来讲解)

DFA 匹配优先很好用,介绍很乏味

NFA除了忽略优先还提供了许多特性,比如环视,条件判断,反向引用,固话分组,最重要的是让使用者直接操控匹配的过程,运用得当,这会带来很大的方便,也可能有某些性能问题.

匹配速度:DFA的速度和正则表达式无关,而NFA中两者直接相关。

能力差异:NFA引擎能提供一些DFA不支持的功能.

4.正则表达式的适用场景

平衡法则

1、只匹配期望的文本,排除不期望的文本。

2、易于控制和理解。

3、使用NFA,注意效率问题,灵活使用(分组,捕获,条件判断,控制)。能够匹配,尽快匹配并返回。不能匹配,尽快报告匹配失败。

1)匹配连续行

    public static void main(String[] args) {
        String str="This is \r\n test string";
        testa(str);
    }

    /**
     * 包含回车换行符的处理
     * Pattern.DOTALL	        在这种模式中,表达式 .可以匹配任何字符,包括行结束符。默认情况下,此表达式不匹配行结束符。
             通过嵌入式标志表达式 (?s) 也可以启用此种模式(s 是 “single-line” 模式的助记符,在 Perl 中也使用它)。
       Pattern.CASE_INSENSITIVE	        默认情况下,大小写不敏感的匹配只适用于US-ASCII字符集。这个标志能让表达式忽略大小写进行匹配
     */
    public static void testa(String teststr){
        Pattern wp = Pattern.compile(".*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
        Matcher m = wp.matcher(teststr);
        String result = m.replaceAll("");
        System.out.println("result:" + result);
    }

2)匹配ip

    public static Boolean isIP(String teststr) {
        // String regex = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";
        String regex = "[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*";//三个点号也可以匹配
        String regex01 = "\\d+\\.\\d+\\.\\d+\\.\\d+";//1234.1234.12345.12 也可以匹配
        String regex02 = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";//999.888.1.2 也可以匹配
        String regex03 = "\\d|\\d\\d|[012]\\d\\d.\\d|\\d\\d|[012]\\d\\d\\.\\d|\\d\\d|[012]\\d\\d\\.\\d|\\d\\d|[012]\\d\\d";//256.256257.1.2 也可以匹配
        String regex04 = "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])";//254.12.1.2 正确匹配ip
        Pattern pattern = Pattern.compile(regex04);
        Matcher m = pattern.matcher(teststr);
        return m.find();
    }

3)匹配电话号码

   /**
     * 电话号码包括固定电话和手机号码
     * @param phone
     * @return
     */
    public static Boolean isPhone(String phone) {
        String regex = "^(\\d{7}|\\d{11})$";
        //固定电话是由区号和号码组成,
        //区号是以 0 开头的,后面是 2~3 位数 0\d{2,3}
        //固定电话号码由 7~8 位数字组成 \d{7,8}
        //固定电话的组合方式可能是“区号-号码”或者是“区号号码” 0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}

        //手机号码是 11 位数,并且以数字 1 开头。考虑到手机号码的特殊性 考虑到手机号码的特殊性,这里使用“13[0-9]\\d{8}|15[1089]\\d{8}”表达式进行匹配
        //验证以 13 或 15 开头的,貌似还有16,18开头的 19(值班电话)
        //还要考虑手机号码前面有0的情况
        //还要考虑手机号码中间是否有“-”
        String regex01 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|1[35689]\\d{9}";
        String regex02 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|0?1[35689]\\d{9}";
        String regex03 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|0?1[35689]\\d{1}[-]?\\d{4}[-]?\\d{4}";
        String regex04 = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|0?1[35689]\\d[-]?\\d{4}[-]?\\d{4}";
        Pattern pattern = Pattern.compile(regex04);
        Matcher m = pattern.matcher(phone);
        return m.find();
    }

4)匹配对称的扩号

/**
     * 匹配对称的括号中内容
     * (ab)
     * (a((b))
     */
    public static Boolean bracketsContent(String phone) {
        String regex01 = "\\(.*\\)";//括号及括号内部的任意字符 \(.*\)
        String regex02 = "\\([^\\)]*\\)";//从第一个开括号到最近的闭括号 \([^)]*\)
        String regex03 = "\\([^\\(\\)]*\\)";//从一个开括号到最近的闭括号,中间不容许有开括号 \([^()]*\)
        String regex04 = "\\([^\\(\\)]*(\\([^\\(\\)]*\\)[^\\(\\)]*)*\\)";//指定括号层级2个层级 \([^()]*\([^()]*)*)
        Pattern pattern = Pattern.compile(regex03);
        Matcher m = pattern.matcher(phone);
        return m.find();
    }

有兴趣的思考下以下情况的匹配过程

5)匹配邮箱

\w[-\w.+]*@([A-Za-z0-9-][-A-Za-z0-9]+\.)+[A-Za-z]{2,14} 

6)匹配网址url

^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+

7)匹配身份证号码

\d{17}[\d|x]|\d{15}

8)匹配用户名

[A-Za-z0-9_\-\u4e00-\u9fa5]+

5.Java使用正则表达式

    public static void simpleRegexTest(){
        //\w匹配字母或数字或下划线
        //\d匹配数字
        String test = "this is my 1st test string";
        String myRegex = "\\d+\\w+";
        Pattern pattern = Pattern.compile(myRegex);
        //Pattern pattern = Patternpile(myRegex);
        Matcher matcher = pattern.matcher(test);
        if(matcher.find() ){
            String matchText = matcher.group();
            int start = matcher.start();
            int end = matcher.end();
            System.out.println("matchText:"+matchText+";"+"from:"+start+";to:"+end);
        }else {
            System.out.println("not match");
        }
    }

Pattern 概述

声明:public final class Pattern implements java.io.Serializable

Pattern 类有final修饰,可知他不能被子类继承。

含义:模式类,正则表达式的编译表示形式。

Pattern 匹配模式(Pattern flags)和Matcher类


compile( )方法有一个版本,它需要一个控制正则表达式的匹配行为的参数:

Pattern Patternpile(String regex, int flag)
	Pattern.CASE_INSENSITIVE
    默认情况下,大小写不敏感的匹配只适用于US-ASCII字符集。这个标志能让表达式忽略大小写进行匹配。要想对Unicode字符进行大小不明感的匹配,只要将UNICODE_CASE与这个标志合起来就行了。
    通过嵌入式标志表达式(?i)也可以启用不区分大小写的匹配。
    指定此标志可能对性能产生一些影响。
        
    Pattern.MULTILINE
    默认情况下,输入的字符串被看作是一行,即便是这一行中包好了换行符也被看作一行。当匹配“^”到“$”之间的内容的时候,整个输入被看成一个一行。启用多行模式之后,包含换行符的输入将被自动转换成多行,然后进行匹配。
    通过嵌入式标志表达式 (?m) 也可以启用多行模式。
        
    Pattern.DOTALL
    在这种模式中,表达式 .可以匹配任何字符,包括行结束符。默认情况下,此表达式不匹配行结束符。
    通过嵌入式标志表达式 (?s) 也可以启用此种模式(s 是 “single-line” 模式的助记符,在 Perl 中也使用它)。
        
    Pattern.COMMENTS
    这种模式下,匹配时会忽略(正则表达式里的)空格字符(不是指表达式里的”//s”,而是指表达式里的空格,tab,回车之类)和注释(从#开始,一直到这行结束)。
    通过嵌入式标志表达式(?x) 也可以启用注释模式。

1.Pattern.split(CharSequence input)

Pattern有一个split(CharSequence input)方法,用于分隔字符串,并返回一个String[],估计String.split(String regex)就是通过Pattern.split(CharSequence input)来实现的.
Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
String[] str=p.split("我的QQ是:456456我的电话是:0532214我的邮箱是:aaa@aaa"); 
//结果:str[0]="我的QQ是:" str[1]="我的电话是:" str[2]="我的邮箱是:aaa@aaa" 

2.Pattern.matcher(CharSequence input)

Matcher类的构造方法也是私有的,不能随意创建,只能通过Pattern.matcher(CharSequence input)方法得到该类的实例.
Pattern类只能做一些简单的匹配操作,要想得到更强更便捷的正则匹配操作,那就需要将Pattern与Matcher一起合作.Matcher类提供了对正则表达式的分组支持,以及对正则表达式的多次匹配支持.
Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("b22bb23"); 
m.pattern();//返回p 也就是返回该Matcher对象是由哪个Pattern对象的创建的 

3.Matcher.matches()/ Matcher.lookingAt()/ Matcher.find()

Matcher类提供三个匹配操作方法,三个方法均返回boolean类型,当匹配到时返回true,没匹配到则返回false
matches()对整个字符串进行匹配,只有整个字符串都匹配了才返回true
matches 和 lookingAt 方法都用来尝试匹配一个输入序列模式。它们的不同是 matches 要求整个序列都匹配,而lookingAt 不要求。lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。

Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("22bb23"); 
m.matches();//返回false,因为bb不能被\d+匹配,导致整个字符串匹配未成功. 
Matcher m2=p.matcher("2223"); 
m2.matches();//返回true,因为\d+匹配到了整个字符串

pattern = Pattern.compile(REGEX);
matcher = pattern.matcher(INPUT);
matcher2 = pattern.matcher(INPUT2);
System.out.println("Current REGEX is: "+REGEX);
System.out.println("Current INPUT is: "+INPUT);
System.out.println("Current INPUT2 is: "+INPUT2);
System.out.println("lookingAt(): "+matcher.lookingAt());
System.out.println("matches(): "+matcher.matches());
System.out.println("lookingAt(): "+matcher2.lookingAt());

//Current REGEX is: foo
//Current INPUT is: fooooooooooooooooo
//Current INPUT2 is: ooooofoooooooooooo
//lookingAt(): true
//matches(): false
//lookingAt(): false

4.Mathcer.start()/ Matcher.end()/ Matcher.group()

当使用matches(),lookingAt(),find()执行匹配操作后,就可以利用以上三个方法得到更详细的信息.
start()返回匹配到的子字符串在字符串中的索引位置.
end()返回匹配到的子字符串的最后一个字符在字符串中的索引位置.
group()返回匹配到的子字符串
Java代码示例:

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("aaa2223bb"); 
m.find();//匹配2223 
m.start();//返回3 
m.end();//返回7,返回的是2223后的索引号 
m.group();//返回2223 

Mathcer m2=m.matcher("2223bb"); 
m.lookingAt();   //匹配2223 
m.start();   //返回0,由于lookingAt()只能匹配前面的字符串,所以当使用lookingAt()匹配时,start()方法总是返回0 
m.end();   //返回4 
m.group();   //返回2223 

Matcher m3=m.matcher("2223bb"); 
m.matches();   //匹配整个字符串 
m.start();   //返回0,原因相信大家也清楚了 
m.end();   //返回6,原因相信大家也清楚了,因为matches()需要匹配所有字符串 
m.group();   //返回2223bb 

5.replaceFirst 和 replaceAll

replaceFirst 替换首次匹配,replaceAll 替换所有匹配

	private static String REGEX = "dog";
    private static String INPUT = "The dog says meow. " +
                                    "All dogs say meow.";
    private static String REPLACE = "cat";
 
    public static void main(String[] args) {
       Pattern p = Pattern.compile(REGEX);
       // get a matcher object
       Matcher m = p.matcher(INPUT); 
       INPUT = m.replaceAll(REPLACE);
       System.out.println(INPUT);
       //The cat says meow. All cats say meow.
   }

6.appendReplacement 和 appendTail 方法

appendReplacement方法:sb是一个StringBuffer,replaceContext待替换的字符串,这个方法会把匹配到的内容替换为replaceContext,并且把从上次替换的位置到这次替换位置之间的字符串也拿到,然后,加上这次替换后的结果一起追加到StringBuffer里(假如这次替换是第一次替换,那就是只追加替换后的字符串啦)。

appendTail方法:sb是一个StringBuffer,这个方法是把最后一次匹配到内容之后的字符串追加到StringBuffer中。

   private static String REGEX = "a*b";
   private static String INPUT = "aabfooaabfooabfoobkkk";
   private static String REPLACE = "-";
   public static void main(String[] args) {
      Pattern p = Pattern.compile(REGEX);
      // 获取 matcher 对象
      Matcher m = p.matcher(INPUT);
      StringBuffer sb = new StringBuffer();
      while(m.find()){
         m.appendReplacement(sb,REPLACE);
      }
      m.appendTail(sb);
      System.out.println(sb.toString());
   }

7.regionStartregionEnd,region方法

regionStart:报告此匹配器区域的开始索引。
regionEnd:报告此匹配器区域的结束索引(不包括)。
region:设置此匹配器的区域限制。重置匹配器,然后设置区域,使其从 start 参数指定的索引开始,到 end 参数指定的索引结束(不包括end索引处的字符)。

Pattern p = Pattern.compile("(\\w+)%(\\d+)");
Matcher m = p.matcher("ab%12-cd%34");
m.region(0, 4);    
while (m.find()) {
    System.out.println("group():" + m.group());
    System.out.println("regionStart():" + m.regionStart());
    System.out.println("regionEnd():" + m.regionEnd());
}
//group():ab%1
//regionStart():0
//regionEnd():4

8.reset()

重置匹配器。可将Matcher 对象状态初始化。将匹配位置指向文件的开头

reset(CharSequence input)

重置此具有新输入序列的匹配器。

Pattern p = Pattern.compile("(\\w+)%(\\d+)");
Matcher m = p.matcher("ab%12-cd%34");
m.reset("ef%56-gh%78");
while (m.find()) {
System.out.println("group():" + m.group());
}

//group():ef%56
//group():gh%78

7.结束语

学以致用,知行合一

本文标签: 正则表达式