摘要
正则表达式可以说是一种非常强大的文本,引用维基百科 的解释:正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。无论是前端还是后端,或者说是日常办公,都会或多或少的用到正则表达式。尤其当我们碰到字符串匹配的时候,或者文本替换的时候,正则表达式都可以非常方便的解决问题。
1.Java的转义字符
记得以前学c语言的时候,要想实现在输出语句的末尾换行的话,需要在最后面加上\n,那时候的大概理解就是在某些特定的字母前面加上\,那么这个字符就会有特别的含义。
1.1 为什么需要转义字符
由于某些字符,比如\n,\r,\b,\t等,无法用某一个键来代表它,所以我们就需要使用\+字符的形式代表他们。注意:在windows系统中\r\n表示一个换行符,在linux系统中用\n表示一个换行符。
1.2 正则表达式的规则
其实真正要学的,也就是这些基本的规则了。
常用元字符 |
|||
|---|---|---|---|
字符
| 含义
| 字符
| 含义
|
| ^ | 开始的位置 | . | 单个任意字符(不一定包括换行符) |
| $ | 结束的位置 | \w | 单个字符(字母/数字/下划线/汉字) |
| \s | 单个空白字符(\n\t\r) | \d | 单个数字字符 |
| \b | 单词的开始或结束 | ||
重复 |
|||
|---|---|---|---|
字符
| 含义
| 字符
| 含义
|
| * | 0次或多次 | + | 1次或多次 |
| ? | 0次或1次 | {n} | n次 |
| {n,} | >=n次 | {n,m} | n到m次 |
选择 |
|||
|---|---|---|---|
字符
| 含义
| 字符
| 含义
|
| [aeiou] | 单个的a/e/i/o/u字符之一 | [0-9] | 单个数字字符 |
| [A-Z] | 单个大写字符 | [A-Z0-9_] | 大写字母或数字或者下划线 |
| Hi|hi | Hi或hi | ||
反义 |
|||
|---|---|---|---|
字符
| 含义
| 字符
| 含义
|
| [^aeiou] | 单个的除a/e/i/o/u字符之外的字符 | [^x] | 单个的非x字符 |
| \W | 单个的非\w(字母/数字/下划线/汉字) | \S | 单个的非\s(空白) |
| \D | 单个的非\d(数字)字符 | \B | 非开头/结束位置 |
注意:
- 1.当需要用到字符原本的含义的时候,都需要使用转义,比如
\^,\[,\]。 - 2.注意,上面列举的各种规范在绝大多数情况下都是能正常工作的,但是由于正则表达式引擎的不同(比如JS、Pythong、Java),所以在细枝末节上,不同的引擎会有不同的实现。
1.3 看一些例子加深印象
- 匹配5到12位的数字:
^\d{5,12}$ - 匹配0或者1-9开头的数字:
^0|[1-9][0-9]*$ - 匹配小数:
^(-?)(\d\.\d+)?$ - 匹配国内的电话号码:
^\d{3}-\d{8}|\d{4}-\d{7}$ - 匹配YYYY-MM-DD格式:
^\d{4}-\d{1,2}-\d{1,2}$ - 匹配Windows中匹配空白行:
\n\s*\r
1.4 Java中的正则表达式
在String中的方法中,同样存在许多正则表达式的例子,比如split()、replaceAll()、replaceFirst()、以及matches()方法,这些方法本质上都是正则表达式。但是注意,在代码中尽量少的使用正则表达式,原因有两点:
1.正则表达式需要解析,解析的过程会调用Pattern.compile方法:
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
这个方法返回一个Pattern实例,如果点进去看Pattern类的话可以看到,整个类中有5000多行代码,说明这个类的实例占用的内存还是挺大的。比较推荐的方法就是定义一个全局的正则表达式,这样就可以只编译一次。
2.正则表达式匹配字符串是非常低效的过程,他的本质就是通过回溯(类似走迷宫),将表达式逐个与字符串比较,知道找到匹配的字符串,所以容易发生调用栈的溢出。
1.5 贪婪与非贪婪
引用贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为,贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配,而非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配。举例说明:
有这样一个特殊的字符串:String s = "a123a123a123a";
如果用贪婪匹配:
String regex = "a\\w*a";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(s);
if(matcher.find()){
System.out.println(matcher.group());
}
结果:
a123a123a123a
Process finished with exit code 0
也就是说当它匹配到第一个符合要求的字符串片段的时候,他不会停止,而是会往后面继续寻找,知道不符合要求时停止。
采用非贪婪模式匹配,将正则表达式写成:
String regex = "a\\w*?a";
结果:
a123a
Process finished with exit code 0
很明显,当它匹配到第一个符合要求的字符串片段时,就会立即返回。
注意:
返回匹配到的结果之前,必须加上条件判断语句 if(matcher.find()),否则无论匹配结果是什么都会报错。
1.6 指定正则表达式的模式
(?i)——忽略大小写(?m)——指定多行模式(?s)——指定.可以匹配包括换行符在内的所有符号
2.正则表达式的分组与捕获
在前面所列举的所有例子中,都是在判断字符串是否匹配正则表达式的书写格式。但是当我们们想要拿到符合正则表达式规则的那部分字符串的时候,这就需要用到正则表达式的分组了。
前面一直没有讲到括号的用法,其实括号除了表示优先级之外,最重要的作用就是分组了,以这个例子举例:
public static void main(String[] args) {
String s = "a123-nras-de12-97de";
String regex = "(a\\w+?)(-\\w+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(s);
if(matcher.find()){
System.out.println(matcher.group(0));
}
}
结果:
- gropu(0)的结果:a123-nras
- group(1)的结果:a123
- gropu(2)的结果:-nras
从结果可以看出,分组的作用就是拿到匹配后的字符串在正则表达式中,对应的组的部分字符串。0代表整个匹配的字符串,1代表第一组括号中的部分字符串......
注意:
- 括号的编号是只看左括号的。
- 如果想忽略某组括号,只需要在括号中加入
?:,举例:
public static void main(String[] args) {
String s = "a123-nras-de12-97de";
String regex = "(a\\w+?)(?:-\\w+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(s);
if(matcher.find()){
System.out.println(matcher.group(0));
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
}
}
上面的代码会报越界错误:
a123-nras
a123
Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 2
at java.util.regex.Matcher.group(Matcher.java:538)
at hello.regix.RegexText.main(RegexText.java:16)
3.正则表达式实战
3.1 获取GC日志中的分区大小信息
一个记事本文件gcLog.txt中存储着JVM的GC信息(在运行参数的VM options中写入-XX:+PrintGCDetails即可获得):
Heap
PSYoungGen total 18432K, used 5519K [0x00000000eb580000, 0x00000000eca00000, 0x0000000100000000)
eden space 15872K, 34% used [0x00000000eb580000,0x00000000ebae3ee8,0x00000000ec500000)
from space 2560K, 0% used [0x00000000ec780000,0x00000000ec780000,0x00000000eca00000)
to space 2560K, 0% used [0x00000000ec500000,0x00000000ec500000,0x00000000ec780000)
ParOldGen total 42496K, used 0K [0x00000000c2000000, 0x00000000c4980000, 0x00000000eb580000)
object space 42496K, 0% used [0x00000000c2000000,0x00000000c2000000,0x00000000c4980000)
Metaspace used 4684K, capacity 4754K, committed 4992K, reserved 1056768K
class space used 525K, capacity 563K, committed 640K, reserved 1048576K
现在我们想拿出JVM各部分的大小信息,代码如下:
public static void main(String[] args) throws IOException {
Pattern pattern = Pattern.compile("\\s+(\\w+)\\s+(\\w*?)\\s*(\\w+)\\s+(\\d+K)");
List<String> list = Files.readAllLines(new File("gcLog").toPath());
list.stream().forEach(s -> {
Matcher matcher = pattern.matcher(s);
if (matcher.find()) {
System.out.println(matcher.group(1) + matcher.group(2) + "_"
+ matcher.group(3) + ":" + matcher.group(4));
}
});
}
结果如下:
PSYoungGen_total:18432K
eden_space:15872K
from_space:2560K
to_space:2560K
ParOldGen_total:42496K
object_space:42496K
Metaspace_used:4684K
classspace_used:525K
3.2 删除日志中的时间戳
String str =
"[2019-08-01 21:24:41] bt3102 (11m:21s)\n"
+ "[2019-08-01 21:24:42] TeamCity server version is 2019.1.1 (build 66192)\n"
+ "[2019-08-01 21:24:43] Collecting changes in 2 VCS roots (22s)\n";
需要将[XXX]替换成"",这里可以使用replaceAll方法,但是由于字符串是多行的,所以需要在正则表达式中指定为多行模式:
String regex = "(?m)^\\[.*?]";
return str.replaceAll(regex, "");
这里另外有两个训练正则表达式的题目,有兴趣的可以自己去尝试下,代码太多我就不贴出来了。github.com/hcsp/regula…
4.参考资料
- 1.CSDN.《正则基础之——贪婪与非贪婪模式》点击此处跳转至源文章
- 2.segmentfault.《正则表达式详解》点击此处跳转至源文章