正则表达式入门

每种语言对正则的支持略有不同, 这里我们主要说的是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 日志, 我们需要查出 cabinX 开头的日志(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 命名. 取的时候可以根据名字来取. 命名示例如下:

(?[Pp]attern)

通过在普通 capturing-group 的最前面, 添加 ? 来指定名字. 使用示例如下:

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:(?\\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 所述字符串.
  • (?

预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。

参考:

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)
(?

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

https://www.cnblogs.com/exmyth/p/7774918.html

https://blog.csdn.net/qq_19865749/article/details/77478489

转载于:https://my.oschina.net/u/1169457/blog/1940717

你可能感兴趣的:(正则表达式入门)