正则表达式
基本用法
?:
、?=
、?<=
、?!
、?<!
的使用和区别
(?=pattern) 正向先行断言
// 给定字符串 "a regular expression", 要想匹配 regular 中的 re,但不能匹配 expression 中的 re,即 re 后面只能是 gular var result = /re(?=gular)/.test('a regular expression') // 将表达式改为 re(?=gular).,将会匹配 reg,因为元字符 . 匹配了 g,括号这一砣匹配了 e 和 g 之间的位置。
(?!pattern) 负向先行断言
// 给定字符串 "regex represents regular expression",要想匹配除 regex 和 regular 之外的 re,即 re 后面不能是 g var result = /re(?!g)/.test('regex represents regular expression')
(?<=pattern) 正向后行断言
// 给定字符串 "present recent", 想要匹配 recent 中的 ent, 即 ent 之前只能是 rec var result = /(?<=rec)ent/.test('present recent')
(?<!pattern) 负向后行断言
// 给定字符串 "present recent", 想要匹配 present 中的 ent, 即 ent 之前不能是 rec var result = /(?<!rec)ent/.test('present recent')
贪婪与非贪婪
我们先看一个简单的问题。
给定一个字符串表示的数字,判断该数字末尾 0 的个数。例如:
"123000"
:3 个 0"10100"
:2 个 0"1001"
:0 个 0
可以很容易地写出该正则表达式:(\d+)(0*)
,Java 代码如下:
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d+)(0*)");
Matcher matcher = pattern.matcher("1230000");
if (matcher.matches()) {
System.out.println("group1=" + matcher.group(1)); // "1230000"
System.out.println("group2=" + matcher.group(2)); // ""
}
}
}
然而打印的结果并不符合预期。
实际上,我们期望分组匹配结果是:
输入 | \d+ | 0* |
---|---|---|
123000 | "123" | "000" |
10100 | "101" | "00" |
1001 | "1001" | "" |
但实际的分组匹配结果是这样的:
输入 | \d+ | 0* |
---|---|---|
123000 | "123000" | "" |
10100 | "10100" | "" |
1001 | "1001" | "" |
仔细观察上述实际匹配结果,实际上它是完全合理的,因为 \d+
确实可以匹配后面任意个 0。
这是因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配。因此,\d+
总是会把后面的 0 包含进来。
要让 \d+
尽量少匹配,让 0*
尽量多匹配,我们就必须让 \d+
使用非贪婪匹配。在规则 \d+
后面加个 ?
即可表示非贪婪匹配。我们改写正则表达式如下:
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d+?)(0*)");
Matcher matcher = pattern.matcher("1230000");
if (matcher.matches()) {
System.out.println("group1=" + matcher.group(1)); // "123"
System.out.println("group2=" + matcher.group(2)); // "0000"
}
}
}
因此,给定一个匹配规则,加上 ?
后就变成了非贪婪匹配。
我们再来看这个正则表达式 (\d??)(9*)
,注意 \d?
表示匹配 0 个或 1 个数字,后面第二个 ?
表示非贪婪匹配。因此,给定字符串 "9999"
,匹配到的两个子串分别是 "" 和 "9999"。因为对于 \d?
来说,可以匹配 1 个 9,也可以匹配 0 个 9,但是因为后面的 ?
表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了 0 个 9。
小结
正则表达式匹配默认使用贪婪匹配,可以使用 ?
表示对某一规则进行非贪婪匹配。
注意区分 ?
的含义:\d??
。
接下来正式讲一下正则中的贪婪和非贪婪匹配。
贪婪模式和非贪婪模式是正则匹配中的重要特性,在理解贪婪和非贪婪的区别时,可以根据实例,一步一步的循序渐进。
匹配规则简介
var str='aabcab';
var reg=/ab/;
var res=str.match(reg);
// ab,index 为 1
console.log(res);
要快速理解正则的匹配规则,可以先尝试理解上述的例子。匹配步骤如下:
- 初始
index=0
,匹配到了字符a
- 接下来匹配下一个字符
a
,但是由于aa
和/ab/
不匹配,因此此次匹配失败 ,index
挪到下一个,从1
开始,又重新匹配了a
- 接下来匹配下一个字符
b
,刚好和/ab/
匹配,因此,此次匹配成功,返回了ab
,index=1
- 如果正则的匹配后面有
g
这种关键字,则会继续开始下一组的匹配(但是本例中没有g
,因此只有一组结果)
要点
- 最先开始的匹配拥有最高的优先权
这一要点的详细解释是: 例如第一个匹配的字符是 a
,假设之后的匹配没有出现匹配失败的情况。则它将一直匹配下去,直到匹配完成,也就是说 index=0
不会变,直到匹配完成(如果出现匹配失败并且无法回溯,index
才会继续往下挪)。
这一点适用于下面的贪婪模式与非贪婪模式中(并且优先级高于它们),因此请谨记。
贪婪模式与非贪婪模式
贪婪匹配模式
定义
正则表达式去匹配时,会尽量多的匹配符合条件的内容。
标识符
+
,?
,*
,{n}
,{n,}
,{n,m}
匹配时,如果遇到上述标识符,代表是贪婪匹配,会尽可能多的去匹配内容。
- 示例 1
var str='aacbacbc';
var reg=/a.*b/;
var res=str.match(reg);
// aacbacb, index 为 0
console.log(res);
上例中,匹配到第一个 a
后,开始匹配 .*
,由于是贪婪模式,它会一直往后匹配,直到最后一个满足条件的 b
为止,因此匹配结果是 aacbacb
。
- 示例 2
var str='aacbacbc';
var reg=/ac.*b/;
var res=str.match(reg);
// acbacb, index 为 1
console.log(res);
第一个匹配的是 a
,然后再匹配下一个字符 a
时,和正则不匹配,因此匹配失败,index
挪到 1
,接下来匹配成功了 ac
,继续往下匹配,由于是贪婪模式,尽可能多的去匹配结果,直到最后一个符合要求的 b
为止,因此匹配结果是 acbacb
。
非贪婪匹配模式
定义
正则表达式去匹配时,会尽量少的匹配符合条件的内容 也就是说,一旦发现匹配符合要求,立马就匹配成功,而不会继续匹配下去((除非有 g
,开启下一组匹配)。
标识符
+?
,??
,*?
,{n}?
,{n,}?
,{n,m}?
({n,m}?
等价于 {n}
)
可以看到,非贪婪模式的标识符很有规律,就是贪婪模式的标识符后面加上一个 ?
。
- 示例 1
var str='aacbacbc';
var reg=/a.*?b/;
var res=str.match(reg);
// aacb, index 为 0
console.log(res);
上例中,匹配到第一个 a
后,开始匹配 .*?
,由于是非贪婪模式,它在匹配到了第一个 b
后,就匹配成功了,因此匹配结果是 aacb
。
为什么是 aacb
而不是 acb
呢? 因为前面有提到过一个正在匹配的优先规则:最先开始的匹配拥有最高的优先权。 第一个 a
匹配到了,只要之后没有发生匹配失败的情况,它就会一直匹配下去,直到匹配成功。
- 示例 2
var str='aacbacbc';
var reg=/ac.*?b/;
var res=str.match(reg);
// acb, index 为 1
console.log(res);
先匹配的 a
,接下来匹配第二个 a
时,匹配失败了 index
变为 1
,继续匹配 ac
成功,继续匹配 b
,由于是非贪婪模式,此时 acb
已经满足了正则的最低要求了,因此匹配成功,结果为 acb
。
- 示例 3
var str='aacbacbc';
var reg=/a.*?/;
var res=str.match(reg);
// a, index 为 0
console.log(res);
var reg2=/a.*/;
var res2=str.match(reg2);
// aacbacbc, index 为 0
console.log(res2);
这一个例子则是对示例 1 的补充。可以发现,当后面没有 b
时,由于是非贪婪模式,匹配到第一个 a
就直接匹配成功了 而后面一个贪婪模式的匹配则是会匹配所有。
示例
在初步理解了贪婪模式与非贪婪模式后,通过以下示例加深理解。
1. 提取 HTML 中的 div 标签
给出一个 HTML 字符串,如下:
<div><span>用户:<span/><span>张三<span/></div>
<div><span>密码:<span/><span>123456<span/></div>
需求:提取出 div 包裹的内容(包括 div 标签本身)。
通过非贪婪模式的全局匹配来完成,如下:
var reg=/<div>.*?<\/div>/g;
var res=str.match(reg);
// ["<div><span>用户:<span/><span>张三<span/></div>", "<div><span>密码:<span/><span>123456<span/></div>"]
console.log(res);
用到了两个知识点,.*?
的非贪婪模式匹配以及 g
全局匹配:
<div>.*?<\/div>
代表每次只会匹配一次div
,这样可以确保每一个div
不会越界- 最后的
g
代表全局匹配,即第一次匹配成功后,会将匹配结果放入数组,然后从下一个index
重新开始匹配新的结果
另外,假设使用了 /<div>.*<\/div>/g
进行贪婪模式的匹配,结果则是:
["<div><span>用户:<span/><span>张三<span/></div><div><span>密码:<span/><span>123456<span/></div>"]
因为贪婪模式匹配了第一个 <div>
后会无限贪婪的匹配接下来的字符,直到最后一个符合条件的 </div>
为止,导致了将中间所有的 div
标签都一起匹配上了。
2. 提取两个 ""
中的子串,其中不能再包含 ""
示例引用自:正则表达式之 贪婪与非贪婪模式详解
给定如下字符串:
var str = '"The phrase \"regular expression\" is called \"Regex\" for short"'
需求:提取两个引号之间的子串,其中不能再包括引号,例如上述的提取结果应该是: "regular expression" 与 "Regex"(每一个结束的"后面都接空格)
错误解法:通过如下的非贪婪匹配(请注意空格)
var str='"The phrase \"regular expression\" is called \"Regex\" for short"';
var reg=/".*?" /g;
var res=str.match(reg);
// ['"The phrase "regular expression" ', '"Regex" ']
console.log(res);
可以看到,上述的匹配完全就是匹配错误了,这个正则匹配到第一个符合条件的 "+
后就自动停下来了。
正确解法:使用贪婪模式进行匹配
var reg=/"[^"]*" /g;
var res=str.match(reg);
// ['"regular expression" ', '"Regex" ']
console.log(res);
解释如下:
从第一个
"
开始匹配,接下来到 12 位时(子串"r
的"
),不满足[^"]
,也不满足之后的"
,因此匹配失败了,index
挪到下一个,开始下一次匹配第二个匹配从子串
"r
的"
开始,一直匹配到子串n"
的空格,这一组刚刚好匹配成功(因为最后符合了正则的"
),匹配好了"regular expression"
第三个匹配匹配到了
"Regex"
(过程不再复述)到最后时,仅剩一个
"
直接匹配失败(因为首先得符合"
才能开始匹配)至此,正则匹配结束,匹配成功,并且符合预期
这个例子相对来说复杂一点,如要更好地理解,可以参考引用来源中的文章,里面有就原理进行介绍。另外,参考文章中还有对非贪婪模式的匹配失败,回溯影响性能等特性进行原理分析与讲解。
3. 去掉 HTML 中的所有标签
用 Java 代码实现如下:
public static String filterHTMLCode(String html) {
// 这个正则匹配非中文字符
// 不能过滤 <哈哈哈>123</哈哈哈> 这样的中文标签
return html.replaceAll("<{1}[^\u4e00-\u9fa5]{1,}?>{1}", "");
}
public static String filterAllHTMLCode(String html) {
return html.replaceAll("<{1}[^>]{1,}>{1}", "");
}
回溯现象与匹配失败
你真的已经理解了贪婪模式和非贪婪模式么?
回溯现象
不知道对上面最后例子中提到的回溯这词有没有概念?
原字符串:"Regex"
贪婪匹配分析
匹配正则:
".*"
第一个
"
取得控制权,匹配正则中的"
,匹配成功,控制权交给.*
.*
取得控制权后,匹配接下来的字符,.
代表匹配任何字符,*
代表可匹配可不匹配,这属于贪婪模式的标识符,会优先尝试匹配,于是接下来从 1 位置处的R
开始匹配,依次成功匹配了R
,e
,g
,e
,x
,接着继续匹配最后一个字符"
,匹配成功,这时候已经匹配到了字符串的结尾,所以.*
匹配结束,将控制符交给正则式中最后的"
"
取得控制权后,由于已经是到了字符串的结尾,因此匹配失败,向前查找可供回溯的状态,控制权交给.*
,.*
让出一个字符"
,再把控制权交给"
,此时刚好匹配成功至此,整个正则表达式匹配完毕,匹配结果为
"Regex"
,匹配过程中回溯了1
次
非贪婪匹配分析
匹配正则:
".*?"
第一个
"
取得控制权,匹配正则中的"
,匹配成功,控制权交给.*?
.*?
取得控制权后,由于这是非贪婪模式下的标识符,因此在可匹配可不匹配的情况下会优先不匹配,因此尝试不匹配任何内容,将控制权交给"
,此时index
在1
处(R
字符处)"
取得控制权后,开始匹配1
处的R
,匹配失败,向前查找可供回溯的状态,控制权交给.*?
,.*?
吃进一个字符,index
到了2
处,再把控制权交给"
"
取得控制权后,开始匹配2
处的e
,匹配失败,重复上述的回溯过程,直到.*?
吃进了x
字符,再将控制权交给"
"
取得控制权后,开始匹配6
处的"
,匹配成功至此,整个正则表达式匹配完毕,匹配结果为
"Regex"
,匹配过程中回溯了5
次
优化去除回溯
上述的贪婪匹配中,出现了一次回溯现象,其实也可以通过优化表达式来防止回溯,比如使用正则表达式 "[^"]*"
。
这个表达式中构建了一个子表达式 []
中的 ^"
,它的作用是排除 "
匹配,这样 *
的贪婪匹配就不会主动吃进 "
,这样最后就直接是 "
匹配 "
,匹配成功,不会进行回溯。
小结
上述的分析中可以看出,在匹配成功的情况下,贪婪模式进行了更少的回溯(可以自行通过更多的实验进行验证),因此在应用中,在对正则掌握不是很精通的情况下,可以优先考虑贪婪模式的匹配,这样可以避免很多性能上的问题。
匹配失败的情况
上述的回溯分析都是基于匹配成功的情况,那如果是匹配失败呢?
var str = '"Regex'
var reg = /"[^"]*"/g;
这个原字符中,没有最好的 "
,因此匹配是会失败的,它的过程大致如下:
"
匹配"
,接着[]
的^"
与*
匹配R
,e
,g
,e
,x
接着到了最后,
"
获取控制权,由于到了最后,开始回溯依次回溯的结果是
*
让出x
,e
,g
,e
,R
,直到*
已经无法再让出字符,第一轮匹配失败接着
index
开始往下挪,依次用"
匹配R
,e
,g
,e
,x
都失败了,一直到最后也没有再匹配到结果,因此此次正则表达式的匹配失败,没有匹配到结果(或者返回null
)
那非贪婪模式呢?
var str = '"Regex'
var reg = /"[^"]*?"/g;
"
匹配"
,接着*
尝试不匹配,"
匹配R
,失败,然后回溯,*
吃进R
接下来类似于上一步,
*
依次回溯吃进e
,g
,e
,x
,一直到最后,*
再次回溯想吃进时,已经到了字符串结尾了,无法继续,因此第一轮匹配失败接着
index
开始往下挪,依次用"
匹配R
,e
,g
,e
,x
都失败了,返回null
小结
通过匹配失败的例子可以看出贪婪和非贪婪的模式区别。贪婪是先吃进,回溯再让出;非贪婪是先忽略,回溯再吃进。
而且,在匹配失败的情况下,贪婪模式也会进行不少的回溯(非贪婪当然一直都很多回溯)
但是,实际情况中是可以通过子表达式优化的,比如构建 ^xxx
,可以当匹配到不符合条件的时候提前匹配失败,这样就会少很多回溯。
var str = '"cccccc'
var reg = /"[^"c]*"/g;
这个由于直接排除了 c
,因此 *
不会吃进它,直接就匹配失败了,减少了很多回溯(当然,上述只是最简单的例子,实际情况要更复杂)。
正则匹配中,贪婪模式与非贪婪模式乍看之下一看便知,很容易理解,但是真正的深入理解需要掌握正则的原理才行,并且,真正理解它们后,就不仅仅只是写出普通的正则表达式,而是高性能的正则表达式了,比如理解非贪婪模式中的回溯特性后更容易写出高性能的表达式。
本笔记也只是做一些浅显的分析与引导,更多是起到抛砖引玉的作用,要深入理解还请去了解正则的原理。
搜索与替换
参考