正则表达式在几乎所有语言中都可以使用,无论是前后端他们都提供相应的接口/函数支持正则表达式。
1. 简单应用
正则 | 描述 |
---|---|
. | 代表任意字符 |
[abc] | a或者b或者c中的一个字符 |
[^abc] | abc以外的一个字符 |
[a-zA-Z] | a-z或者A-Z范围内的字符 |
[abc[def]] | abcdef 中的一个字符 |
[abc&&[adf]] | 他们之间的交集字符a |
\s | 一个空白符: space/tab/newline/ |
\S | 空白符之外的字符: 等同于[^\s] |
\d | 数字[0-9] |
\D | 非数字^0-9[] |
\w | 单词字符[a-zA-Z_0-9] |
\W | [^\w] |
2. 元字符
元字符 | 描述 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
- 匹配有abc开头的字符串:
\babc或者^abc- 匹配8位数字的QQ号码:
^\d\d\d\d\d\d\d\d$- 匹配1开头11位数字的手机号码:
^1\d\d\d\d\d\d\d\d\d\d$
重复限定符
有了元字符就可以写不少的正则表达式了,但细心的你们可能会发现:别人写的正则简洁明了,而不理君写的正则一堆乱七八糟而且重复的元字符组成的。正则没提供办法处理这些重复的元字符吗?
答案是有的!
为了处理这些重复问题,正则表达式中一些重复限定符,把重复部分用合适的限定符替代,下面我们来看一些限定符:
语法 | 描述 |
---|---|
* | 重复0次或更多次 |
+ | 重复1次或者更多次 |
? | 重复0次或者1次 |
{n} | 重复n次 |
{n,} | 重复n次或者更多次 |
{n,m} | 重复n到m次 |
匹配8位数字的QQ号码:
^\d{8}$
^1\d{10}$
匹配1开头11位数字的手机号码:
^\d{14,18}$
匹配银行卡号是14~18位的数字:
1^ab*$
匹配以a开头的,0个或多个b结尾的字符串, *限定符是作用在与他左边最近的一个字符
分组
正则表达式中用小括号()来做分组,也就是括号中的内容作为一个整体。
当我们要匹配多个ab时,我们可以这样
^(ab)*
转义
如果要匹配的字符串中本身就包含小括号,那是不是冲突?应该怎么办?
正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,
做法很简答,就是在要转义的字符前面加个斜杠,也就是\即可。
匹配以(ab)开头: ^(\(ab\))*
条件或
正则用符号 | 来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成功。
^(130|131|132|155|156|185|186|145|176)\d{8}$
区间
正则提供一个元字符中括号 [] 来表示区间条件。
限定0到9 可以写成[0-9]
限定A-Z 写成[A-Z]
限定某些数字 [165]
eg : ^((13[0-2])|(15[56])|(18[5-6])|145|176)\d{8}$
零宽断言
断言:俗话的断言就是“我断定什么什么”,而正则中的断言,就是说正则可以指明在指定的内容的前面或后面会出现满足指定规则的内容,
意思正则也可以像人类那样断定什么什么,比如"ss1aa2bb3",正则可以用断言找出aa2前面有bb3,也可以找出aa2后面有ss1.
零宽:就是没有宽度,在正则中,断言只是匹配位置,不占字符,也就是说,匹配结果里是不会返回断言本身。
正向先行断言(正前瞻):
语法:(?=pattern)
作用:匹配pattern表达式的前面内容,不返回本身。
eg:
String reg=".+(?=</span>)";
String test = "<span class=\"read-count\">阅读数:11</span>";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
while(mc.find()){
System.out.println("匹配结果:" + mc.group())
System.out.println(mc.group());
}
//
//匹配结果: <span class="read-count">阅读数:11
如果我们只是需要</span> 前面的数字: 匹配数字 \d,那可以改成
String reg="\\d+(?=</span>)";
//匹配结果:11
正向后行断言(正后顾):
语法:(?<=pattern)
作用:匹配pattern表达式的后面的内容,不返回本身。
eg:
String reg="(?<=<span class=\"read-count\">阅读数:)\\d+";
String test = "<span class=\"read-count\">阅读数:11</span>";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
while(mc.find()){
System.out.println("匹配结果:" + mc.group())
System.out.println(mc.group());
}
//匹配结果: 11
负向先行断言(负前瞻)
语法:(?!pattern)
作用:匹配非pattern表达式的前面内容,不返回本身。
负向后行断言(负后顾)
语法:(?<!pattern)
作用:匹配非pattern表达式的后面内容,不返回本身。
捕获和非捕获
单纯说到捕获,他的意思是匹配表达式,但捕获通常和分组联系在一起,也就是“捕获组”
捕获组:匹配子表达式的内容,把匹配结果保存到内存中中数字编号或显示命名的组里,以深度优先进行编号,之后可以通过序号或名称来使用这些匹配结果。
数字编号捕获组:
语法:(exp)
解释:从表达式左侧开始,每出现一个左括号和它对应的右括号之间的内容为一个分组,在分组中,第0组为整个表达式,第一组开始为分组。
eg:
固定电话的:030-92363333
他的正则表达式为:(0\d{2})-(\d{8})
按照左括号的顺序,这个表达式有如下分组:
序号 编号 分组 内容
0 0 (0\d{2})-(\d{8}) 030-92363333
1 1 (0\d{2}) 030
2 2 (\d{8}) 92363333
//java
String test = "030-92363333";
String reg="(0\\d{2})-(\\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
if(mc.find()){
System.out.println("分组的个数有:"+mc.groupCount());
for(int i=0;i<=mc.groupCount();i++){
System.out.println("第"+i+"个分组为:"+mc.group(i));
}
}
// result
分组的个数有:2
第0个分组为:030-92363333
第1个分组为:030
第2个分组为:92363333
命名编号捕获组:
语法:(?<name>exp)
解释:分组的命名由表达式中的name指定
比如区号也可以这样写:(?<quhao>0\d{2})-(?<phone>\d{8})
按照左括号的顺序,这个表达式有如下分组:
序号 名称 分组 内容
0 0 (0\d{2})-(\d{8}) 030-92363333
1 quhao (0\d{2}) 030
2 phone (\d{8}) 92363333
//java
String test = "030-92363333";
String reg="(?<quhao>0\\d{2})-(?<phone>\\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
if(mc.find()){
System.out.println("分组的个数有:"+mc.groupCount());
for(int i=0;i<=mc.groupCount();i++){
System.out.println("第"+i+"个分组为:"+mc.group(i));
}
}
//result
分组的个数有:2
分组名称为:quhao,匹配内容为:030
分组名称为:phone,匹配内容为:92363333
非捕获组:
语法:(?:exp)
解释:和捕获组刚好相反,它用来标识那些不需要捕获的分组,说的通俗一点,就是你可以根据需要去保存你的分组。
eg:
(?:\0\d{2})-(\d{8})
序号 编号 分组 内容
0 0 (0\d{2})-(\d{8}) 030-92363333
1 1 (\d{8}) 92363333
// java
String test = "020-85653333";
String reg="(?:0\\d{2})-(\\d{8})";
Pattern pattern = Pattern.compile(reg);
Matcher mc= pattern.matcher(test);
if(mc.find()){
System.out.println("分组的个数有:"+mc.groupCount());
for(int i=0;i<=mc.groupCount();i++){
System.out.println("第"+i+"个分组为:"+mc.group(i));
}
}
// result
分组的个数有:1
第0个分组为:030-92363333
第1个分组为:92363333
反向引用
捕获会返回一个捕获组,这个分组是保存在内存中,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。
根据捕获组的命名规则,反向引用可分为:
数字编号组反向引用:\k 或 \number
命名编号组反向引用:\k 或者 ‘name’
捕获组是匹配子表达式的内容按序号或者命名保存起来以便使用
注意两个字眼:“内容” 和 “使用”; “内容”,是匹配结果,而不是子表达式本身
还是举栗子吧:
比如要查找一串字母"aabbbbfghbddesddfiid"里成对的字母
如果按照我们之前学到的正则,什么区间啊限定啊断言啊可能是办不到的,
现在我们先用程序思维理一下思路:
1)匹配到一个字母
2)匹配第下一个字母,检查是否和上一个字母是否一样
3)如果一样,则匹配成功,否则失败
这里的思路2中匹配下一个字母时,需要用到上一个字母,那怎么记住上一个字母呢???
这下子捕获就有用处啦,我们可以利用捕获把上一个匹配成功的内容用来作为本次匹配的条件
好了,有思路就要实践
首先匹配一个字母:\w
我们需要做成分组才能捕获,因此写成这样:(\w)
那这个表达式就有一个捕获组:(\w)
然后我们要用这个捕获组作为条件,那就可以:(\w)\1
这样就大功告成了
可能有人不明白了,\1是什么意思呢?
还记得捕获组有两种命名方式吗,一种是是根据捕获分组顺序命名,一种是自定义命名来作为捕获组的命名
在默认情况下都是以数字来命名,而且数字命名的顺序是从1开始的
因此要引用第一个捕获组,根据反向引用的数字命名规则 就需要 \k<1>或者\1
当然,通常都是是后者。
// java
String test = "aabbbbfghbddesddfiid";
Pattern pattern = Pattern.compile("(\\w)\\1");
Matcher mc= pattern.matcher(test);
while(mc.find()){
System.out.println(mc.group());
}
// result
aa
bb
bb
dd
dd
ii
2. 假如想要把字符串中abc换成a
String test = "abcbbabcbcgbddesddfiid";
String reg="(a)(b)c";
System.out.println(test.replaceAll(reg, "$1"));
// result
abbabcgbddesddfiid
贪婪和非贪婪
贪婪
贪婪匹配:当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符,这匹配方式叫做贪婪匹配。
特性:一次性读入整个字符串进行匹配,每当不匹配就舍弃最右边一个字符,继续匹配,依次匹配和舍弃(这种匹配-舍弃的方式也叫做回溯),直到匹配成功或者把整个字符串舍弃完为止,因此它是一种最大化的数据返回,能多不会少。
重复限定符,其实这些限定符就是贪婪量词,比如表达式
\d{4,6}
它是一种贪婪模式的匹配,也就是假如字符串里有6个个数字可以匹配,那它就是全部匹配到
eg:
String reg="\\d{3,6}";
String test="61762828 176 2991 871";
System.out.println("文本:"+test);
System.out.println("贪婪模式:"+reg);
Pattern p1 =Pattern.compile(reg);
Matcher m1 = p1.matcher(test);
while(m1.find()){
System.out.println("匹配结果:"+m1.group(0));
}
// result
文本:61762828 176 2991 44 871
贪婪模式:\d{3,6}
匹配结果:617628
匹配结果:176
匹配结果:2991
匹配结果:871
多个贪婪在一起时,如果字符串能满足他们各自最大程度的匹配时,就互不干扰,但如果不能满足时,会根据深度优先原则,也就是从左到右的每一个贪婪量词,优先最大数量的满足,剩余再分配下一个量词匹配。
eg:
String reg="(\\d{1,2})(\\d{3,4})";
String test="61762828 176 2991 87321";
System.out.println("文本:"+test);
System.out.println("贪婪模式:"+reg);
Pattern p1 =Pattern.compile(reg);
Matcher m1 = p1.matcher(test);
while(m1.find()){
System.out.println("匹配结果:"+m1.group(0));
}
// result
文本:61762828 176 2991 87321
贪婪模式:(\d{1,2})(\d{3,4})
匹配结果:617628
匹配结果:2991
匹配结果:87321
懒惰(非贪婪)
懒惰匹配:当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能少的字符,这匹配方式叫做懒惰匹配。
特性:从左到右,从字符串的最左边开始匹配,每次试图不读入字符匹配,匹配成功,则完成匹配,否则读入一个字符再匹配,依此循环(读入字符、匹配)直到匹配成功或者把字符串的字符匹配完为止。
懒惰量词是在贪婪量词后面加个“?”
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复
eg:
// java
String reg="(\\d{1,2}?)(\\d{3,4})";
String test="61762828 176 2991 87321";
System.out.println("文本:"+test);
System.out.println("贪婪模式:"+reg);
Pattern p1 =Pattern.compile(reg);
Matcher m1 = p1.matcher(test);
while(m1.find()){
System.out.println("匹配结果:"+m1.group(0));
}
// result
文本:61762828 176 2991 87321
贪婪模式:(\d{1,2}?)(\d{3,4})
匹配结果:61762
匹配结果:2991
匹配结果:87321
反义
元字符 解释
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符
正则参考原文: https://mp.weixin.qq.com/s/717TT4T1sqHcOXyf-P7s4g