深入入门正则表达式(java) - 1 - 入门基础

深入入门正则表达式(java) - 引言

深入入门正则表达式(java) - 1 - 入门基础 
深入入门正则表达式(java) - 2 - 基本实例

深入入门正则表达式(java) - 3 - 正则在java中的使用

深入入门正则表达式(java) - 匹配原理 - 1 - 引擎分类与普适原则

深入入门正则表达式(java) - 匹配原理 - 2 - 回溯

 

1.元字符

很多人对正则表达式的印象就是乱码。。许许多多的符号组合在一起,偶见单词,正则确实是这样的,所以下面我们要看看这些符号都是什么意思

有些符号不是大家看到的字面上的意思:比如“.”、“!”、“?” ……

这些符号就称之为元字符

 

很遗憾,这些字符的意义必须牢记

注意:本文的所有红色加粗中文引号内的内容均为正则表达式,而不是一般的字符和符号

 

下面我们逐一说明

“\” :转义符号,在字符组内依然是元字符。

 

在检查一行文本时

“^” :脱字符:表示一行的开始

“$” :美元符号:表示一行的结束

 

字符组

“[]” :一对中括号,里面可以添加任何内容,比如[hate],括号里面的内容是或者的关系,这个正则的意义是:我想匹配一个字符,这个字符可以是h,可以是a,也可以是t或e。

记住:字符组最终只会匹配一个字符。 即使你的目标字符串是hate,那么一次匹配成功的结果也只是第一个字母h,并不是整个单词,如果我就想匹配hate这个单词怎么办?很容易,正则内容为“hate”,在字符组外面的字符的关系是“和,并且”的意思。

 

注意:字符组内的元字符和字符组外的元字符并不一样,即使字符一样,表示的意义也未必相同 (*)

我们先学习下一个内容,然后再来给大家解释上面的这句话

 

“[^]” 括号里面以“^”开头,是字符组的否定形式 ,比如:[^hate]表示:匹配除了h、a、t、e以外的其他字符,依然只会匹配一个字符

之前刚刚学过“^”表示一行的开始,但是脱字符位于[]内的起始位置则表示否定,这也解释了(*) 的内容

如果脱字符位于括号内其他位置表示什么呢?那它表示它自己,此时并不再是一个元字符

 

“-” :减号,可以在字符组内表示区间、范围。比如“[a-z]”,表示匹配a到z直接的任意一个小写字母,如果是“[-z]”,那么“-”只表示它自己,没有任何特殊意义,它在字符组外也没有任何特殊意义。

ps:关于“^”、“$”、“-”的其他用法将在高级基础篇讲述

 

“.” :点号,匹配一个任意字符的字符组简便写法。“.”在字符组内没不是元字符

ps:“.”也不是万能的,有些字符无法匹配,之后会详细解释并给出替代方案

 

“|” :竖线,表示或者,这个很容易记忆,如果想匹配hate或者like,那么正则为:“hate|like”

注意:如果想匹配I hate you和I like you。那么正则为:“I (hate|like) you”。如果写成“I hate|like you”,那么匹配的结果为“I hate”或者是“like you”了

这里圆括号的作用是控制竖线的作用范围,“()”有很多作用,稍后再说

(对于传统NFA来说,选择结构是按顺序的,并不是匹配优先也不是忽略优先,关于NFA请参看 深入入门正则表达式(java) - 匹配原理 - 1 - 引擎分类与普适原则 

 

“\b” :它的作用是单词分隔符,如果我想匹配like,那么正则为“like”,没错,但是会得到一些我不想要的结果,比如likely也会跑到结果集中,我可不想要这些单词。那么修改正则为:“\blike\b”,这回就只能匹配like这个单词了。

注意:java中的单词分隔符为“\b”,有些语言的单词分隔符为“\<” “\>” 

单词边界是什么呢?其实正则没有那么聪明,它不会识别“Ilikeyou”为“I like you”,它只是找到数字和字母的起始位置和结束位置而已

 

“\w” :单词字符。在java中相当于“[a-zA-Z0-9_]”。 但是java中的“\b”却支持Unicode字符。

 

下面我们来看看正则中的“数字” - 量词

“?” :表示0个至1个

“*” :表示0个至任意多个

“+” :表示至少一个

“{min,max}” :区间量词。“{2,5}”,表示匹配2到5次。“{2,}”表示至少两次,“{2}”表示只匹配两次。 “{,2}”,正则错误,无意义

举个例子:正如上面的反面教程所说,如果想匹配一个正整数,那么应该如何来做

首先我们需要明确:我不想匹配0123,只想匹配123这样的数字,0不能开头,第二个要求就是除了数字不能有其他字符了

之 前我们学过字符组了,“[0-9]”可以表示0至9中任意一个数字,那么根据上面的量词,我们改正则为“[0-9]+”,表示至少有一个数字,至多没有限 制。但是0123这样的数字也会满足要求。那么我们再修改一下,首先第一位不能为0,那么这一位可以写为“[1-9]”,表示匹配1到9的任何一个数字, 之后呢?后面的数字有没有都无所谓了,有,那就是多位数,没有,那就是一位数。所以正则为“[1-9][0-9]*”。

 

贪婪与懒惰

我们再来看一个量词的例子。比如我想匹配一个单词,正则可以这么写“[a-zA-Z]+”。

RegexBuddy每次匹配颜色交叉,就能知道每次都匹配到的是什么了

我们来看第一次的结果,是“there”

有个问题就出来了:“\w+”表示至少一个“\w”,那么为什么结果不是“t”、“h”、“e”、“r”、“e”,而是“there”。

上面的量词,除了“{times}”这种指定匹配次数的,其余默认均为贪婪匹配。也就是说尽可能多的匹配。

相对的就有惰性匹配,那么惰性匹配如何使用?

下面修改一下例子:“\w*e”表示以e结尾的单词,现在这里的*还是贪婪匹配。

如果我想匹配到单词中的第一个e,那么如何修改呢?

还是来看there,这回“\w+”只匹配了“th”,并没有匹配到“ther”才停止。

惰性匹配就是尽可能少的匹配,使用方法就是在量词后面加上“?”

如果量词后面没有“?”等其他量词,那么就是默认的贪婪匹配。

 

“?”,“*”,“+”:也叫匹配优先量词

“*?”,“+?”,“??”:也叫忽略优先量词

 

其实还有一种量词:

“?+”,“*+”,“++”:占有优先量词  (支持这种量词的正则引擎很少,java支持)

这节不讨论这种类型量词,之后的章节讨论

 

() :将括号里面的内容作为一个独立的单元,理解为一组。

 

反向引用

“\”+数字 :好多语言能记住“()”里面匹配到的内容,比如java。如果我只想使用“()”匹配到的内容,那么可以使用反向引用。

比如我想找两个一样的单词,总有不小心写错的时候,可能会出现hate hate这样的时候,那么正则应该如何书写呢?

“(hate) \1”

这样就能匹配到了

 

通常我们不只局限于一个单词,那么我们可以写为:“([a-zA-Z]+) \1”,这样也能找到hate hate形式的错误。

注意:这里([a-zA-Z]+)表示至少一个英文字母,后面的\1不表示至少一个英文字母,而是表示说与()匹配的结果一致! “([a-zA-Z]+) \1”不是“([a-zA-Z]+) ([a-zA-Z]+)”的简写

比如:目标字符串为likely like,“([a-zA-Z]+) ”会匹配到likely和一个空格,“\1”不会匹配“([a-zA-Z]+)”,因为之前括号的结果不是like,所以这次查找失败,无结果。

 

反向引用中的数字的确定

例如“((\d)(\d))\d”来匹配123这个数字。那么“\1”的结果是遇见的第一个括号,就是最外面的,“\1”结果为12。按着顺序“\2”结果为1,“\3”结果为2

 

最后再说下“()” 反向引用 的开始已经说出了括号的另一个作用,那就是保存捕获的内容,这种类型的括号叫做捕获型括号 ,相对应的就有非捕获型括号“(?:)”

 

“(?:)” :非捕获型括号。“(?:hate)”会匹配到hate这个字符串,但是如果你企图使用反向引用“(?:hate) \1”,那么就会出现错误。这样的操作是非法的。

注意:非捕获型括号不影响捕获计数 ,获取“((?:\d)(\d))”中的“(\d)”捕获的内容为\2,不再是\3

好处:

1.提高效率,很容易理解,不记住捕获的内容也就不占用内存了

2.结构清晰

 

常用空白字符

“\s” :表示所有空白字符。

“\t” :制表符。

“\n” :换行符。

“\r” :回车。

 

一些其他常用的缩略表示

“\S” :除“\s”之外的任何字符

 \ w” :等同于[a-zA-Z0-9_]

 \W  :除“\w”之外的任何字符

 \ d” :等同于[0-9]

 \ D” :除“d”之外的任何字符

有些工具不支持,比如EditPlus v3.10 中的查找就不支持\d等。

 

环视(零宽断言)

环视分为顺序和逆序,肯定和否定,组合一下一共4种。下面就看看环视到底是什么

“(?=)” :顺序肯定环视:(从左至右)查看文本,如果能够匹配,就返回匹配成功信息。

“(?<=)” :逆序肯定环视:(从右至左)查看文本,如果能够匹配,就返回匹配成功信息。

“(?!)” :顺序否定环视:(从左至右)查看文本,如果不能够匹配,就返回匹配成功信息。

“(?<!)” :逆序否定环视:(从右至左)查看文本,如果不能够匹配,就返回匹配成功信息。

 

下面看几个简单的实例,然后就很容易明白上面的火星语了

例:下面有两句话,加入你只想找到book,不想找到books

there is a book on the desk.
there are some books on the desk.

最简单的办法是:“book\b”,这很容易理解,book后面跟着单词分隔符,book后面如果是s,那么肯定被认为是一个单词,所以这样不会匹配到books。如果用环视,应该如何书写呢

“book(?!\w)”

正则中的book很好理解,依次匹配b、o、o、k,然后呢,\w在上面说过等同于[a-zA-Z0-9],“(?!\w)”是说:我要找这样一个位置,这个位置的后面不能是\w。

第一句话中,在匹配了book后,发现紧跟的是一个空格,恩,不是\w中的内容,匹配成功。

 

注意:环视不会占用字符!环视查找的是字符与字符之间的位置。

环视括号并没有捕获字符的功效,所以不能使用反向引用。


上图画三角号的位置就是这个环视匹配的位置

 

如果想匹配books的book怎么办,很简单“book(?=s)”

 

上面演示了肯定顺序环视和否定顺序环视。下面演示了环视不占用字符。


比较容易理解吧,这个例子没有任何实际意义,仅做演示用

之后会给出一些有意义的例子

 

 

 

2.正则模式与匹配模式

此部分内容基本来自《精通正则表达式》 v3

1.忽略大小写模式: (java中使用Pattern.CASE_INSENSITIVE)

2.宽松排列与注释模式: 此模式会忽略字符组外部的所有空白字符 但是在java.util.regex中,字符组之外的所有空白字符并非都被忽略,而是作为一个“无意义字符(do-nothing metacharacter)” 。正则“\12 3”表“3”在“\12”之后,而非表示“\123”(java中使用Pattern.COMMENTS)

ps:“空白字符”的定义取决于所采用的字符编码的定义,以及此编码对空白字符的支持程度。大多数程序只能识别ASCII的空白字符。

3.点号通配模式(单行模式): (dot-match-all match mode)通常点号是不能匹配换行符的。 但是在java中情况特殊:在sun的正则表达式包,点号能够匹配未使用此模式时点号不能匹配的所有单字符Unicode行终止符。 (java中使用Pattern.DOTALL )

 

符:Unicode行终止符

字符 描述
LF U+000A
VT U+000B
FF U+000C
CR U+000D
CR/LF U+000D U+000A
NEL U+0085
LS U+2028
PS U+2029
ASCII 换行符
ASCII 垂直制表符
ASCII 进纸符
ASCII 回车
ASCII 回车/换行
Unicode 换行
Unicode 行分隔符
Unicode 段分隔符

 

4.增强的锚点模式(多行文本模式): (Enhanced line-anchor match mode)此模式影响“^”和“$”的匹配。 通常情况下“^”不能匹配字符串内部的换行符,而只能匹配目标字符串的起始位置。在此增强模式下,它能够匹配字符串中内嵌的文本行的开头位置。“$”也是这样,此模式下可以匹配字符串内部的换行符。这里可能不是很好理解,之后的实例会介绍,其中还包括“\A”,“\Z” “\z” 的意义(java中使用Pattern.CASE_INSENSITIVE)

5.文字文本模式: 此模式几乎不能识别任何元字符 。比如此模式下正则“[a-z]+” 匹配的就是字符串“[a-z]+”而并不是连续的小写英文字母(java中使用Pattern.CASE_INSENSITIVE)

 

上面的几种模式如果没有理解,可以看之后的教程。

将会在基本实例和高级基础中讨论。

 

本系列将带新手入门,之所以叫深入入门,就是想和网上的各种教程区分

正则总得来说虽然是简单,但是也不是30分钟就能搞定的事情。

 

内容是自己曾经积累的知识总结和书本的提炼:比如《精通正则表达式》 - 中文版第三版

整理过程中难免有错误,可能是笔误也可能是……

顺序是

1.基础知识

2.基本实例

3.java中的正则使用

4.匹配原理

5.技巧与优化

6.android中的正则

 

使用的测试工具是RegexBuddy

工具比较强大,可以模拟多种语言的正则,而且还支持正则中的debug

 

 

正则表达式的作用主要体现在字符串的查找与替换上。

比如我现在做的android开发,调试的时候总是有大量log,没等你看清内容是什么呢新log就把旧log挤掉了,log中夹杂了大量你不关心的内容,于是我经常在adb logcat后面加上grep保证只留下我希望看到的内容。

 

我相信大家在开发过程中肯定遇到过下面的情况:你想查找的内容并不是很确定

举个简单例子:比如你想找hate和like,文本中这样的字符串不多,一次都列出来的话你也很容易能应付过来,所以你并不希望自己要查询两次,那么可以用正则,如果是大写,你也希望匹配,那么也可以用正则……

这样的例子很多,下面我们就从最基础的内容开始,希望大家有点耐心

 

ps: 我非常反对从网上复制“常用”正则之类的内容,首先写这个的人是为了当时自己的需求而写,比如人家匹配电话是4位区号-7位电话,而之后电话号码升到8位 了,于是你的正则就不灵了。这还是好的情况{7}改成{8}就行了,即使程序出了问题不会正则也能猜出来修改哪里。刚才随便在网上搜了一下 “常用正则”,于是发现一个人如下写到

Java代码  收藏代码
  1. ^[0-9]*[1-9][0-9]*$    //匹配正整数  

 

这有很多问题

首先它匹配到的确实是正整数,但是可能会匹配到01这样的类型,我想很多人不希望这样吧

前面的[0-9]*完全多余

 

还有一个例子

Java代码  收藏代码
  1. ^((-\d+)|(0+))$    //匹配非正整数(负整数 + 0)  

我不知道为什么这个人要加这么多括号,这样只会拖慢程序的速度,起码最里面的两个括号我想不到什么理由加上

 

不举例了,相信大家在复制的过程中都遭遇过尴尬

下面开始正题

在有java示例的时候几乎都要引入如下几个类,之后不再赘述。

Java代码  收藏代码
  1. import java.util.regex.Matcher;  
  2. import java.util.regex.Pattern;  
  3. import java.util.regex.PatternSyntaxException;  

 

1.元字符

很多人对正则表达式的印象就是乱码。。许许多多的符号组合在一起,偶见单词,正则确实是这样的,所以下面我们要看看这些符号都是什么意思

有些符号不是大家看到的字面上的意思:比如“.”、“!”、“?” ……

这些符号就称之为元字符

 

很遗憾,这些字符的意义必须牢记

注意:本文的所有红色加粗中文引号内的内容均为正则表达式,而不是一般的字符和符号

 

下面我们逐一说明

“\” :转义符号,在字符组内依然是元字符。

 

在检查一行文本时

“^” :脱字符:表示一行的开始

“$” :美元符号:表示一行的结束

 

字符组

“[]” :一对中括号,里面可以添加任何内容,比如[hate],括号里面的内容是或者的关系,这个正则的意义是:我想匹配一个字符,这个字符可以是h,可以是a,也可以是t或e。

记住:字符组最终只会匹配一个字符。 即使你的目标字符串是hate,那么一次匹配成功的结果也只是第一个字母h,并不是整个单词,如果我就想匹配hate这个单词怎么办?很容易,正则内容为“hate”,在字符组外面的字符的关系是“和,并且”的意思。

 

注意:字符组内的元字符和字符组外的元字符并不一样,即使字符一样,表示的意义也未必相同 (*)

我们先学习下一个内容,然后再来给大家解释上面的这句话

 

“[^]” 括号里面以“^”开头,是字符组的否定形式 ,比如:[^hate]表示:匹配除了h、a、t、e以外的其他字符,依然只会匹配一个字符

之前刚刚学过“^”表示一行的开始,但是脱字符位于[]内的起始位置则表示否定,这也解释了(*) 的内容

如果脱字符位于括号内其他位置表示什么呢?那它表示它自己,此时并不再是一个元字符

 

“-” :减号,可以在字符组内表示区间、范围。比如“[a-z]”,表示匹配a到z直接的任意一个小写字母,如果是“[-z]”,那么“-”只表示它自己,没有任何特殊意义,它在字符组外也没有任何特殊意义。

ps:关于“^”、“$”、“-”的其他用法将在高级基础篇讲述

 

“.” :点号,匹配一个任意字符的字符组简便写法。“.”在字符组内没不是元字符

ps:“.”也不是万能的,有些字符无法匹配,之后会详细解释并给出替代方案

 

“|” :竖线,表示或者,这个很容易记忆,如果想匹配hate或者like,那么正则为:“hate|like”

注意:如果想匹配I hate you和I like you。那么正则为:“I (hate|like) you”。如果写成“I hate|like you”,那么匹配的结果为“I hate”或者是“like you”了

这里圆括号的作用是控制竖线的作用范围,“()”有很多作用,稍后再说

(对于传统NFA来说,选择结构是按顺序的,并不是匹配优先也不是忽略优先,关于NFA请参看 深入入门正则表达式(java) - 匹配原理 - 1 - 引擎分类与普适原则

 

“\b” :它的作用是单词分隔符,如果我想匹配like,那么正则为“like”,没错,但是会得到一些我不想要的结果,比如likely也会跑到结果集中,我可不想要这些单词。那么修改正则为:“\blike\b”,这回就只能匹配like这个单词了。

注意:java中的单词分隔符为“\b”,有些语言的单词分隔符为“\<” “\>”

单词边界是什么呢?其实正则没有那么聪明,它不会识别“Ilikeyou”为“I like you”,它只是找到数字和字母的起始位置和结束位置而已

 

“\w” :单词字符。在java中相当于“[a-zA-Z0-9_]”。 但是java中的“\b”却支持Unicode字符。

 

下面我们来看看正则中的“数字” - 量词

“?” :表示0个至1个

“*” :表示0个至任意多个

“+” :表示至少一个

“{min,max}” :区间量词。“{2,5}”,表示匹配2到5次。“{2,}”表示至少两次,“{2}”表示只匹配两次。 “{,2}”,正则错误,无意义

举个例子:正如上面的反面教程所说,如果想匹配一个正整数,那么应该如何来做

首先我们需要明确:我不想匹配0123,只想匹配123这样的数字,0不能开头,第二个要求就是除了数字不能有其他字符了

之前我们学过字符组了,“[0-9]”可以表示0至9中任意一个数字,那么根据上面的量词,我们改正则为“[0-9]+”,表示至少有一个数字,至 多没有限制。但是0123这样的数字也会满足要求。那么我们再修改一下,首先第一位不能为0,那么这一位可以写为“[1-9]”,表示匹配1到9的任何一 个数字,之后呢?后面的数字有没有都无所谓了,有,那就是多位数,没有,那就是一位数。所以正则为“[1-9][0-9]*”。

 

贪婪与懒惰

我们再来看一个量词的例子。比如我想匹配一个单词,正则可以这么写“[a-zA-Z]+”。

RegexBuddy每次匹配颜色交叉,就能知道每次都匹配到的是什么了

我们来看第一次的结果,是“there”

有个问题就出来了:“\w+”表示至少一个“\w”,那么为什么结果不是“t”、“h”、“e”、“r”、“e”,而是“there”。

上面的量词,除了“{times}”这种指定匹配次数的,其余默认均为贪婪匹配。也就是说尽可能多的匹配。

相对的就有惰性匹配,那么惰性匹配如何使用?

下面修改一下例子:“\w*e”表示以e结尾的单词,现在这里的*还是贪婪匹配。

如果我想匹配到单词中的第一个e,那么如何修改呢?

还是来看there,这回“\w+”只匹配了“th”,并没有匹配到“ther”才停止。

惰性匹配就是尽可能少的匹配,使用方法就是在量词后面加上“?”

如果量词后面没有“?”等其他量词,那么就是默认的贪婪匹配。

 

“?”,“*”,“+”:也叫匹配优先量词

“*?”,“+?”,“??”:也叫忽略优先量词

 

其实还有一种量词:

“?+”,“*+”,“++”:占有优先量词 (支持这种量词的正则引擎很少,java支持)

这节不讨论这种类型量词,之后的章节讨论

 

() :将括号里面的内容作为一个独立的单元,理解为一组。

 

反向引用

“\”+数字 :好多语言能记住“()”里面匹配到的内容,比如java。如果我只想使用“()”匹配到的内容,那么可以使用反向引用。

比如我想找两个一样的单词,总有不小心写错的时候,可能会出现hate hate这样的时候,那么正则应该如何书写呢?

“(hate) \1”

这样就能匹配到了

 

通常我们不只局限于一个单词,那么我们可以写为:“([a-zA-Z]+) \1”,这样也能找到hate hate形式的错误。

注意:这里([a-zA-Z]+)表示至少一个英文字母,后面的\1不表示至少一个英文字母,而是表示说与()匹配的结果一致! “([a-zA-Z]+) \1”不是“([a-zA-Z]+) ([a-zA-Z]+)”的简写

比如:目标字符串为likely like,“([a-zA-Z]+) ”会匹配到likely和一个空格,“\1”不会匹配“([a-zA-Z]+)”,因为之前括号的结果不是like,所以这次查找失败,无结果。

 

反向引用中的数字的确定

例如“((\d)(\d))\d”来匹配123这个数字。那么“\1”的结果是遇见的第一个括号,就是最外面的,“\1”结果为12。按着顺序“\2”结果为1,“\3”结果为2

 

最后再说下“()” 反向引用 的开始已经说出了括号的另一个作用,那就是保存捕获的内容,这种类型的括号叫做捕获型括号 ,相对应的就有非捕获型括号“(?:)”

 

“(?:)” :非捕获型括号。“(?:hate)”会匹配到hate这个字符串,但是如果你企图使用反向引用“(?:hate) \1”,那么就会出现错误。这样的操作是非法的。

注意:非捕获型括号不影响捕获计数 ,获取“((?:\d)(\d))”中的“(\d)”捕获的内容为\2,不再是\3

好处:

1.提高效率,很容易理解,不记住捕获的内容也就不占用内存了

2.结构清晰

 

常用空白字符

“\s” :表示所有空白字符。

“\t” :制表符。

“\n” :换行符。

“\r” :回车。

 

一些其他常用的缩略表示

“\S” :除“\s”之外的任何字符

\ w” :等同于[a-zA-Z0-9_]

\W :除“\w”之外的任何字符

\ d” :等同于[0-9]

\ D” :除“d”之外的任何字符

有些工具不支持,比如EditPlus v3.10 中的查找就不支持\d等。

 

环视(零宽断言)

环视分为顺序和逆序,肯定和否定,组合一下一共4种。下面就看看环视到底是什么

“(?=)” :顺序肯定环视:(从左至右)查看文本,如果能够匹配,就返回匹配成功信息。

“(?<=)” :逆序肯定环视:(从右至左)查看文本,如果能够匹配,就返回匹配成功信息。

“(?!)” :顺序否定环视:(从左至右)查看文本,如果不能够匹配,就返回匹配成功信息。

“(?<!)” :逆序否定环视:(从右至左)查看文本,如果不能够匹配,就返回匹配成功信息。

 

下面看几个简单的实例,然后就很容易明白上面的火星语了

例:下面有两句话,加入你只想找到book,不想找到books

there is a book on the desk.
there are some books on the desk.

最简单的办法是:“book\b”,这很容易理解,book后面跟着单词分隔符,book后面如果是s,那么肯定被认为是一个单词,所以这样不会匹配到books。如果用环视,应该如何书写呢

“book(?!\w)”

正则中的book很好理解,依次匹配b、o、o、k,然后呢,\w在上面说过等同于[a-zA-Z0-9],“(?!\w)”是说:我要找这样一个位置,这个位置的后面不能是\w。

第一句话中,在匹配了book后,发现紧跟的是一个空格,恩,不是\w中的内容,匹配成功。

 

注意:环视不会占用字符!环视查找的是字符与字符之间的位置。

环视括号并没有捕获字符的功效,所以不能使用反向引用。


上图画三角号的位置就是这个环视匹配的位置

 

如果想匹配books的book怎么办,很简单“book(?=s)”

 

上面演示了肯定顺序环视和否定顺序环视。下面演示了环视不占用字符。


比较容易理解吧,这个例子没有任何实际意义,仅做演示用

之后会给出一些有意义的例子

 

 


2.正则模式与匹配模式

此部分内容基本来自《精通正则表达式》 v3

1.忽略大小写模式: (java中使用Pattern.CASE_INSENSITIVE)

2.宽松排列与注释模式: 此模式会忽略字符组外部的所有空白字符 但是在java.util.regex中,字符组之外的所有空白字符并非都被忽略,而是作为一个“无意义字符(do-nothing metacharacter)” 。正则“\12 3”表“3”在“\12”之后,而非表示“\123”(java中使用Pattern.COMMENTS)

ps:“空白字符”的定义取决于所采用的字符编码的定义,以及此编码对空白字符的支持程度。大多数程序只能识别ASCII的空白字符。

3.点号通配模式(单行模式): (dot-match-all match mode)通常点号是不能匹配换行符的。 但是在java中情况特殊:在sun的正则表达式包,点号能够匹配未使用此模式时点号不能匹配的所有单字符Unicode行终止符。 (java中使用Pattern.DOTALL )

 

符:Unicode行终止符

字符 描述
LF U+000A
VT U+000B
FF U+000C
CR U+000D
CR/LF U+000D U+000A
NEL U+0085
LS U+2028
PS U+2029
ASCII 换行符
ASCII 垂直制表符
ASCII 进纸符
ASCII 回车
ASCII 回车/换行
Unicode 换行
Unicode 行分隔符
Unicode 段分隔符

 

4.增强的锚点模式(多行文本模式): (Enhanced line-anchor match mode)此模式影响“^”和“$”的匹配。 通常情况下“^”不能匹配字符串内部的换行符,而只能匹配目标字符串的起始位置。在此增强模式下,它能够匹配字符串中内嵌的文本行的开头位置。“$”也是这样,此模式下可以匹配字符串内部的换行符。这里可能不是很好理解,之后的实例会介绍,其中还包括“\A”,“\Z” “\z” 的意义(java中使用Pattern.CASE_INSENSITIVE)

5.文字文本模式: 此模式几乎不能识别任何元字符 。比如此模式下正则“[a-z]+” 匹配的就是字符串“[a-z]+”而并不是连续的小写英文字母(java中使用Pattern.CASE_INSENSITIVE)

 

上面的几种模式如果没有理解,可以看之后的教程。

将会在基本实例和高级基础中讨论。

这里例子都是从《正则表达式经典实例》中选取的

书中的第5、6章每个例子我都独立完成了一遍,觉得基础的会拿出来给大家分享

我把长度为0的匹配结果看作一个缝隙,比如“\b”是单词之间的缝隙,“^”是字符与开头的缝隙……

 

A.单词

1.查找单词cat

 

正则:\bcat\b

开始的例子总是很简单,之前我们讲过了\b可以找到单词的边界,\b会匹配下面3种位置

1.如果目标文本第一个字符是单词字符,则匹配第一个字符之前的位置

2.如果目标文本最后一个字符是单词字符,则匹配最后一个字符之后的位置

3.如果两个连续的字符中一个是单词字符一个不是,那么匹配中间位置

注意:java的\b是支持Unicode字符的。

知识点:单词分割符

 

2.查找多个单词之一,如:匹配one或者two或three

正则:\b(?:one|two|three)\b

这个也很容易理解,使用非捕获括号是为了提高效率,不让程序单独记住括号内捕获的内容

当然,你可以这么写“\b(?:one|t(?:wo|hree))\b” ,似乎只是为了让人更加迷惑。。。

知识点:单词分割符,非捕获括号,选择结构

 

3.查找相似单词,如:Steven、Steve、Stephen

正则:\bSte(?:ven?|phen)\b

这几个单词都是以Ste开头,后面公共的部分就只有一个e了,最后的n可有可无。如果你这么想,那你的正则很有可能写成下面这样

\bSte(?:v|ph)en?\b

不仔细看还真容易被糊弄过去,当然,上面3中形式都可以匹配,但是,它是不是也能匹配Stephe呢?

知识点:单词分割符 ,非捕获括号,选择结构 ,量词(贪婪匹配)

 

4.查找除了某个单词之外的任意单词,如:查找除了cat之外的任意其他单词

正则:\b(?!cat\b)\w+\b

如果查找任意一个单词很容易:\b\w+\b,我们来改造一下,首先肯定要有\b,这能让我们找到单词的开头,然后我们要看看接下来的三个字母,如果他们分别是c、a、t,并且之后单词结束,那我们就不需要,否则就是我们需要的。

“(?!cat)”,它匹配一个缝隙,这个缝隙的后面不是c、a、t,如果我们找到了这样的缝隙“(?!cat\b)”,那么这个缝隙之后的单词字符就是我们所需要的。当然我们可以把\w换成[a-zA-Z]

知识点:单词分割符 ,非捕获括号,单词字符,顺序否定环视 ,量词(贪婪匹配)

 

5.查找不包含另一个单词的单词,如:查找不包含cat的单词

正则:\b(?:(?!cat)\w)+\b

这个结果看上去和上一个题目差不多,我们来看看他们的区别。

(?!cat)上面已经解释过了,那么我们找到了这个位置,后面是不是就都是我们所需要的字符了呢?如果你写成了\b(?!cat)\w+\b ,那么很遗憾,答案是错的。它只会保证匹配的结果不是以cat开头而已。我们可以想一下查找过程:我们先看单词的前3个字符,如果不是cat,那么我们看 单词的第2、3、4个字符是不是cat,这样就能排除acat了。如果第2、3、4不是cat,那么我们看第3、4、5个字符……直到看完最后三个字符位 置。也就是说,我们每确定一个我们需要的字符,就要重新看看我们现在所在位置后的3个字符是否满足条件,所以我们要多次使用顺序否定环视((?!cat)\w)+ 。最终结果为\b(?:(?!cat)\w)+\b

知识点:单词分割符 ,非捕获括号,单词字符,顺序否定环视 ,量词(贪婪匹配)

 

6.查找后面不跟着某个特定单词的任意单词,如:查找后面不跟着cat的任意单词,可以忽略其中空格和标点和非单词字符

正则:\b\w+\b(?!\W+cat\b)

先找到一个单词\b\w+\b ,然后看看接下来的内容是什么,如果后面跟着cat,那么匹配失败,否则匹配成功。

ps:当然这个正则是可以匹配到cat这个单词本身的,只要cat后面不再跟着cat。

知识点:单词分割符 单词字符,非 单词字符, 顺序否定环视 ,量词(贪婪匹配)

 

7.查找不跟在某个特定单词之后的任意单词,如:查找之前的单词不是cat的单词

正则:(?<!\bcat \W{1,9} ) \b\w+\b

和上一个题目很像,上一个题目要求后面不是什么什么,现在要求前面不是什么什么,所以很容易修改上面的正则:把顺序否定环视改为逆序否定环视 “(?!) ” -> “(?<!) ”。然后把环视的内容倒过来“\W+cat\b ” -> “\bcat\W+ ”,结果为(?<!\bcat \W+ ) \b\w+\b

很遗憾,在java中无法运行。原因是逆序环视和“+”的结合。 java中逆序环视不允许出现这种无限制长度的量词,但是可以使用区间量词。 似乎只有.NET允许这么写。所以我们把“+”改为“{1,9}”,这个结果是书中给的,当然长度自己定,我倾向于“{1,20}”。

知识点:单词分割符 单词字符,非 单词字符,逆 序否定环视 ,量词(贪婪匹配)

 

8.查找重复单词,如:连续写了两个相同的单词,中间空白字符相隔。

正则:\b([a-zA-Z]+)\s+\1\b

这个比较简单,所以不解释了。

知识点:单词分割符 字符集,空白字符 捕获括号, 反向引用 ,量词(贪婪匹配)

 

B.数字

 

1.整数

正则:-?(?:[1-9]\d*|0)

 

首先明确整数除了0之外,不能以0开头。那么[1-9]开头,正整数即为“[1-9]\d*”,非负整数为“[1-9]\d*|0”。

ps:书中的答案数字是允许以0开头的(后来发现在6.4节给出了数字前面非0的答案)

书中不推荐使用\d替代[0-9],因为在.NET和Perl中,\d能匹配到任意文字中的任意数字,不过在java中不必担心。

知识点:选择结构 字符集,数字字符集简写,非 捕获括号, 量词(贪婪匹配)

 

2.特定范围内的数字

1-12(小时,月份)

正则:^1[012]|\d$

 

1-24(小时)

正则:^2[0-4]|1\d|[1-9]$

 

1-31(每月的天次)

正则:^3[01]|[12]\d|[1-9]$

 

1-53(每年的周次)

正则:^5[0-3]|[1-4]\d|[1-9]$

 

0-59(分、秒)

正则:^[1-5]?\d$

 

0-100(百分数)

正则:^[1-9]?\d|100$

 

1-100

正则:^[1-9]\d?|100$

^[1-9]\d|[1-9]|100$可简化为^[1-9]\d?|100$

 

32-126(可打印的ASCII字符)

正则:^3[2-9]|[4-9]\d|1[01]\d|12[0-6]$

 

0-127(非负有符号字节数)

正则:^[1-9]?\d|1[01]\d|12[0-7]$

根据0-100修改。

 

-128-127(有符号字节数)

正则:^[1-9]?\d|1[01]\d|12[0-7]|-(?:[1-9]\d?|1[01]\d|12[0-8])$

0-127和-1-至-128

当然也可以写成1-127和-1至-127和0和-128,其实我觉得这样更简单一些。

 

0-255(无符号字节数)

正则:^[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]$

根据0-127修改。

 

1-366(一年中的天次)

正则:^[1-9]\d?|[12]\d\d|3[0-5]\d|36[0-6]$

根据1-100修改。

知识点:选择结构 字符集,数字字符集简写,非 捕获括号, 量词(贪婪匹配)

上面的正则没有什么太多难点,只是数字大了之后情况就多了,所以需要谨慎的考虑。情况多了正则就会变长,看着比较吓人而已。

 

基本实例就列举这些吧,这里包含了上一节的90%的知识点,看完这节内容的同学写一些简单的正则应该是没有什么问题了

下一节主要介绍java中的正则特性以及相关的api。

jdk版本选为1.6

1.5,1.4中的正则bug较多

 

我们先来总结一下java正则流派的特性,这里直接完全引用《精通正则表达式》中的表格

1.字符缩略表示法

\a [\b] \e \f \n \r \t \0octal \x## \u#### \cchar  ---  \u####只运行4位16进制数字;\0octal要求开头是0,后面接1至3为10进制数字;\cchar是区分大小写的,直接对后面字符的十进制编码进行异或操作。

2.字符组及相关结构

字符组:[...],[^...],可包含运算符

几乎任何字符:点号(根据模式不同,含义不同)

字符组缩略表示法:\w \d \s \W \D \S  ---  \w \W只能识别ASCII字符

3.锚点及其他零长断言

行/字符串起始位置:^ \A

行/字符串结束位置:$ \z \Z

当前匹配的起始位置:\G

单词分解符:\b \B  ---  能够识别Unicode字符

环视结构:(?=...) (?!...) (?<=...) (?<!...)  ---  顺序环视结构中可以使用任意正则表达式,逆序环视中只能匹配长度有限的文本

4.注释及修饰模式

模式修饰符:(?mods-mods)允许出现的模式:x d s m i u

模式修饰范围:(?mods-mods:...)

注释:从#到行末(只有在启动时有效)  ---  只有在使用/x修饰符或者Pattern.COMMENTS选项时,#才算注释。没有转移的ASCII空白字符将被忽略。字符组内部的注释和空白字符也会被忽略

文字文本模式:\Q...\E

5.分组及捕获

捕获型括号:(...)  \1 \2...

仅分组的括号:(?:...)

固化分组:(?>...)

多选结构:|

匹配优先量词:* + ? {n} {m,n} {m,}

忽略优先量词:*? +? ?? {n}? {n,}? {m,n}?

占有优先量词:*+ ++ ?+ {n}+ {n,}+ {m,n}+

ps:其中标注为蓝绿色的内容将在之后的教程讲解

 

 

下面开始介绍java中的正则api

首先看看正则的编译

Java代码  收藏代码
  1. Pattern regex = Pattern.compile(".*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);  

正则的编译相对来说很耗时 ,所以要注意复用。

第一个参数是正则,第二个是编译选项,可以同时指定多个,当然,也可以像下面这样什么也不指定

Java代码  收藏代码
  1. Pattern regex = Pattern.compile(".*?");  

 

Matcher

我们把字符串传给matcher,然后设置各种条件,最后再用它干活

下面看看matcher都能干些什么

首先要获取Matcher对象

Java代码  收藏代码
  1. Matcher matcher = pattern.matcher(str);  

中途要更换正则

Java代码  收藏代码
  1. matcher.usePattern(newPattern);  

中途替换目标字符串

Java代码  收藏代码
  1. matcher.reset(str);  

此时的matcher会丢失之前所有的明确的状态信息 - 比如下面要说到的搜索范围,之前匹配过的信息也就没有了

另一个相似函数,只是没有替换字符串而已

Java代码  收藏代码
  1. matcher.reset();  

 

设定搜索范围

Java代码  收藏代码
  1. matcher.region(start, end);  
  2. matcher.regionStart();  
  3. matcher.regionEnd();  

第一个用做设置搜索边界。默认为搜索整个字符串

后两个用来得到设置的边界位置

 

设置边界后的环视

Java代码  收藏代码
  1. matcher.useTransparentBounds(bool);  
  2. matcher.hasTransparentBounds();  

如果设置了边界,那么环视查找时,是否允许检查环视外的字符可以通过上面的函数设置

默认为false,也就是说不考虑边界之外的字符。 

给一个简单的例子

目标字符串为abcde,我要查找b,但是要求b的前面是a。

如果边界设置为[1,5],也就是在bcde中查找,那么默认情况下是匹配不到结果的,,因为b已经在边界上了

但是如果允许在边界外检查,那么这里的b就符合要求

Java代码  收藏代码
  1. String str = "abcde";  
  2. String regex = "(?<=a)b";  
  3. Pattern pattern = Pattern.compile(regex);  
  4. Matcher matcher = pattern.matcher(str);  
  5. matcher.region(15);//设置边界  
  6. System.out.println("hasTransparentBounds:" + matcher.hasTransparentBounds());//查看默认状态  
  7. System.out.println("find:" + matcher.find());//查找结果  
  8.   
  9. matcher.reset();//重置  
  10. System.out.println("hasTransparentBounds:" + matcher.hasTransparentBounds());//查看重置后状态  
  11. matcher.useTransparentBounds(true);//设置  
  12. System.out.println("find:" + matcher.find());//查看结果  
  13.   
  14. matcher.reset();//重置  
  15. System.out.println("hasTransparentBounds:" + matcher.hasTransparentBounds());//查看重置后状态  

输出:

Xml代码  收藏代码
  1. hasTransparentBounds:false  
  2. find:false  
  3. hasTransparentBounds:false  
  4. find:true  
  5. hasTransparentBounds:true  

我们可以看出,hasTransparentBounds默认是false

重置之后依然是false,当设置为true的时候再去重置,hasTransparentBounds没有改变

 

应用正则

查找

Java代码  收藏代码
  1. matcher.find();  
  2. matcher.find(int);  

find():在当前检索范围应用正则。如果找到匹配,返回true,否则返回false。多次调用,则每次都从上次匹配之后的位置开始查找。

Java代码  收藏代码
  1. String str = "are you a boy?";  
  2. String regex = "\\b\\w+\\b";  
  3. Pattern pattern = Pattern.compile(regex);  
  4. Matcher matcher = pattern.matcher(str);  
  5. while (matcher.find()) {  
  6.     System.out.println(matcher.group());  
  7. }  

结果:

Xml代码  收藏代码
  1. are  
  2. you  
  3. a  
  4. boy  

 

find(int):参数为查找的起始偏移量。此函数不受当前检索范围影响,因为它调用了reset

Java代码  收藏代码
  1. public boolean find(int start) {  
  2.     int limit = getTextLength();  
  3.     if ((start < 0) || (start > limit))  
  4.         throw new IndexOutOfBoundsException("Illegal start index");  
  5.     reset();  
  6.     return search(start);  
  7. }  

 

完全的匹配

Java代码  收藏代码
  1. matcher.matches();  

正则如果能完全匹配目标字符串,那么返回true,否则返回false。匹配成功意味着匹配的结果为检索范围开始到检索范围结束的所有文本。

 

Java代码  收藏代码
  1. matcher.lookingAt();  

与matches()类似,但是不要求检索范围内的整段文本都能匹配

 

匹配结果

Java代码  收藏代码
  1. matcher.group();  
  2. matcher.group(int);  
  3. matcher.groupCount();  

group()返回上一次匹配的完整结果

group(int)返回上一次匹配中第N组的结果,如果N=0,那么同group()结果一样

Java代码  收藏代码
  1. public String group() {  
  2.     return group(0);  
  3. }  

groupCount()返回捕获型括号的数目,组数

 

以下几个函数返回匹配结果的位置,其中无参的返回完整匹配的起始和结束位置,有参的返回分组匹配的起始和结束位置

Java代码  收藏代码
  1. matcher.start();  
  2. matcher.start(int);  
  3. matcher.end();  
  4. matcher.end(int);  

 

替换

Java代码  收藏代码
  1. matcher.replaceAll(String);  
  2. matcher.replaceFirst(String);  

返回目标字符串副本,其中匹配到的字符被替换

 

Java代码  收藏代码
  1. matcher.appendReplacement(StringBuffer result, String replacement);  
  2. matcher.appendTail(StringBuffer result);  

appendReplacement:将上次匹配结束到这次匹配之前的字符串加入result,然后将这次匹配的内容替换为replacement后加入result

appendTail:找到所有匹配(或用户期望的匹配)后,将剩余的字符串加入result

 

下面是jdk6中的示例

Java代码  收藏代码
  1. Pattern p = Pattern.compile("cat");  
  2. Matcher m = p.matcher("one cat two cats in the yard");  
  3. StringBuffer sb = new StringBuffer();  
  4. while (m.find()) {  
  5.     m.appendReplacement(sb, "dog");  
  6. }  
  7. m.appendTail(sb);  
  8. System.out.println(sb.toString());  

输出:

Xml代码  收藏代码
  1. one dog two dogs in the yard  

红色为上次匹配之前到这次匹配之间的字符串

蓝绿色为这次匹配的字符串,将被替换成replacement

深蓝色为appendTail的工作

由于空格无法看出颜色,所以将空格用横线替代

 

过程为:

1."one- cat -two-cats-in-the-yard",result="one-dog"

2."one- cat -two- cat s-in-the-yard",result="one-dog-two-dogs"

3."one- cat -two- cat s -in the yard ",result="one-dog two-dogs-in-the-yard"

 

 

扫描程序

两个相关的api

Java代码  收藏代码
  1. matcher.hitEnd();  
  2. matcher.requireEnd();  
  3.   
  4. /** 
  5.  * Boolean indicating whether or not more input could change 
  6.  * the results of the last match.  
  7.  *  
  8.  * If hitEnd is true, and a match was found, then more input 
  9.  * might cause a different match to be found. 
  10.  * If hitEnd is true and a match was not found, then more 
  11.  * input could cause a match to be found. 
  12.  * If hitEnd is false and a match was found, then more input 
  13.  * will not change the match. 
  14.  * If hitEnd is false and a match was not found, then more 
  15.  * input will not cause a match to be found. 
  16.  */  
  17. boolean hitEnd;  
  18.   
  19. /** 
  20.  * Boolean indicating whether or not more input could change 
  21.  * a positive match into a negative one. 
  22.  * 
  23.  * If requireEnd is true, and a match was found, then more 
  24.  * input could cause the match to be lost. 
  25.  * If requireEnd is false and a match was found, then more 
  26.  * input might change the match but the match won't be lost. 
  27.  * If a match was not found, then requireEnd has no meaning. 
  28.  */  
  29. boolean requireEnd;  

hitEnd:

如果为true,继续输入可能导致之前的匹配更改为一个新的匹配 (或者之前匹配成功,之后丢失匹配,匹配失败**) ,或者之前没有匹配后来有了匹配。

如果为false,继续输入则不会改变匹配结果。

关于**说明:变量上面的注释似乎没有说明这一点,但是《精通正则表达式》提及到了,**的结论是正确的。下面给出一个例子

Java代码  收藏代码
  1. String subjectString = "1";  
  2. Pattern regex = Pattern.compile("^\\d$", Pattern.CASE_INSENSITIVE);  
  3. Matcher regexMatcher = regex.matcher(subjectString);  
  4. while(regexMatcher.find()){  
  5.     System.out.println(regexMatcher.group());  
  6.     System.out.println(regexMatcher.hitEnd());  
  7. }  

上面的例子中,我只想匹配一个数字,那么结果是能匹配到的,输出如下

Xml代码  收藏代码
  1. 1  
  2. true  

 

如果目标字符串有两个数字,那么

Java代码  收藏代码
  1. String subjectString = "12";  
  2. Pattern regex = Pattern.compile("^\\d$", Pattern.CASE_INSENSITIVE);  
  3. Matcher regexMatcher = regex.matcher(subjectString);  
  4. while(regexMatcher.find()){  
  5.     System.out.println(regexMatcher.group());  
  6.     System.out.println(regexMatcher.hitEnd());  
  7. }  

则没有输出

也就是说,hitEnd=true,并且之前是能找到匹配的,但是继续输入字符串,结果有可能变为无法找到匹配。

 

 

requireEnd:

如果为true,继续输入可能导致之前的丢失之前的匹配结果

如果为false,并且找到了匹配,更多的输入可能会导致之前的匹配内容改变,但是结果不会改变;如果没有找到匹配,那么此变量无意义。

 

 

最后看看Pattern的几个方法

Java代码  收藏代码
  1. split(CharSequence input);  
  2. split(CharSequence input,int limit);  

split(CharSequence input):以input匹配到的内容做分割,返回分割好的数组

split(CharSequence input,int limit):分三种情况

1.limit<0:会保留结尾的空元素

2.limit=0:与split(CharSequence input)相同

3.limit>0:返回的数组最多为limit项,正则至多会应用limit-1次

下面对1和3举例说明:

Java代码  收藏代码
  1. Pattern regex = Pattern.compile(",");  
  2. String[] ss = regex.split("a,b,c,d,",limit);  
  3. for (int i = 0; i < ss.length; i++) {  
  4.     System.out.println(ss[i]);  
  5. }  

limit=-1时,数组为5个元素:“a”,“b”,“c”,“d”和一个空字符串

limit=2时,数组为2个元素:“a”,“b,c,d,”,只应用了一次正则

 

编译参数

Java代码  收藏代码
  1. regex.flags();  

返回compile时传递的参数

 

块转义:

\Q...\E 将\Q和\E之间的正则转义为字面意义。 比如正则:\Q[1]\E,表示的是匹配一对方括号,里面有一个数字1,而不是只有数字1的字符组。

下面的静态函数有同样的功效

Java代码  收藏代码
  1. regex.quote(String s);  

例:

Java代码  收藏代码
  1. System.out.println(Pattern.quote("[1]"));  
  2. //输出为\Q[1]\E  

 

查找:

Java代码  收藏代码
  1. Pattern.matches(String regex, CharSequence input);  

 看了matches的源码我们就知道其含义了

Java代码  收藏代码
  1. public static boolean matches(String regex, CharSequence input) {  
  2.     Pattern p = Pattern.compile(regex);  
  3.     Matcher m = p.matcher(input);  
  4.     return m.matches();  
  5. }  

 

 

至此java中的正则基本使用就介绍完了,希望大家拍砖的同时能给出意见,多谢

 

本节第一部分主要介绍正则引擎的分类,由于java属于NFA,所以只重点介绍此类。其余类型简要或不做介绍。

分类的内容全部来自《精通正则表达式》v3

 

引擎类型 程序
DFA awk(大多数版本)、egrep(大多数版本)、flex、lex、MySQL、Procmail
传统NFA GNU Emacs、Java、grep(大多数版本)、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

 

NFA(非确定型有穷自动机):表达式主导

正则:“to(nite|knight|night)”

目标文本:“tonight”

正则表达式从“t”开始,每次检查一部分(由引擎查看表达式的一部分),同时检查当前文本是否匹配表达式的当前部分。如果是,则继续表达式的下一部分,直到表达式的所有部分都能匹配。

此例中第一个元素是“t”,它会重复尝试,在目标字符串中找到“t”为止,然后检查“o”,过程与此一致。然后是“(nite|knight|night)”部分,表达式会一次尝试,直到宣告匹配成功或失败才会停止。 表达式中的控制权在不同元素之间转换,所以作者称其为“表达式主导”

所以正则:“nfa|nfa not”,目标字符串:“nfa not”中,也只是匹配“nfa”而已,而不会完整的匹配。

 

DFA (确定型有穷自动机) :文本主导

DFA引擎在扫描字符串时,会记录“当前有效”的所有匹配可能。

还是最初的例子,引擎移动到“t”时,它会在当前处理匹配可能中添加一个潜在的可能

接下来扫描的每个字符,都会更新当前的可能匹配序列。继续扫描两个字符之后的情况如上图。分支“knight”被排除。

书中作者称其问文本主导,是因为扫描每个字符的时候都对引擎进行了控制

 

 

测试引擎类型

1.如果支持忽略优先量词,那么基本就是传统NFA。DFA不支持忽略优先量词,POSIX NFA中也没有意义。

2.DFA不支持捕获型括号和回溯。在这两种混合类型的引擎中,如果没有使用捕获型括号,就会使用DFA

ps:在RegexBuddy中似乎只有传统NFA,起码做1的验证时结果是这样的,所以DFA和混合型引擎在这就不做验证了,本文也主要针对java,所以这里指着重介绍和java相关内容

 

 

 

两条普适原则(来自 《精通正则表达式》 v3)

1.优先匹配最左面(最靠开头)的匹配结果

注意:此原则并没有规定优先匹配结果的长度,而只是规定在所有可能的匹配结果中,优先选择最左边的(可能有)。

作者关于此原则的解释:匹配先从需要查找的字符串的起始位置尝试匹配。这里的“尝试匹配”的意思是:在当前位置测试整个正则表达式能匹配的每个可 能。如果在当前位置测试了所有的可能之后找不到匹配结果,就需要从字符的第二个字符之前的位置开始重新尝试……只有在尝试过所有的起始位置(直到字符串的 最后一个字符)都找不到匹配结果的情况下,才会报告失败。

下面给出一个例子:

目标字符串“This is a cat.”

我想匹配字符“is”,我的正则为“is”

结果如下(图1):

这里找到了两个结果,根据原则1,最先找到的是“this”中的“is”,而并没有找到“is”这个单词。这也很容易理解。下面我们看看RegexBuddy中debug的过程

这里怎么会有这么多字符,目标字符串实际只有13个字符,那么多出来的那些都是哪来的呢?

我觉得,RegexBuddy是把字符与字符之间的位置也算为一个character。再来看看图1

我之所以把每一个字符都装在表格里,就是让大家看的清楚。这里,每一个竖线(其实是不存在的)也作为一个character,我觉得这样是有道理的,比如零宽断言,它的匹配就是在某一个竖线的位置。我们不妨用“^”测试一下,看看debug的结果。


 

当正则以“字符串起始位置锚点”开头,引擎就会知道如果能匹配,那么肯定是从字符串开头,所以不需要做更多的尝试。

ps:这和RegexBuddy上面的debug结果似乎是矛盾的。确实是这样,不知道是不是其一个bug,起码v3.5.4是这样的

RegexBuddy暂时是不支持字符组的集合操作的,不知这算功能缺失还是算个bug

 

 

2.标准的量词(*,+,?,{m,n})是匹配优先的

目标字符串:“copyright 2003”

正则:“.*”,那么匹配的结果为全部字符

正则:“.*[0-9]*”,这个时候,由于量词是匹配优先的,所以“.*”会匹配整个字符串,而后面的“[0-9]*”怎什么也匹配不到 ,这并不影响最终结果,因为“*”表示0个也可以,我们可以添加一组括号来验证这个结果,如下图显示的一样

我们现在将正则改成这样:“.*[0-9]+”,这时候“.*”也会先把字符串全部匹配,之后是“[0-9]+”这个部分,发现它要求至少匹配到一 个数字才行,所以它会强迫“.*”吐出它之前匹配到的内容给自己使用,当“.*”吐出字符“3”之后,“[0-9]+”成功匹配,至此匹配结束,不再进行 其他尝试。

我们来debug看看这一过程:

书中作者总结为:先来先服务。我觉得,也就是说,多个匹配优先量词时,如果目标字符串“不能无法同时满足其需求”,那么写在前面的量词会得到尽量多的字符,后面的量词会像“类似”忽略优先量词一样进行匹配 - 给点就行。

 

回溯(backtracking)

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

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

两个要点:

1.如果需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词来说,引擎会优先选择“进行尝试”,对于忽略优先量词来说,会选择“跳过尝试”

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

 

实际上,NFA搜索的过程算法就是深度优先(关于深度优先介绍见文章末尾,内容来自中文维机百科),只不过并不一定完全遍历,完成匹配之后就停止搜索了。下面我举几个简单的例子,画图来描述一下。

例,假如我们要匹配一串数字中的最后两位,目标字符串“3456”,正则“\d+(\d\d)”,下面是一个流程示意图

匹配过程比较简单,首先\d+匹配3、4、5、6,其中绿色的圆圈是\d+的备用位置。

\d+继续尝试匹配,发现没有字符了,所以它的匹配结束,把控制权交给了\d,然而\d也无法匹配,所以需要进行回溯。

 

正则回到第二个绿色圆圈那里,然后控制权交给\d。现在\d可以匹配到数字6了,匹配结束,控制权交给\d,发现没有字符留给它,所以还需要回溯。

正则回到第一个绿色圆圈那里,然后控制权交给\d。现在\d可以匹配到数字5了,匹配结束,控制权交给\d,匹配到了数字6,匹配结束,至此整个表达式完成了匹配。

这里红色的圆圈表示交换控制权,这样方便理解。只有在绿色圆圈处才可能产生新的分支,其余地方,如果匹配失败,只需要原路返回到绿色圆圈处即可,然后尝试量词和多选结构的备用状态)

 

环视中的回溯

如果环视结构的匹配尝试结束,那么它就不会留下任何备用状态。如果匹配成功,它会放弃剩余的备用状态;如果匹配失败,则继续尝试匹配,直到所有备用状态用光,所以也不会留下备用状态。

环视中,是有可能放弃备用状态的,下面要介绍的固化分组和占有优先量词也会具有这样的性质。

 

 

下面有一条显而易见,但是又容易让大家忽略的事实。

无论是匹配优先还是忽略优先,只要引擎报告匹配失败,它就必然尝试了所有可能。

所以,如果有太多的回溯的可能,那么可能会使得你的程序阻塞,在android里面会产生ANR。之后会给出能阻塞程序的例子。

(对于传统NFA来说,选择结构是按顺序的,并不是匹配优先也不是忽略优先)

 

固化分组与占有优先量词

(?>...) :固化分组

“?+”、“*+”、“++”、“{m,n}+” :占有优先量词

 

固化分组

对于“(?>...)” 中 的内容部分(省略号省略的部分)来说,与之前将过的匹配规则一致,没有什么区别,但是,当此部分表达式匹配完毕,开始匹配括号外面的部分时,括号内的所有 备用状态都会被放弃,也就是说,如果之后的匹配失败,也不会回退固化分组之前记录的状态(因为出了固化分组后,它就忘了之前的状态了,这哥们记性不是很 好)。

 

固化分组和环视都有放弃备用状态的特点,我们可以考虑使用肯定环视来模拟固化分组。

对于“(?>regex)” ,我们希望匹配了regex之后就放弃其备用状态,我们知道“(?=regex)”匹配结束之后会放弃其备选状态,那么可以使用“(?=(?:regex))\1”,这样会比真正的固化分组慢一些,因为还要重新匹配“\1”。

 

下面给出一个简单的例子:目标字符串“abc”,正则“(?=\w+)\1”

首先\w+会匹配abc,匹配完成后放弃其所有备选状态,把控制权交给“\1”。“\1”再次重新匹配abc。

如果正则改为:“(?=\w+)\1c”

我想让\w+匹配到“ab”,这样“\1”就匹配到了“ab”,“c”对应“c”,匹配成功。但是,结果并不是这样的!

和上面的匹配过程一样:首先\w+会匹配abc,匹配完成后放弃其所有备选状态,把 控制权交给“\1”。“\1”再次重新匹配abc。然后把控制权交给“c”,发现匹配失败,没有备用状态,整体匹配就失败了。有的同学可能会想,如果我让 正则回溯到环视之前呢?其实也是一样的,当把控制权交给环视的时候,“\w+”依然直接匹配“abc”,后面大家都知道了,然后再次回溯……

所以当“c”无法匹配字符时,没有必要进行回溯,可以直接宣告匹配失败。

 

下面看看这个正则表达式:“(?>.*?)”

如果上面的内容理解了,那么这个正则也不难了,它永远也匹配不到任何字符。

 

 

占有优先量词

占有优先量词与匹配优先量词(贪婪匹配)很像,区别在于:占有优先量词不会交还字符,而匹配优先在需要的时候会交还字符。

下面给大家一个例子:

字符串:aaaaa

正则1:“\w+a”

正则2:“\w++a”

正则1:首先“\w+a” 的\w+部分会匹配所有字符,它会占有5个a,然后“\w+a” 对其中的a进行匹配,发现已经没有字符留给它了,这时候\w会交还之前占有的字符,每次交还一个。交还一个后,\w拥有“aaaa”,这时候“\w+a” 的a发现,它能匹配\w交还的字符,于是匹配成功,匹配结束。

 

正则2:同样,“\w++a” 的\w++部分会匹配所有字符,然后发现“\w++a” 的a部分无法匹配,但是\w++不会交还之前匹配到的字符,于是,匹配宣告失败!

 

 

区分固化分组与占有优先

作者告诉我们:请务必区分 下面两个表达式

表达式1:“(?>M)+”

表达式2:“(?>M+)”

表达式1放弃了M的备用状态,但是M并没有创造状态,所以这样做没有什么意义

表达式2放弃了M+的备用状态,这样显然有意义。

表达式3:“M++”

与表达式2一样,占有优先量词可以用固化分组来实现。

 

下面是一个稍微复杂点的占有优先表达式,如何将它转化为固化分组呢?

(\\"|[^"])*+

其实我觉得,如果理解了上面的文字,那么转化还是挺简单的,结果如下

(?>(\\"|[^"])*)

可作者觉得,可能会有很多人写成下面错误 的形式

(?>\\"|[^"])*

所以作者特意总结了一下:去掉表示占有优先的加号,用固化分组把余下的部分包括起来。

 

 

深度优先算法(Depth-First-Search)

类别: 搜索算法
数据结构:
时间复杂度:
空间复杂度:
最佳解:
完全性:
其他:

b-分支系数

m-图的最大深度

搜索算法的一种。是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节 点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所 有节点都被访问为止。属于盲目搜索。

posted on 2013-02-28 16:10  p-Flower  阅读(735)  评论(0编辑  收藏  举报

导航