正则表达式
最近几天再刷leetcode,遇到有些题觉得用正则就会把问题简化很多。于是想着把正则总结一下吧。
一、何为正则表达式
正则表达式**,又称规则表达式**。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。
通过百度搜索一翻,可以了解到如上书面性的解释。记得我刚开始接触正则表达式的时候,那时候看到这句话,也不能理解。说的太官方了。当然了,纸上得来终觉浅。一切都需要我们去一步步学习。正则是个很强的工具,学会了他,你可以用正则去分析别人的网页去爬取你想要的资源。当然,现在爬虫的库已经很多了,我们几乎不用正则表达式,也可以爬取到相应的资源。可是,我们还是不能停止对正则的研究。因为有一天你也想要学会如何造轮子,或者你自己写的小项目对于字符串的需要分析的时候,你自然而然就会想到正则表达式。
二、正则表达式的作用
-
正则表达式(regular expression)就是用一个“字符串”来描述一个特征,然后去验证另一个“字符串”是否符合这个特征。
比如 表达式“ab+” 描述的特征是“一个 ‘a’ 和 任意个 ‘b’ ”,那么 ‘ab’, ‘abb’, ‘abbbbbbbbbb’ 都符合这个特征。
注意:这里的 + 号代表 前面的字符出现零次或者更多次。所以ab后面不管出现多少个b都会匹配到该字符串。这就是正则表达式的魅力。
一般来说,正则表达式可以为我们提供如下:
(1)验证字符串是否符合指定特征,比如验证是否是合法的邮件地址。
(2)用来查找字符串,从一个长的文本中查找符合指定特征的字符串,比查找固定字符串更加灵活方便。(3)用来替换,比普通的替换更强大。
这里举个例子,如果让你判断一个字符串是否满足QQ邮箱的条件?
如果你没有学过正则,你可能会写类似如下的代码…:(只提供java语言实现)
/*
*
* @param email要验证的QQ邮箱
* @author raven
*/
private boolean checkEmail(String email){
boolean res = true;
if(email == null || email.equals("")|| !email.contains("@"))
res = false;
return res ;
String array[] = email.splint("@");
// 如果不是qq.com 为后缀
if( !array[1].containsIgnoreCase("qq.com")){
res = false;
return res;
}
// 获取邮箱前缀。我们都知道该前缀一般为 数字。为了简化问题,此处不考虑 其他字符形式。因为QQ邮箱前缀一般都是QQ。
String num = array[0];
for( int i = 0; i < num.length(); i ++ ){
//没有0开头的QQ
if( i == 0 && num.charAt(i) == '0' ){
res = false;
}
char current = num.charAt(i);
// 如果当前字符的ascii码 不是数字
if( current < 0x30 || current > 0x39 ){
res = false;
}
}
return res;
}
emm,其实代码写出来也挺漂亮的。写的蛮舒服的。可是,这就是判断个邮箱类型啊,就写了这么多代码,要知道,现在的业务,字段属性都很多的,如果每个都要这么校验,那还不累死你。你这时候就会想。哎,我要是会正则就好了啊。那你会说我:你老是吹正则怎么怎么样,那你给我用正则写个呗,我看看。
行,那我就写个给你看看。
java版:
private boolean checkEmail(String email){
Pattern.matches("^[1-9]\\d{4,10}@(qq|QQ)\\.com$", email)
}
javaScript版:
/*
*
* @param str 要验证的QQ邮箱
*/
function checkEmail(str){
var parse = /^[1-9]\d{4,10}@(qq|QQ)\.com$/;
return parse.test(str)
}
emm,看看就是 这么简单。一句代码完成了该功能。什么,你说你看不懂?那我给你讲讲就好了。
首先呢,^ 代表 字符串的匹配起始符。也就是说从字符串的第一个字符开始匹配。其次[1-9]
代表第一个次数是0-9的任意一个数字。\d{4,10}
又是什么呢?这个就是匹配第二个以后的字符 \d代表数字[0-9]
,后面的{4,10}
代表数字可能出现4-10次。为什么是4-10次呢?因为第一位我们前面刚才写了[1-9]已经占了一个长度了,再加后面的数字循环区间[4,10],故匹配的数字长度就是 [5-11]了,符合QQ的长度。后面的@照写。紧跟着(qq|QQ)
相当于判断。这里要么是大写的,要么是小写的。最后直接写个.com
结束正则表达式的简写。再加最有一个$
代表着匹配结束符。
所以呢,就是 这么简单。正则是不是真香,再也不用写那么多代码了。就算你那样写出来效率也比正则低。所以 ,正则真的是个法宝。但是正则有一个缺点,就是不容易理解。哈哈哈,其实用习惯就会发现好多了。
这里分享几个正则的元字符,大家记着记好了:
1.点.:匹配除换行符以外的任意字符
2.[]:是字符集合,表示匹配方括号中所包含的任意一个字符
[0123456789] 表示匹配任意一个数字
[0-9] 表示匹配任意一个数字
[yml] 表示匹配'y','m','l'中任意一个字符
[a-z] 表示匹配任意一个小写字符
[A-Z] 表示匹配任意一个大写字符
[0-9a-zA-Z] 表示匹配任意一个数字和字母
[0-9a-zA-Z_] 表示匹配任意一个数字、字母和下划线
3.^:是脱字符,表示不匹配集合中的字符,注意^在[]中才表示不匹配,在其他位置意义不一样
[^test] 表示匹配除了test这几个字母以外的所有字符
[^0-9] 表示匹配所有的非数字字符
4.\d:匹配数字,效果同[0-9]
\d 表示匹配任意一个数字,这里可以不写[],直接使用\d
[^\d] 表示匹配所有的非数字字符,效果同[^0-9]
5.\D:匹配非数字字符,效果同[^0-9]
6.\w:匹配数字、字母和下划线,效果同[0-9a-zA-Z]
7.\W:匹配非数字、字母和下划线,效果同[^0-9a-zA-Z]
8.\s:匹配任意的空白符(空格、换行、回车、换页、制表符),效果同[ \f\n\r\t]
9.\S:匹配任意的非空白符(空格、换行、回车、换页、制表符),效果同[^ \f\n\r\t]
10. \b 匹配字母或数字边界 [\b] 匹配转义字符\b本身,匹配退格键 \u0008
11. \B 匹配非字母和数字边界
12. ^ 行首匹配,注意^在[]中才表示不匹配集合中的字符
13. $ 行尾匹配
14. \A 从字符串开始处匹配
1. (xyz) 匹配小括号内的内容,()内的内容xyz作为一个整体去匹配
2. x? 匹配0个或者1个x,非贪婪匹配(尽可能少的匹配)
3. x* 匹配0个或者任意多个x,贪婪匹配(尽可能多的匹配)
.* 表示匹配0个或任意多个字符(换行符除外)
4. x+ 匹配至少一个x,贪婪匹配(尽可能多的匹配)
5. x{n} 匹配确定的n个x,n是一个非负整数
6. x{n,} 匹配至少n个x,n是一个非负整数
7. x{n,m} 匹配至少n个最多m个x,注意:n <= m
8. x|y |表示或,匹配的是x或y
三、捕获组 与 非 捕获组
首先,要弄清楚什么是捕获组什么是非捕获组。
捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用。()
可以声明一个子表达式为捕获组。
但是有时候,因为特殊原因用到了()
,但又没有引用它的必要,这时候就可以用非捕获组声明,防止它作为捕获组,降低内存浪费。?:
可以声明一个子表达式为非捕获组。
以下是 捕获组在java 中的使用:
public static final String data = "2017-04-25";
public static final String ngex = "(\\d{4})-((\\d{2})-(\\d{2}))";
Pattern pattern = Pattern.compile(ngex);
Matcher matcher = pattern.matcher(data);
matcher.find();
//输出===>2017-04-25
System.out.printf("matcher.group(0) value:%s", matcher.group(0));
//输出===>2017
System.out.printf("matcher.group(1) value:%s", matcher.group(1));
//输出===>04-25
System.out.printf("matcher.group(2) value:%s", matcher.group(2));
//输出===>04
System.out.printf("matcher.group(3) value:%s", matcher.group(3));
//输出===>25
System.out.printf("matcher.group(4) value:%s", matcher.group(4));
3.1、捕获组命名:
(?<捕获组名称>\\Regex命令)
捕获组命令,这样获取就更方便了,不用拿下标获取捕获组的值。
public static final String P_NAMED = "(?<year>\\d{4})-(?<md>(?<month>\\d{2})-(?<date>\\d{2}))";
public static final String DATE_STRING = "2017-04-25";
Pattern pattern = Pattern.compile(P_NAMED);
Matcher matcher = pattern.matcher(DATE_STRING);
matcher.find();
System.out.printf("===========使用名称获取=============");
// 输出===>2017-04-25
System.out.printf("matcher.group(0) value:%s", matcher.group(0));
// 输出===>2017
System.out.printf(" matcher.group('year') value:%s", matcher.group("year"));
// 输出===>04-25
System.out.printf("matcher.group('md') value:%s", matcher.group("md"));
// 输出===> 04
System.out.printf("matcher.group('month') value:%s", matcher.group("month"));
// 输出===> 25
System.out.printf("matcher.group('date') value:%s", matcher.group("date"));
matcher.reset();
// 下面输出的值跟上面的一样 就不注释了。
System.out.printfln("===========使用编号获取=============");
matcher.find();
System.out.printfln("matcher.group(0) value:%s", matcher.group(0));
System.out.printfln("matcher.group(1) value:%s", matcher.group(1));
System.out.printfln("matcher.group(2) value:%s", matcher.group(2));
System.out.printfln("matcher.group(3) value:%s", matcher.group(3));
System.out.printfln("matcher.group(4) value:%s", matcher.group(4));
3.2 、非捕获组:
public static final String P_UNCAP = "(?:\\d{4})-((\\d{2})-(\\d{2}))";
public static final String DATE_STRING = "2017-04-25";
Pattern pattern = Pattern.compile(P_UNCAP);
Matcher matcher = pattern.matcher(DATE_STRING);
matcher.find();
// 输出 ==> 2017-04-25
System.out.printfln("matcher.group(0) value:%s", matcher.group(0));
// 输出 ==> 04-25
System.out.printfln("matcher.group(1) value:%s", matcher.group(1));
// 输出 ==> 04
System.out.printfln("matcher.group(2) value:%s", matcher.group(2));
// 输出 ==> 25
System.out.printfln("matcher.group(3) value:%s", matcher.group(3));
四、反向引用
所谓的反向引用,即就是 当后面 要用到的 的 正则表达式元字符组合 跟 前面的一样,那么可以重复利用。只要把需要 引用的 元字符 组成 捕获组,即可引用。
以下是 反向引用在javaScript 中的使用:
var str = 'ooo<div>hello world!</div>234';
// 正则表达式里的 \1 代表引用 正则表达式的第一个捕获组
var reg = /<(\w+).*<\/\1>/g;
var result = str.match(reg);
// 输出==> <div>hello world!</div>
console.log(result[0]);
以下是 反向引用在java 中的使用:
String content="<tr>hello world!</tr>";
String regextStr="<(\\w+)>.*?</\\1>";
Pattern pattern=Pattern.compile(regextStr, Pattern.CASE_INSENSITIVE);
Matcher matcher =pattern.matcher(content);
if(matcher.find()){
// 输出==> <tr>hello world!</tr>
System.out.println(matcher.group(0));
// 输出==> <tr>
System.out.println(matcher.group(1));
}else{
System.out.println("no found");
}
五、环视
环视强调位置(前面或后面),必须匹配环视表达式,才能匹配成功。
环视可认为是虚拟加入到它所在位置的附加判断条件,并不消耗正则的匹配字符。
类型 | 正则表达式 | 匹配成功条件 |
---|---|---|
肯定逆序环视 | (?<=expresion) | 匹配子表达式右侧的位置 |
否定逆序环视 | (?<!expresion) | 匹配非子表达式右侧的位置 |
肯定顺序环视 | (?=expresion) | 匹配子表达式左侧的位置 |
否定顺序环视 | (?!expresion) | 匹配非子表达式左侧的位置 |
5.1 肯定逆序环视
(?<=a)
代表 找到字符a右边的引索位置
String str = "f123a123";
String pattern = Pattern.compile("(?<=a)");
String matcher = pattern.matcher(str);
//输出 ==> f123a,123
System.out.println(matcher.replaceAll(","));
5.2 否定逆序环视
(?<=a)
代表 不是字符a右边的引索位置,所以 只要不是a字符,那么该字符的右边必然有个逗号
String str = "f1a2c3a4";
String pattern = Pattern.compile("(?<!a)");
String matcher = pattern.matcher(str);
//输出 ==> ,f,1,a2,c,3,a4,
System.out.println(matcher.replaceAll(","));
5.3 肯定顺序环视
(?=a)
代表 a字符左边位置的索引位置。所以每个a字符左边都有个 逗号。
String str = "f1a2c3a4";
String pattern = Pattern.compile("(?=a)");
String matcher = pattern.matcher(str);
//输出 ==> f1,a2c3,a4
System.out.println(matcher.replaceAll(","));
5.4 否定顺序环视
(?=a)
代表 非 a字符左边位置的索引位置。所以每个非a字符左边都有个 逗号。
String str = "f1a2c3a4";
String pattern = Pattern.compile("(?!a)");
String matcher = pattern.matcher(str);
//输出 ==> ,f,1a,2,c,3a,4,
System.out.println(matcher.replaceAll(","));
六、总结
可以看出来,正则表达式的内容也不是很多。但是我们熟悉掌握了它。对我们的帮助很大呢。对于 捕获组和非捕获组这一块的内容,也不是很难理解。把上面的例子搞懂了就很好理解了。by 2020.2.3