正则表达式

什么是正则表达式

         正则表达式(英文:Regular Expression)在计算机科学中,是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。在很多文本编辑器或其他工具里,正则表达式通常被用来检索或替换那些符合某个模式的文本内容。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sedgrep)普及开的。正则表达式通常缩写成“regex”

在现实工作场景中,正则表达式常被用于进行输入验证、内容检索、字符串替换等工作。个人认为正则表达式应该算程序员的必修课之一,由于它应用的非常之广泛,而且在各种语言基本上都可以使用,更重要的是在 Linux 或 Unix 系统下能灵活使用正则表达式也可大大提高工作效率,所以学习它是非常保值的。

正则表达式与正则表达式引擎

直观而言正则表达式是一段负责正则表达式语法的字符串,而负责处理这段表达式的程序,就是正则表达式引擎。表达式引擎由语言或环境提供,作为开发者并不直接面对它。我们只需要编写表达式,然后交给表达式引擎进行处理就可以了。不同语言或环境会以不同方式为我们使用正则表达式提供支持,就拿Java语言来说,我们可以在 String.replaceAll() / String.split() / String.matches() 上直接使用正则表达式,又或者通过 java.util.regex 包中提供的类来使用正则表达式。

不同环境的正则表达式写法与支持或多或少会有一些差别,不过这你可以完全不需要当心,因为这并不影响你使用正则表达式,近代的表达式引擎都非常类似。Perl 5 类型的引擎应该算应用最为广泛的引擎。如果你想了解各种风格的引擎的语法支持,可以参考《Regexp Syntax Summary》。

 

 

表达式与符号

让我们从一个最简单的程序(Java)开始说起:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String string = "gooooooogle";                     
  2. String regex = "go*gle";                           
  3. System.out.println(string.matches(regex));        


 

这个段代码运行的话,将会在控制台输出“true”。它是说字符串 string 与正则表达式 regex 匹配,换句话说,就是字符串 string 符合正则表达式 regex 所描述的模式。在该例子中,最起码我们可以知道正则表达式的操作对象是字符串,而正则表达式也是一个字符串。字符串又是由字符所构成的,在表达式 Go*gle 中 g,o,l,e 是文字字符而 是数量限定字符,它代表前面的字符可以不出现,也可以出现一次或者多次。

文字字符

最基本的正则表达式由单个文字符号组成。如 a ,它将匹配字符串中第一次出现的字符“a”。如对字符串“Jack is a boy”“J”后的“a”将被匹配。而第二个“a”将不会被匹配。正则表达式也可以匹配第二个“a”,这必须是你告诉正则表达式引擎从第一次匹配的地方开始搜索。在文本编辑器中,你可以使用查找下一个。如果变成 Java 代码的话就是这样:使用matcher中的匹配方法,每调用一次,匹配索引由前向后移动,而group方法必须在find/matches/lookingAt方法后使用

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String string = "Jack is a boy";                                            
  2. // 将字符串编译为正则表达式的对象表示形式                                   
  3. Pattern pattern = Pattern.compile("a");                                     
  4. // 创建对字符串 string 根据正则表达式 pattern 进行匹配操作的匹配器对象      
  5. Matcher matcher = pattern.matcher(string);                                  
  6. // 查找下一个匹配的字符串内容,如果找到返回 true,找不到返回 false          
  7. while(matcher.find()) {                                                     
  8.     // 输出捕获到的匹配内容                                                 
  9.     System.out.print(matcher.group() + "\t");                               
  10. }               
  11.                                                             

类似的,cat 会匹配“About cats and dogs”中的“cat”。这等于是告诉正则表达式引擎,找到一个c,紧跟一个a,再跟一个t。要注意,正则表达式引擎缺省是大小写敏感的。除非你告诉引擎忽略大小写,否则 cat 不会匹配“Cat”,就像下面这样。(除了这种方法,还可以在表达式内声明什么内容需要区分大小写什么不需要,这在后面会有介绍)

  

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String string = "About Cats and dogs";                                
  2.  // 在编译表达式时使用标记 CASE_INSENSITIVE,使表达式忽略大小写        
  3.  Pattern pattern = Pattern.compile("cat", Pattern.CASE_INSENSITIVE);   
  4.  Matcher matcher = pattern.matcher(string);                            
  5.  while(matcher.find()) {                                               
  6.      System.out.print(matcher.group() + "\t");                         
  7.  }     

                                                          

特殊字符

对于文字字符,有11个字符被保留作特殊用途。他们是:[ ] \ ^ $ . | ? * + ( )这些特殊字符也被称作元字符。 如果你想在正则表达式中将这些字符用作文本字符,你需要用反斜杠“\”对其进行换码 (escape)。例如你想匹配“1+1=2”,正确的表达式为 1\+1=2 。需要注意的是,1+1=2 也是有效的正则表达式。但它不会匹配“1+1=2”,而会匹配“123+111=234”中的“111=2”。因为“+”在这里表示特殊含义(重复1次到多次)。

不可显示字符

可以使用特殊字符序列来代表某些不可显示字符:
\t 代表Tab(0×09)
\r 代表回车符(0x0D)
\n 代表换行符(0x0A)
要注意的是Windows中文本文件使用“\r\n”来结束一行而Unix使用“\n”

 

Java 正则表达式全攻略 (二) 

 正则表达式引擎的内部工作机制

知道正则表达式引擎是如何工作的,将有助于你很快理解为何某个正则表达式不像你期望的那样工作,还可以使你清楚如何对表达式进行性能优化。从最基本的正则表达式引擎实现思路上来分的话,有两种:确定型有限状态机(Deterministic Finite-State Automaton)简称DFA和不确定型有限状态机(Nodeterministic Finite-State Automaton)简称NFA,也有人称其为文本导向和正则导向。以下这个网址 http://osteele.com/tools/reanimator/ 以一种非常直观的方式说明了 DFA 和 NFA 对相同的表达式的不同编译结果。

由于我们的目的不在于学习状态机,所以我们忽略这2者的工作原理,直接对比他们的影响。就拿表达式 a|ab|abc|abcd 来对比。 我们可以看到NFA的结果比较复杂,而DFA十分简洁,这是否又会影响到2者的性能呢?确实如此,DFA的执行速度与表达式无关,它在编译时的优化已经优于大多数 NFA引擎的复杂优化措施。而NFA的执行速度与表达式有着直接的关系。从匹配结果来看,DFA总是返回最左边最长的匹配结果,而NFA总是比较猴急,总会匹配第一个找到的结果。根据这一点,我们可以轻易分辨出所使用的引擎是DFA还是NFA,你可以使用表达式 nfa|nfa not 对字符串”nfa not”进行测试,如果匹配结果是 nfa ,那该引擎是NFA的,而Java就是属于NFA的。最后一点就是,NFA能提供的功能比DFA更多,例如:捕获由括号内的子表达式匹配的文本、环视,以及其他复杂的零长度确认、惰性量词等。而我们讲的是Java的正则表达式,那当然也就是在说NFA啦,而NFA由于功能比较多用起来比较方便,因此比DFA要流行些。

正则导向的引擎总是返回最左边的匹配

这是需要你理解的很重要的一点:即使以后有可能发现一个更好的匹配,正则导向的引擎也总是返回最左边的匹配。 当把 cat 应用到“He captured a catfish for his cat”,引擎先比较 c 和“H”,结果失败了。于是引擎再比较 c 和“e”,也失败了。直到第四个字符,c 匹配了“c”a 匹配了第五个字符。到第六个字符 t 没能匹配“p”,也失败了。引擎再继续从第五个字符重新检查匹配性。直到第十五个字符开始,cat 匹配上了“catfish”中的“cat”,正则表达式引擎急切的返回第一个匹配的结果,而不会再继续查找是否有其他更好的匹配。

字符集

        字符集是由一对方括号“[]”括起来的字符集合。使用字符集,你可以告诉正则表达式引擎仅仅匹配多个字符中的一个。如果你想匹配一个“a”或一个“e”,使用 [ae]。你可以使用 gr[ae]y 匹配graygrey。这在你不确定你要搜索的字符是采用美国英语还是英国英语时特别有用。相反,gr[ae]y 将不会匹配graaygraey。字符集中的字符顺序并没有什么关系,结果都是相同的。

你可以使用连字符“-”定义一个字符范围作为字符集。[0-9] 匹配09之间的单个数字。你可以使用不止一个范围。[0-9a-fA-F] 匹配单个的十六进制数字,并且大小写不敏感。你也可以结合范围定义与单个字符定义。[0-9a-fxA-FX] 匹配一个十六进制数字或字母X。再次强调一下,字符和范围定义的先后顺序对结果没有影响。

取反字符集

        在左方括号“[”后面紧跟一个尖括号“^”,将会对字符集取反。结果是字符集将匹配任何不在方括号中的字符。不像“.”,取反字符集是可以匹配回车换行符的。

需要记住的很重要的一点是,取反字符集必须要匹配一个字符。q[^u] 并不意味着:匹配一个q,后面没有u跟着。它意味着:匹配一个q,后面跟着一个不是u的字符。所以它不会匹配“Iraq”中的q,而会匹配“Iraq is a country”中的q和一个空格符。事实上,空格符是匹配中的一部分,因为它是一个不是u的字符。如果你只想匹配一个q,条件是q后面有一个不是u的字符,我们可以用后面将讲到的向前查看来解决。

字符集中的元字符

        需要注意的是,在字符集中只有4个 字符具有特殊含义。它们是:] \ ^ -”“]”代表字符集定义的结束;“\”代表转义;“^”代表取反;“-”代表范围定义。其他常见的元字符在字符集定义内部都是正常字符,不需要转义。例如,要搜索星号*或加号+,你可以用 [+*] 。当然,如果你对那些通常的元字符进行转义,你的正则表达式一样会工作得很好,但是这会降低可读性。

        在字符集定义中为了将反斜杠“\”作为一个文字字符而非特殊含义的字符,你需要用另一个反斜杠对它进行转义。[\\x] 将会匹配一个反斜杠和一个X“]^-”都可以用反斜杠进行转义,或者将他们放在一个不可能使用到他们特殊含义的位置。我们推荐后者,因为这样可以增加可读性。比如对于字符“^”,将它放在除了左括号“[”后面的位置,使用的都是文字字符含义而非取反含义。如 [x^] 会匹配一个x^[]x] 会匹配一个“]”“x”[-x] 或 [x-] 都会匹配一个“-”“x”

字符集的简写

因为一些字符集非常常用,所以有一些简写方式。

.

任何字符,会匹配空格(与行结束符可能匹配也可能不匹配)

\d

数字:[0-9]

\D

非数字:[^0-9]

\s

空白字符:[\t\n\x0b\f\r]

\S

非空白字符:[^\s]

\w

单词字符:[a-zA-Z_0-9]

\W

非单词字符:[^\w]


字符集的重复

         如果你用?*+”操作符来重复一个字符集,你将会重复整个字符集。而不仅是它匹配的那个字符。正则表达式 [0-9]+ 会匹配837以及222。如果你仅仅想重复被匹配的那个字符,可以用向后引用达到目的。我们以后将讲到向后引用。

*

重复零次或更多次

+

重复一次或更多次

?

重复零次或一次

{n}

重复n

{n,}

重复n次到更多次

{n,m}

重复nm

结合前面的知识,我们就可以写出以下这类常用的表达式:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 判断字符串是否一个合法的16进制                                          
  2. String regex = "[-+]?0[xX]?[0-9a-fA-F]+";                                  
  3. System.out.println("0xFF".matches(regex));  // true                        
  4. System.out.println("-0Xff".matches(regex)); // true                        
  5. System.out.println("ff".matches(regex));    // false                       
  6. System.out.println("0x1H".matches(regex));  // false                       
  7. // 简单地判断一个字符串是否合法的身份证号码                                
  8. regex = "\\d{15}|\\d{18}";                                                 
  9. System.out.println("440104700101001".matches(regex));    // ture;          
  10. System.out.println("44010700101001".matches(regex));     // false;         
  11. System.out.println("440104197001010015".matches(regex)); // ture;          
  12. System.out.println("4401041970010100015".matches(regex));// false;        


 

 

 

 

Java 正则表达式全攻略 (三)  

 

数量词/限定符

        从前面的例子中,我们可以了解到数量词,是用来指定正则表达式的一个给定字符集必须要出现多少次才能满足匹配。有 或 或 或 {n} 或 {n,} 或 {n,m} 6种。每种都有GreedyReluctantPossessive三种匹配方式,Greedy是默认的匹配方式。*+?限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个?就可以实现懒惰或最小匹配。不同的匹配方式,对正则表达式的执行方式和性能影响都是十分重要的。

Greedy 贪婪

        Greedy量词被看作贪婪,因为它们在试图搜索第一个匹配之前读完(或者说吃掉)整个输入字符串。如果第一个匹配尝试(整个输入字符串)失败,匹配器就会在输入字符串中后退一个字符并且再次尝试,重复这个过程,直到找到匹配或者没有更多剩下的字符可以后退为止。以下面代码为例(它的输出结果为“xfooxxxxxxfoo”)

 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Pattern p = Pattern.compile(".*foo");  
  2. Matcher m = p.matcher("xfooxxxxxxfoo");  
  3. while (m.find()) {  
  4.     System.out.print(m.group() + "\t");  
  5. }  

 

        假设你想用一个正则表达式匹配一个HTML标签。你知道输入将会是一个有效的HTML文件,因此正则表达式不需要排除那些无效的标签。所以如果是在两个尖括号之间的内容,就应该是一个HTML标签。 许多正则表达式的新手会首先想到用正则表达式 <.+> ,他们会很惊讶的发现,对于测试字符串,“This is a <EM>first</EM> test”,你可能期望会返回<EM>,然后继续进行匹配的时候,返回</EM>。 但事实是不会。正则表达式将会匹配“<EM>first</EM>”。很显然这不是我们想要的结果。原因在于Greedy(贪婪)是默认的配置方式。也就是说,“+”会导致正则表达式引擎试图尽可能的重复前导字符。只有当这种重复会引起整个正则表达式匹配失败的情况下,引擎会进行回溯。也就是说,它会放弃最后一次的重复,然后处理正则表达式余下的部分。

 

Reluctant 懒惰

        与Greedy完全相反,Reluctant量词被看作懒惰它们从输入字符串的开头开始,然后逐步地一次读取一个字符搜索匹配。它们最后试图匹配的内容是整个输入字符串。以下面代码为例(它的输出结果为“xfoo    xxxxxxfoo”)

 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Pattern p = Pattern.compile(".*?foo");  
  2. Matcher m = p.matcher("xfooxxxxxxfoo");  
  3. while (m.find()) {  
  4.     System.out.print(m.group() + "\t");  
  5. }  

 

一个用于修正前面HTML问题的可能方案就是使用惰性代替贪婪性。你可以使用 <.+?> 来完成HTML标签的提取。除此以外可以用一个贪婪重复与一个取反字符集:“ <[^>]+> ”。这是一个更好的方案,它利用了Greedy的特性来提高匹配效率。

Possessive 独占

        最后,Possessive量词总是读完整个输入字符串,尝试一次(而且只有一次)匹配。和Greedy量词不同,Possessive从不后退,即使这样做能允许整体匹配成功。继续以代码为例(这次没有任何输出,因为匹配失败)

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Pattern p = Pattern.compile(".*+foo");  
  2. Matcher m = p.matcher("xfooxxxxxxfoo");  
  3. while (m.find()) {  
  4.     System.out.print(m.group() + "\t");  
  5. }  

 

这种情况下,.*+消耗整个输入字符串,在表达式的结尾没有剩下满足“foo”的内容。Possessive量词用于处理所有内容,但是从不后退的情况;在没有立即发现匹配的情况下,它的性能优于功能相同的Greedy量词。

 

 

 

 

 

Java 正则表达式全攻略 (四)  

边界匹配器

^

行的开头

$

行的结尾

\b

单词边界

\B

非单词边界

\A

输入的开头

\G

上一个匹配的结尾

\Z

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

\z

输入的结尾

           边界匹配和一般的正则表达式符号不同,它不匹配任何字符。相反,他们匹配的是字符之前或之后的位置;因此,你不可使用边界匹配器来捕获内容。 ^a 将会匹配字符串“abc”中的a。 ^b 将不会匹配“abc”中的任何字符。类似的,$匹配字符串中最后一个字符的后面的位置。所以 c$ 匹配“abc”中的c

在日常工作中边界匹配是十分有用的。例如用户输入中,常常会有多余的前导空格或结束空格。你可以用 ^\s* 和 \s*$ 来匹配前导空格或结束空格。还有,如果你想检查一段对话内 Frank 这个名字出现了多少次,你可以使用 \bFrank\b 进行匹配,他不会把 Frank 和 Frankie 当成同一个人。

基本上边界匹配都比较容易理解,不过还有有些地方稍微注意一下的。

 

注意行结束符

         行结束符是一个或两个字符的序列,标记输入字符序列的行结尾。以下代码被识别为行结束符:

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

· 后面紧跟新行符的回车符 ("\r\n")

· 单独的回车符 ('\r')

· 下一行字符 ('\u0085')

· 行分隔符 ('\u2028') 

· 段落分隔符 ('\u2029)

         如果你所要匹配的内容有多行,需要使用行边界配置,请不要忘记打开多行模式。因为默认情况下,正则表达式 和 仅分别与整个输入序列的开头和结尾匹配。就拿下面一段代码来说,

 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String string = "This is\r\na test\r\nString.";  
  2. Pattern p = Pattern.compile("^.+$", Pattern.MULTILINE); // 打开多行匹配模式  
  3. Matcher m = p.matcher(string);  
  4. while (m.find()) {  
  5.     System.out.println("[" + m.group() + "]");  
  6. }  

 

        如果你没有打开多行配置模式,你将无法匹配任何内容。

此外,还需要注意的是 . 和行结束符的关系,如果未指定 DOTALL 标志的情况下,则正则表达式 . 可以与任何字符(行结束符除外)匹配。

 

分支的实现

          正则表达式中“|”表示选择。你可以用选择符匹配多个可能的正则表达式中的一个。如果你想搜索文字“cat”“dog”,你可以用 cat|dog 。选择符在正则表达式中具有最低的优先级,也就是说,它告诉引擎要么匹配选择符左边的所有表达式,要么匹配右边的所有表达式。你也可以用圆括号来限制选择符的作用范围。如 \b(cat|dog)\b ,这样告诉正则引擎把(cat|dog)当成一个正则表达式单位来处理。

注意个选择的顺序

\d{5}-\d{4}|\d{5} 这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。如果你把它改成 \d{5}|\d{5}-\d{4} 的话,那么就只会匹配5位的邮编(以及9位邮编的前5)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。

 

 

Java 正则表达式全攻略 (五)  

 

捕获组

        捕获组就是把正则表达式中的一部分用“()”括起来形成组,然后你可以对整个组使用一些正则操作,例如重复操作符。捕获组可以通过从左到右计算其开括号来编号。例如,在表达式 (A)(B(C)) 中,存在四个这样的组:

0

(A)(B(C))

1

(A)

2

(B(C))

3

(C)

如果把表达式改为 ((A)(B(C))) ,则存在五个这样的组:

0

((A)(B(C)))

1

((A)(B(C)))

2

(A)

3

(B(C))

4

(C)

其中0组始终代表整个表达式。我们可以通过下面的代码实例来进一步理解:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Pattern p = Pattern.compile("(\\w+)@(\\w+\\.\\w+)"); // 一个用于匹配邮件地址的简单表达式  
  2. Matcher m = p.matcher("gzyangfan@gmail.com");  
  3. m.matches();                                                 // 进行匹配  
  4. assertThat(m.groupCount(), is(2)); // 返回捕获组数,该表达式不算0组共有2个捕获组  
  5. assertThat(m.group(0), equalTo("gzyangfan@gmail.com"));        // 0组永远代表整个表达式  
  6. assertThat(m.group(1), equalTo("gzyangfan"));                // 1组代表邮箱名  
  7. assertThat(m.group(2), equalTo("gmail.com"));                // 2组代表网站名  

 

捕获组还可以对整个组进行表达式操作,例如重复,我们看下面这个例子:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String regex = "(cat){1,3}";  
  2. assertThat("catcatcat".matches(regex), is(true))  
  3. assertThat("catcat".matches(regex), is(true));  
  4. assertThat("cat".matches(regex), is(true));  
  5. assertThat("dog".matches(regex), is(false));  

 

这个例子里我们定义了一个捕获组,并通过数量词,允许这个组整体出现1次到3次。

 

后引用

         当用“()”定义了一个正则表达式组后,正则引擎则会把被匹配的组按照顺序编号,存入缓存。当对被匹配的组进行向后引用的时候,可以用“\数字的方式进行引用。 \1 引用第一个匹配的后引用组, \2 引用第二个组,以此类推, \n 引用第n个组。而 则引用整个被匹配的正则表达式本身。我们看一个例子。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String string = "联系信息:<tel>020-81234567</tel><email>gzyangfan@gmail.com</MyEmail>";  
  2.  // 范例一  
  3.  Pattern p1 = Pattern.compile("<\\w+>.*?</\\w+>");  
  4.  Matcher m1 = p1.matcher(string);  
  5.  m1.find();  
  6.  assertThat(m1.group(), is("<tel>020-81234567</tel>"));  
  7.  m1.find();  
  8.  assertThat(m1.group(), is("<email>gzyangfan@gmail.com</MyEmail>"));  
  9.   
  10.    
  11.  // 范例二  
  12.  Pattern p2 = Pattern.compile("<(<a target=_blank href="file://\\w+)>.*?</\\1>');">\\w+)>.*?</\\1>");  
  13. </a> Matcher m2 = p2.matcher(string);  
  14.  m2.find();  
  15.  assertThat(m2.group(), is("<tel>020-81234567</tel>"));  
  16.  assertThat(m2.find(), is(false));  

 

             我们可以看到范例一中的表达式无法正确判断结束标签是否与开始标签一致,在代码第9行中将一个不合法的内容也匹配出来了。而范例二用过使用捕获组和对组进行后引用,使错误的标签不会被匹配出来,可见向后引用是非常有用的功能。

不过我们还是需要注意一下后引用的一些要求:

· 一个后向引用不能用于它自身。([abc]\1) 是错误的。因此你不能将 用于一个正则表达式匹配本身,它只能用于替换操作中。

· 后向引用不能用于字符集内部。(a)[\1b] 中的 \1 并不表示后向引用。在字符集内部,\1 可以被解释为八进制形式的转码。


String str = "我要要学学学编编编编程";                                 
		str = str.replaceAll("(.)\\1+","$1");
		System.out.println(str);
		//打印结果"我要学编程"
注意\1和$1的区别:\1是在同一个正则表达式中引用,$1是在另外的正则表达式中引用

 

非捕获组

后引用会降低引擎的速度,因为它需要存储匹配的组。如果你不需要后引用,你可以告诉引擎对某个组不存储,即将其声明为非捕获组。例如:Get(?:Value)。其中“(”后面紧跟的“?:”会告诉引擎组“(Value)”为非捕获组,不存储匹配的值以供后引用。

 

 

Java 正则表达式全攻略 (六)

 零宽断言

        Perl 5 引入了两个强大的正则语法:向前查看向后查看。他们也被称作零长度断言。他们和锚定一样都是零长度的(所谓零长度即指该正则表达式不消耗被匹配的字符串)。不同之处在于前后查看会实际匹配字符,只是他们会抛弃匹配只返回匹配结果:匹配或不匹配。这就是为什么他们被称作断言。他们并不实际消耗字符串中的字符,而只是断言一个匹配是否可能。(ps:现在几乎所有正则表达式引擎的实现都支持向前和向后查看,不过JavaScript只支持向前查看。)

语法

说明

(?=X)

肯定式向前查看,X代表查看的表达式

(?!X)

否定式向前查看,X代表查看的表达式

(?<=X)

肯定式向后查看,X代表查看的表达式

(?<!X)

否定式向后查看,X代表查看的表达式

 

向前查看

        (?=X) 代表肯定式的向前查看,在任何匹配 Pattern 的字符串开始处匹配查找 表达式所代表的字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。这估计还是太难理解,还是直接用实例说明,请看下面的代码:

 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Pattern p = Pattern.compile("Windows(?=NT|2000|2003)");  
  2. Matcher m1 = p.matcher("WindowsXP");  
  3. assertThat(m1.find(), is(false));  
  4. Matcher m2 = p.matcher("Windows2003");  
  5. assertThat(m2.find(), is(true));  
  6. assertThat(m2.group(), equalTo("Windows"));  

 

       表达式 Windows(?=NT|2000|2003) 能匹配 Windows2003 中的 Windows,但不能匹配 WindowsXP 中的 Windows,因为我们的表达式声明了它要匹配的 Windows 后面必须跟着 NT 或 2000 又或 2003 。这种查看的断言是不消耗字符的,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含断言的字符之后开始

如果我们现在把表达式稍作调整,改为 Windows(?!NT|2000|2003) ,这样一来结果就刚好和上面例子的相反。这个时候查看的内容就变为否定式的,在否定式中查看的结果与肯定式相反,如果查看成功将引起整体匹配的失败,而查看失败则匹配成功。

向后查看

与向前查看相反,我们还有向后查看,语法为:(?<=X),它用户限定表达式前必须包含指定的内容。我们还是用一个简单的例子来说明:

 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String string = "dog doggie bulldog";  
  2. // 向后查看的内容为 (?<= )  
  3. Pattern p = Pattern.compile("(?<= )dog");  
  4. Matcher m = p.matcher(string);  
  5. assertThat(m.find(), is(true));  
  6. assertThat(m.group(), equalTo("dog"));  
  7. assertThat(m.start(), is(4));  
  8. assertThat(m.end(), is(7));  

        我们可以看到当我们声明了向后查看 (?< ) 后,我们找到的内容仅为红色的 dog 部分,我们可以从它的工作方式来理解一下。在刚开始匹配时,表达式引擎先向后查看一下有没有空格字符。由于现在在开始位置,前面没有内容,所以查看失败,直接跳过第一个d开始查看oo前面也没有空格,所以也失败进入下个字符。知道到第二个d,这时候向后查看成功了,后续的表达式也匹配成功,因此返回了匹配结果。

同向前查看一样,向后查看也有否定式,它的语法为 (?<!=X) 

 

向前查看和向后查看的应用

        我们来看这样一个例子:查找一个具有6位字符的,含有“cat”的单词。首先,我们可以不用向前向后查看来解决问题,例如:cat\w{3}|\wcat\w{2}|\w{2}cat\w|\w{3}cat 很麻烦吧!
         接下来再让我们来看看使用向前查看的方案。在这个例子中,我们有两个基本需求要满足:一是我们需要一个6位的字符,二是单词含有“cat”。满足第一个需求的正则表达式为 \b\w{6}\b 。满足第二个需求的正则表达式为 \b\w*cat\w*\b 。把两者结合起来,我们可以得到如下的正则表达式:(?=\b\w{6}\b)\b\w*cat\w*\b
具体的匹配过程留给读者。但是要注意的一点是,向前查看是不消耗字符的,因此当判断单词满足具有6个字符的条件后,引擎会从开始判断前的位置继续对后面的正则表达式进行匹配。最后作些优化,可以得到下面的正则表达式: \b(?=\w{6}\b)\w{0,3}cat\w*

 

 

 

 

Java 正则表达式全攻略 (七)  

原子组与防治回溯

         原子组在 Java 里也被称为独立非捕获组.Net方面的说法称为贪婪子表达式,不过我还是觉得称为原子组更合适些。它的语法为 (?>X) ,具体来说,使用原子组匹配和正常的匹配没有差别,只是在匹配到原子组结束时(即闭括号之后),原子组中的所有可供回溯的备用状态都会被丢弃。也就是说,在原子组匹配结束时,原子组的匹配内容只能整体保留或丢弃,回溯始终不能使用这些已匹配过的内容。

         还是以一个实例来说明原子组会更为清晰。例如使用 a(bc|b)c (捕获组)这一表达式你可以匹配 abcc 和 abc,但如果使用 a(?>bc|b)c (原子组)这表达式,你只能匹配 abcc 而不能匹配 abc
在匹配 abc 时,两个表达式都会先匹配到 ,然后在成功匹配到 bc ,再接着尝试匹配 ,这时就会出现匹配失败。一般捕获组的表达式,这时会回溯到进入组前的状态,再匹配另一条路径,先匹配 成功,再重新尝试匹配 也成功,这是 abc 的匹配就成功了。而使用原子组的表达式,由于在 bc 匹配成功,结束组时就备份状态丢弃了,所以当匹配 失败时,就是直接的失败,表达式引擎并没有其它状态可供回溯。

使用原子组可以有效地防止不必要的回溯,因此可以大大提高表达式的性能。

匹配模式

在 Java 内我们除了可以在创建 Pattern 对象实例时,声明匹配模式外,还可以直接在表达式内声明表达式的匹配模式。下面为可用匹配标识的具体含义:

i

CASE_INSENSITIVE

启用不区分大小写的匹配

d

UNIX_LINES

启用 Unix 行模式。在此模式中,.和 的行为中仅识别 ‘\n’ 行结束符。

m

MULTILINE

启用多行模式。

s

DOTALL

启用 dotall 模式。在 dotall 模式中,表达式 可以匹配任何字符,包括行结束符。默认情况下,此表达式不匹配行结束符。

u

UNICODE_CASE

启用 Unicode 感知的大小写折叠。

x

COMMENTS

模式中允许空白和注释。

以下面的代码来说:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String regex = "(?i)ab(?-i)cd";  
  2. assertThat(Pattern.matches(regex, "ABcd"), is(true));  
  3. assertThat(Pattern.matches(regex, "abCD"), is(false));  

 

(?i) 开启了不区分大小写,所以 ABcd 可以被匹配;而 (?-i) 则为关闭不区分大小写,所以 abCD 无法被匹配。对上面的表达式,Java 还支持另一种写法 (?i:ab)(?-i:cd)。

 

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String regex = "^(?i:PNG|PDF)$";  
  2. System.out.println(Pattern.matches(regex, "PDF"));  

 

 

 

Java 正则表达式全攻略 (八) 

注释

         正则表达式大多都是苦涩难读的,因此我们需要在表达式中添加注释,来增强表达式的可读性。Java 正则的注释以“#”开始,回车结束,而且默认是不开启的。我们可以看看下面的例子:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. String regex =  "(19|20)\\d\\d[- /.]#年,只允许是19或20开头的\n" +  
  2.                 "(0?[1-9]|1[012])[- /.]#月,只允许1-12\n" +  
  3.                 "(0?[1-9]|[12][0-9]|3[01])#日,只允许1到31";  
  4. Pattern p = Pattern.compile(regex, Pattern.COMMENTS);  
  5. Matcher m = p.matcher("2010.2.21");  
  6. assertThat(m.matches(), is(true));  
  7. m = p.matcher("1879.1.8");  
  8. assertThat(m.matches(), is(false));  
  9. m = p.matcher("2010.13.21");  
  10. assertThat(m.matches(), is(false));  
  11. m = p.matcher("2010.2.32");  
  12. assertThat(m.matches(), is(false));  


 

这样看起来是不是比较清晰呢?

其他

常用的表达式

1 Email

^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$

2 十六进制值

^#?([a-f0-9]{6}|[a-f0-9]{3})$

3 URL

^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$

4 年--

^(d{2}|d{4})-((0([1-9]{1}))|(1[1|2]))-(([0-2]([1-9]{1}))|(3[0|1]))$

5 IP地址

^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$

6 HTML标签

^<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)$

7 中文字符

[\u4e00-\u9fa5]

8 双字节字符(包括汉字在内)

[^\x00-\xff]

9 国内电话号码

^(\d{3}-|\d{4}-)?(\d{8}|\d{7})?$

10 腾讯QQ

^[1-9]*[1-9][0-9]*$

其他资源

http://regexlib.com/

强烈推荐的一个网站,想找正则表达式,直接在上面查就可以了

http://www.regular-expressions.info/

经典的正则表达式教程,国内大部分正则教程的源头,可惜就是E文的

posted @ 2017-01-18 14:33  車輪の唄  阅读(9)  评论(0编辑  收藏  举报  来源