每种语言对正则的支持略有不同, 这里我们主要说的是Java对正则表达式的支持.
什么是正则表达式
正则表达式(Regular Expression), 常简写为regex, 是定义搜索模式的一组字符串 (用一组字符描述了一个字符串规则). 通常用于字符串查找和字符串的校验.
小试牛刀
判断手机号字符串的正则表达式.
笼统讲手机号的特点: 以1开头, 后面是10位数字. 所以我们写一个简单判断是否满足这个条件的正则表达式:
^1\d{10}$
其中, ^
代表起始位置, $
代表结尾位置. \d
代表是一个数字字符, 后面大括号里面的数字, 代表它前面的元素会出现10次.
我们可以简单用一行代码验证下:
System.out.println("13436417560".matches("^1\\d{10}$"));
注, 因 Java 代码里面反斜杠起转义作用, 例如
\t
代表tab
, 所以在 Java 代码里, 两个反斜杠才能表示出一个反斜杠.
正则表达式中常用字符
正则表达式中的转义
正则表达式通过反斜杠来进行转义. 例如:
点字符.
代表任意字符, 而\.
代表真正的小数点.\d
代表一个数字字符, 而 \\d
代表一个反斜杠和一个字符d
.\\
代表一个反斜杠字符.
常用字符
常用字符 | 意义 |
---|---|
x | 普通字符 x |
^ | 表示字符串起始位置 |
$ | 表示字符串结尾位置 |
\\ | 反斜杠字符 |
\t | tab字符 |
\n | 换行字符 |
\r | 回车字符 |
. | 代表任意字符(默认不会匹配\r和\n, 需要配置才匹配) |
[abc] | 方括号表示其中的任意字符. 方括号中任意字符(可能是a 或 b 或 c) |
[^abc] | 不在方括号中的任意字符 |
[a-zA-Z] | 任意字母, 包括大写和小写 |
\d | 任意数字 [0-9] |
\D | 任意非数字 [^0-9] |
\s | 任意空字符 [ \t\n\x0B\f\r] |
\S | 任意非空字符 [^\s] |
\w | 任意组成单词字符 [a-zA-Z_0-9] |
\W | 任意非组成单子字符 [^\w] |
x? | 代表x字符不存在或者存在1次(最多存在1次) |
x* | 代表x字符不存在或者存在任意次 |
x+ | 代表x字符至少存在1次 |
x{5} | x字符存在5次 |
x{3,5} | x字符存在3到5次 |
正则表达式应用场景简介
字符串查找
正则表达式查找可以解决普通查找只能根据"特定文本"查找的问题.
示例:
假设现在有一堆 JSON 日志, 我们需要查出 cabin
以 X
开头的日志(X后面只可能跟数字): 假设日志如下:
..."from":"PEK", "cabin":"Y"...
..."from":"SHA", "cabin":"X"...
..."from":"XIY", "cabin":"X2"...
..."from":"XIY", "cabin":"Y2"...
我们提取的正则表达式是:
cabin":"X\d*"
字符串校验
使用正则表达式可以校验文本是否符合一定规范. 例如一开始提到的手机号格式校验. 还有邮箱格式校验等. 也可以对身份证格式进行简单的校验.
字符串提取
我们可以使用正则表达式将文本中的一部分提取出来. 主要用到了正则表达式中的 capturing group
概念.
capturing group
正则表达式中可以使用小括号将表达式分成多个捕获组. 分组以后, 可以通过捕获组号, 取出对应匹配的内容. 通过数左半括号即可获得组号. 例如:
((A)(B(C)))
上面正则表达式对应的捕获组信息如下: 捕获组号 | 对应内容 ---|--- 1 | ((A)(B(C))) 2 | (A) 3 | (B(C)) 4 | (C)
第0组总是代表整个正则表达式.
示例代码如下:
Pattern pattern = Pattern.compile("((A)(B(C)))");
Matcher matcher = pattern.matcher("XXXABCDEF");
if (matcher.find()) {
for (int i = 0; i <= matcher.groupCount(); i++) {
System.out.println("group " + i + " : " + matcher.group(i));
}
}
输出为:
group 0 : ABC
group 1 : ABC
group 2 : A
group 3 : BC
group 4 : C
其他应用示例:
从大量日志中, 获取每天的车次号, 并计数.
named-capturing group
在Java中, Java7以后, 可以为 capturing-group 命名. 取的时候可以根据名字来取. 命名示例如下:
(?<Name>[Pp]attern)
通过在普通 capturing-group 的最前面, 添加 ?<xxx>
来指定名字. 使用示例如下:
public static String fetchOneNamedGroup(String src, String regex, String groupName) {
Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(src);
if (matcher.find()) {
return matcher.group(groupName);
}
return null;
}
public static void main(String[] args) throws Exception {
String sourceType = fetchOneNamedGroup("sourceType:PC,name:test", "sourceType:(?<sourceType>\\w+)",
"sourceType");
System.out.println(sourceType);
}
// 结果是"PC"
注意, capturing-group 的名字必须满足条件:
[A-Za-z][A-Za-z0-9]*
贪婪匹配 vs 最小匹配
我们要从Hello World
中获取H和l以及之间的字符 Hel
. 于是, 我们写了如下正则表达式:
H.*l
但是结果却并不是我们想要的:
Hello Worl
仔细分析发现, Hello Worl
这个结果也同样满足我们的正则表达式.
现在问题在于我们可以认为 Hel
是我们要的结果(最小匹配), 也可以认为 Hello Worl
是第一个H和最后一个l之间的字符(贪婪匹配).
正则表达式默认情况下是贪婪匹配模式. 想要最小匹配, 只需要在贪婪匹配模式符后面加一个?
, 即可转化为最小匹配.
例如:
我们使用H.*?l
的结果就是最小匹配:
Pattern pattern = Pattern.compile("H.*?l");
Matcher matcher = pattern.matcher("Hello World");
if (matcher.find()) {
System.out.println(matcher.group(0));
}
// 结果: Hel
Java里面的正则表达式
Java里面通过 java.util.regex.Pattern
实现了对正则的支持.
Java里面的正则和grep命令里面的就略有不同, 例如在Java里面, 星号 * 代表0个或多个, 而在 grep 里面, * 代表任意内容.
.* 多行匹配
默认情况下, .
在Java不会匹配换行符. 也就是 .*
只能匹配一行. 如果需要通过 .*
匹配多行情况, 可以开启 DOTALL
mode.
示例:
public static String fetchGroup1(String src, String regex) {
Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(src);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
String 中正则表达式相关方法
String 中有几个直接正则表达式的方法, 使用方便, 同时可以用于测试.
boolean matches (String regex)
String replaceAll (String regex, String replacement)
String replaceFirst (String regex, String replacement)
String[] split (String regex)
String[] split (String regex, int limit)
注意, replace, replaceFirst 和 replaceAll 的区别.
痛点
Java字符串通过反斜杠来标示特殊字符, 而正则表达式同样通过反斜杠来专业, 导致代码中的正则表达式可读性极差.
正则表达式中的特殊结构体(不常用)
(?:pattern) 非获取匹配
匹配pattern但不获取匹配结果, 也就是说这是一个非获取匹配. 尽管匹配, 将此group匹配结果不保存, 不作为最终结果返回.
示例:
https://stackoverflow.com/questions/tagged/regex
正则表达式: (https?|ftp)://([^/\r\n]+)(/[^\r\n]*)?
匹配结果:
Match "https://stackoverflow.com/questions/tagged/regex"
Group 1: "https"
Group 2: "stackoverflow.com"
Group 3: "/questions/tagged/regex"
如果并不在意用的什么协议, 但是 http/https/ftp 协议是使用括号括起来并用了|连接符. 如果想把这个group的结果给舍弃了, 则通过费获取匹配:
(?:https?|ftp)://([^/\r\n]+)(/[^\r\n]*)?
结果是:
Match "https://stackoverflow.com/questions/tagged/regex"
Group 1: "stackoverflow.com"
Group 2: "/questions/tagged/regex"
参考: https://stackoverflow.com/questions/3512471/what-is-a-non-capturing-group-what-does-do
(?=pattern) (?!pattern) (?<=pattern) (?<!pattern) 预查匹配
- (?=pattern) 正向肯定预查, 判断当前匹配后面是否有 pattern 所述字符串.
- (?!pattern) 正向否定预查, 判断当前匹配后面是否不包含 pattern 所述字符串.
- (?<=pattern) 反向肯定预查, 判断当前匹配前面是否有 pattern 所述字符串.
- (?<!pattern) 反向否定预查, 判断当前匹配前面是否不包含 pattern 所述字符串.
预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
参考:
bar(?=bar) finds the 1st bar ("bar" which has "bar" after it)
bar(?!bar) finds the 2nd bar ("bar" which does not have "bar" after it)
(?<=foo)bar finds the 1st bar ("bar" which has "foo" before it)
(?<!foo)bar finds the 2nd bar ("bar" which does not have "foo" before it)
https://stackoverflow.com/questions/2973436/regex-lookahead-lookbehind-and-atomic-groups
使用经验
- 尽量少些长正则, 因为难以维护
- 在写稍长的正则表达式时, 可以分段写, 写一段测一段
- 如果正则非常复杂, 而且麻烦, 就要考虑是否是正则适合的场景, 需要考虑使用其他方式来实现.
特殊示例
System.out.println("www".replaceAll("a?", "替换"));
// 结果是: 替换w替换w替换w替换
参考
https://docs.oracle.com/javase/9/docs/api/java/util/regex/Pattern.html#sum
https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines