阅读此文须有正则的基础知识,若没有请先了解正则表达式。正则表达式用于快速地匹配文本。 写正则时先局部匹配,然后再当作一个整体。
正则表达式需要转义的字符
正则中用到的字符,不加转义正则会解析成特殊含义。需要转义的即在正则中代表特殊含义的字符。如下:
^、$、.、*、+、?、|、\、(、)、[、]、{、}、=、!、:、-
若要匹配以上字符在java中必须用\进行转义,否则代表特殊含义。比如String中的split方法,以点分隔。split("\\.")
正则的横向模糊匹配与纵向模糊匹配
横向模糊匹配:一个正则表达式匹配的字符串长度是不固定的,比如?,*,+,{m,n}等。
例如:ab{2,4}c。匹配的字符有(注意红色字体,很好的体现了横向这个词):
abbbc
abbbbc
abbbbbc
纵向模糊匹配:一个正则匹配的字符串,在匹配的位不确定位的值,比如\d,[a-z],\w等。
例如:a[123]b。匹配的有(注意红色字体,很好的体现了纵向这个词):
a1c
a2c
a3c
正则中的分组
- 正则中的分组是以()来计算的,可以正向引用及反向引用分组。使正则的功能更加强大。若不引用分组在括号中的前面加?:。
如(?:)即代表不引用分组。 - java中使用Matcher对象的group方法获取分组的值。
- group()即是group(0)。只要字符串匹配正则就是group(0)。
比如字符串为:abbcd。正则为ab{2,4}c,则group()为abbc。
abbbcd 。正则为ab{2,4}c,则group()为abbbc。
group(int m)m值为分组。
比如正则表达式:(ab)(bc)d。有两个分组,分组1为ab,分组2为bc
如何计算分组?
给小括号分等级。先找到最外的小括号然后找它的孩子、孙子直至找完,然后找兄弟,兄弟的孩子、孙子直至找完。依次类推就能正确的计算分组,这是使用正向引用、反向引用分组的前提。分组是括号中匹配到的内容。
比如正则((\d)(\d(\d)))(a)。先找到一对小括号,((\d)(\d(\d)))为分组1,即三个数字。接着找它的孩子,即(\d)、(\d(\d))分别为分组2,分组3。然后找分组3的孩子(\d)为分组4。找完之后,然后找它的兄弟,即(\a)为分组5。
如:字符串为123a。正则为上述正则。
分组1为:123
分组2为:1
分组3为:23
分组4为:3
分组5为:a
正向引用分组
使用\数字进行引用分组。比如引用分组\1。即把数字给转义代表引用的分组。正向引用是在正则表达式中引用,而反向引用是在匹配后的结果中引用。
例如匹配日期支持以下三种形式:
2020-06-20
2020/06/20
2020.06.20
想下正则:按照先局部再整体的原则
匹配年份:(\d){4}
匹配格式符号:([\\-\\./])
匹配月份:(\d){2}
匹配天数:(\d){2}
整体的正则为:(\d){4}([-./])(\d){2}([-./])(\d){2}
本以为万事大吉,但是能匹配到2020-06.20等即没控制格式符号。导致不符合正则的要求。
使用分组正则表达式改为:(\d){4}([\\-\\./])(\d){2}(\2)(\d){2}
private static void regexReference() {
String date = "2020-06-20";
String date1 = "2020/06/20";
String date2 = "2020.06.20";
String date3 = "2020-06.20";
String regex = "(\\d){4}([\\-\\./])(\\d){2}([\\-\\./])(\\d){2}";
System.out.println(Pattern.compile(regex).matcher(date).find());
System.out.println(Pattern.compile(regex).matcher(date1).find());
System.out.println(Pattern.compile(regex).matcher(date2).find());
System.out.println(Pattern.compile(regex).matcher(date3).find());
//匹配格式符号是分组2,所以引用的是分组2
String regexReference = "(\\d){4}([\\-\\./])(\\d){2}(\\2)(\\d){2}";
System.out.println(Pattern.compile(regexReference).matcher(date).find());
System.out.println(Pattern.compile(regexReference).matcher(date1).find());
System.out.println(Pattern.compile(regexReference).matcher(date2).find());
System.out.println(Pattern.compile(regexReference).matcher(date3).find());
//关于不引用分组,加了(?:)所以引用分组改成了引用分组1
String regexReference1 = "(?:\\d){4}([\\-\\./])(\\d){2}(\\1)(\\d){2}";
System.out.println(Pattern.compile(regexReference).matcher(date).find());
System.out.println(Pattern.compile(regexReference).matcher(date1).find());
System.out.println(Pattern.compile(regexReference).matcher(date2).find());
System.out.println(Pattern.compile(regexReference).matcher(date3).find());
}
反向引用分组
使用$数字来反向引用分组,反向引用分组是对结果的引用。
比如sql模糊查询的时候,输入的字符串中有%_需要进行其转义。在mysql中也是使用%进行转义。只有模糊查询的时候需要转义。
String str = “%hel_”;
正则:[%_]
\就要用\\表示因为第二个反斜线在第二个参数中有特殊意义
str.replaceAll("([%])","\\\\$1")
输出为:**\%hel\**
注意:
分组后面有量词,所匹配的分组是最后一个满足的字符。
比如字符串:ab90。正则([ab90])。那么匹配的分组为0。
若想引用所用的加括号,正则改为(([ab90]))。
正则中的贪婪匹配与惰性匹配
所谓贪婪量词即尽可能匹配多的字符满足正则表达式,所谓惰性量词即尽可能匹配少的字符满足正则表达式。
贪婪匹配的量词有:?,*,+,{m,n}
惰性匹配:在贪婪量词后加?即为惰性匹配。
惰性匹配的量词有:??,*?,+?,{m,n}?,还有分支结构**|**
private static void greedAndInertia(){
String str = "abcccde";
String greedRegex = "abc+";
Pattern greedPattern = Pattern.compile(greedRegex);
Matcher greedMatcher = greedPattern.matcher(str);
while (greedMatcher.find()) {
System.out.println("贪婪匹配到的字符串为:");
System.out.println(greedMatcher.group());//输出结果为:abccc
}
//惰性匹配
String inertiaRegex = "abc+?";
Pattern inertiaPattern = Pattern.compile(inertiaRegex);
Matcher inertiaMatcher = inertiaPattern.matcher(str);
while (inertiaMatcher.find()) {
System.out.println("惰性匹配到的字符串为:");
System.out.println(inertiaMatcher.group());//输出结果为:abc
}
}
注意: 分支结构"|"。
String str = "foohefoot";
String regex = "foo|foot";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(str);
while(matcher.find()){
System.out.println(matcher.group());
}
输出结果不是我们想要的foo和foot。两次的输出结果都是foo。
解决办法:
- 自己写的正则,把匹配的长串放在前面。比如正则改成"foot|foo"。
- 接口传来的因其不可控,所以先用"|"进行分隔,转化成数组再转化成List。利用list.sort()方法。参数传一个Comparator匿名内部类,在compareTo方法中写比较的逻辑。或者直接使用Collections.sort(strList,Collections.reverseOrder());第二个参数代表逆序排序。String实现了Comparable,可以直接使用Collections.sort。但它是按照ascii值来排序的,但对于此问题没有影响。比如正则是foo|foot|g那么调用Collections.sort之后便是g,foot,foo满足foot在foo的前面
总结:无论贪婪匹配、惰性匹配都是在满足正则的基础上更多或更少的匹配字符。
正则匹配忽略大小写及区间去除
忽略大小写:(?i)
例如:(?i)abc。正则的含义为匹配abc且忽略大小写。
(?i)的作用范围在之后的所有字符(若没有括号)。若有括号作用范围就在括号内,包括子括号。
比如匹配abc其中忽略bc大小写。正则为:a(?i)bc
比如匹配abc其中忽略b大小写。正则为:a((?i)b)c
比如:a((?i)b(d)),代表的含义是匹配abd且bd忽略大小写。
区间内去除:[区间&&[^去除的字符]]
比如:匹配字符a-f但不包含ce。正则为:[a-f&&[^ce]]。
匹配位置
什么是位置,也称为锚。字符相邻之间都有位置。比如字符串hello。箭头所示就是位置。
hello的等价形式可写成。""即是位置。与图作为呼应。
“hello” == “” + “h” + “” + “e” + “” + “l” + “” + “l” + “o” + “”;
匹配位置的一共有八个:^、$、\b、\B、(?=p)、(?!p)、(?<=p)、((?<!p))。后四种叫做断言或是环视。在匹配的时候可以想象成等价的形式,看匹配的是哪个位置。
- ^以什么开头。比如 ^abc则匹配的是以a开头的字符串后面是bc。匹配abc,abcd等,但不匹配babc。用在[^]中,表示不匹配的字符。比如[^bc]代表不匹配字符b跟c。
- $以什么结尾。比如abc$。表示以c结尾的。比如addc,abcc,dabc等。
- \b是单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置。比如匹配单词feature。正则为\\dfeature\\d。
匹配的是单词边界,一个单词有两个边界。若正则写成feature\\d。则匹配的是以feature结尾的单词。 - \B它的含义与\b相反。
- (?=p) 肯定顺序环视。p是一个子模式,即该位置的后面匹配p。也可以理解成在模式p前面的位置。比如正则为(?=hel)。字符串为hello。匹配的即是hel前面的(位置),是图h前面的箭头是等价形式h前面的""。
- (?!p)否定顺序环视。与肯定顺序环视相反。比如正则为(?!hel)。匹配的位置为除了hel前面的位置。即图中除了h前面的箭头其它位置都匹配。
- (?<=p)肯定逆序环视。p是一个子模式,即该位置的前面匹配p。也可以理解成在模式p后面的位置。比如正则为(?=hel)。字符串为hello。匹配的即是hel后面的(位置),是图第一个l后面的箭头是等价形式第一个l后面的""。
- ((?<!p))否定逆序环视。匹配的位置与肯定逆序环视相反。
比如 字符串"for example he is wise man"。想在这句话的后面加me too。是在后面的位置加。所以使用(?<=P)。正则为:(?<=he is wise man)。
String str = "for example he is wise man";
String regex = "(?<=he is wise man)";
System.out.println(str.replaceAll(regex," me too"));
环视匹配的是位置,替换或分隔时不会消除模式p的字符。
String str = "hello";
String regex = "l";
System.out.println(Arrays.toString(str.split(regex)));//结果[he, , o]
String lookAroundRegex = "(?=l)";
System.out.println(Arrays.toString(str.split(lookAroundRegex)));//结果[he, l, lo]
小技巧:匹配不是开头位置 ,[?!^]。
参考资料:
老马说编程
JavaScript正则表达式迷你书
每篇一语
拟把疏狂图一醉,对酒当歌,强乐还无味。——《蝶恋花·伫倚危楼风细细》柳永