Java-正则表达式

Java 正则表达式 笔记

测试正则表达式,可选 Java 风格 https://regex101.com/

正则大全 https://any86.github.io/any-rule/


正则表达式实现原理

DFA 和 NFA

正则引擎大体上可分为不同的两类:DFA 和 NFA,而 NFA 又基本上可以分为传统型 NFA 和 POSIX NFA。 DFA(Deterministic finite automaton) 确定型有穷自动机 NFA(Non-deterministic finite automaton) 非确定型有穷自动机

DFA引擎因为不需要回溯,所以匹配快速,但不支持捕获组,所以也就不支持反向引用和$number这种引用方式,目前使用DFA引擎的语言和工具主要有awk、egrep 和 lex。DFA 自动机的时间复杂度是线性的,更加稳定,但是功能有限。

POSIX NFA 主要指符合 POSIX 标准的 NFA 引擎,它的特点主要是提供 longest-leftmost 匹配,也就是在找到最左侧最长匹配之前,它将继续回溯。同 DFA 一样,非贪婪模式或者说忽略优先量词对于 POSIX NFA 同样是没有意义的。 NFA 的功能更加强大,大多数语言和工具使用的是传统型的NFA引擎,包括 Java 、.NET、Perl、Python、Ruby、PHP 等语言都使用了 NFA 去实现其正则表达式。

匹配过程

NFA 是以正则表达式为基准去匹配的。也就是说,NFA 自动机会读取正则表达式的一个一个字符,然后拿去和目标字符串匹配,匹配成功就换正则表达式的下一个字符,否则继续和目标字符串的下一个字符比较。

例 1 源字符串:abc 正则表达式:abc 匹配过程: 首先由字符“a”取得控制权,从位置0开始匹配,由“a”来匹配“a”,匹配成功,控制权交给字符“b”;由于“a”已被“a”匹配,所以“b”从位置1开始尝试匹配,由“b”来匹配“b”,匹配成功,控制权交给“c”;由“c”来匹配“c”,匹配成功。 此时正则表达式匹配完成,报告匹配成功。匹配结果为“abc”,开始位置为0,结束位置为3。

例 2 源字符串:abc 正则表达式:ab?c 量词“?”属于匹配优先量词,在可匹配可不匹配时,会先选择尝试匹配,只有这种选择会使整个表达式无法匹配成功时,才会尝试让出匹配到的内容。这里的量词“?”是用来修饰字符“b”的,所以“b?”是一个整体。 匹配过程: 首先由字符“a”取得控制权,从位置0开始匹配,由“a”来匹配“a”,匹配成功,控制权交给字符“b?”;由于“?”是匹配优先量词,所以会先尝试进行匹配,由“b?”来匹配“b”,匹配成功,控制权交给“c”,同时记录一个备选状态;由“c”来匹配“c”,匹配成功。记录的备选状态丢弃。 此时正则表达式匹配完成,报告匹配成功。匹配结果为“abc”,开始位置为0,结束位置为3。

例 3 源字符串:abd 正则表达式:ab?c

匹配过程: 首先由字符“a”取得控制权,从位置0开始匹配,由“a”来匹配“a”,匹配成功,控制权交给字符“b?”;先尝试进行匹配,由“b?”来匹配“b”,同时记录一个备选状态,匹配成功,控制权交给“c”;由“c”来匹配“d”,匹配失败,此时进行回溯,找到记录的备选状态,“b?”忽略匹配,即“b?”不匹配“b”,让出控制权,把控制权交给“c”;由“c”来匹配“b”,匹配失败。此时第一轮匹配尝试失败。

正则引擎使正则向前传动,由位置1开始尝试匹配,由“a”来匹配“b”,匹配失败,没有备选状态,第二轮匹配尝试失败。

继续向前传动,直到在位置3尝试匹配失败,匹配结束。此时报告整个表达式匹配失败。

正则基础之——NFA引擎匹配原理 https://blog.csdn.net/lxcnn/article/details/4304651


Pattern

java.util.regex.Pattern

public final class Pattern
extends Object
implements Serializable

正则表达式的编译表示形式。 指定为字符串的正则表达式必须首先被编译为此类的实例。然后,可将得到的模式用于创建 Matcher 对象,依照正则表达式,该对象可以与任意字符序列匹配。执行匹配所涉及的所有状态都驻留在匹配器中,所以多个匹配器可以共享同一模式。

因此,典型的调用顺序是:

Pattern p = Pattern.compile("a*b");
Matcher m = p.matcher("aaaaab");
boolean b = m.matches();

在仅使用一次正则表达式时,可以方便地通过此类定义 matches 方法。此方法编译表达式并在单个调用中将输入序列与其匹配。 语句boolean b = Pattern.matches("a*b", "aaaaab"); 等效于上面的三个语句,尽管对于重复的匹配而言它效率不高,因为它不允许重用已编译的模式。

compile()

public static Pattern compile(String regex) 将给定的正则表达式编译到模式中。 参数: regex - 要编译的表达式 抛出: PatternSyntaxException - 如果表达式的语法无效

matcher()

public Matcher matcher(CharSequence input) 创建匹配给定输入与此模式的匹配器。 参数: input - 要匹配的字符序列 返回: 此模式的新匹配器

matches() 整体是否匹配

static boolean matches(String regex, CharSequence input) 编译给定正则表达式并尝试将给定输入与其匹配。 例如:boolean b = Pattern.matches("a*b", "aaaaab"); 调用此便捷方法的形式 Pattern.matches(regex, input); 与表达式 Pattern.compile(regex).matcher(input).matches() 的行为完全相同。 如果要多次使用一种模式,编译一次后重用此模式比每次都调用此方法效率更高。 参数: regex - 要编译的表达式 input - 要匹配的字符序列 抛出: PatternSyntaxException - 如果表达式的语法无效


匹配模式

https://www.cnblogs.com/mengw/p/11454848.html

Pattern.CASE_INSENSITIVE(?i) 忽略大小写

Java 正则表达式忽略大小写

1、加 (?i) 指定某些字符忽略大小写 (?i)abc 表示abc都忽略大小写 a(?i)bc 表示bc忽略大小写 a((?i)b)c 表示只有b忽略大小写

2、整体都忽略大小写 Pattern.compile(rexp, Pattern.CASE_INSENSITIVE)

Pattern.DOTALL(?s) .匹配换行

默认 . 匹配除换行符以外的任意字符,不匹配换行符,所以无法匹配多行文本。 如果想让 . 匹配多行文本,需要使用 Pattern 类的 DOTALL 模式,DOTALL 模式下 . 可以匹配包括换行符在内的任意字符。

Pattern p1 = Pattern.compile("a.*b");
System.out.println(p1.matcher("a\nb").find()); // 输出false,默认点(.)没有匹配换行符

Pattern p2 = Pattern.compile("a.*b", Pattern.DOTALL);
System.out.println(p2.matcher("a\nb").find()); //输出true,指定Pattern.DOTALL模式,可以匹配换行符。

Pattern p3 = Pattern.compile("(?s)a.*b");
System.out.println(p3.matcher("a\nb").find()); // true,(?s)指定DOTALL模式

例如从模式 法律名:章节名:【条目名】 条目内容 中匹配出条目内容

List<String> texts = List.of(
        """
        中华人民共和国劳动争议调解仲裁法:第一章 总则:【第二条】 【适用范围】中华人民共和国境内的用人单位与劳动者发生的下列劳动争议,适用本法:
        (一)因确认劳动关系发生的争议;
        (二)因订立、履行、变更、解除和终止劳动合同发生的争议;
        """,
        """
        失业保险条例:第二章 失业保险基金:【第十一条】 失业保险基金必须存入财政部门在国有商业银行开设的社会保障基金财政专户,实行收支两条线管理,由财政部门依法进行监督。
        存入银行和按照国家规定购买国债的失业保险基金,分别按照城乡居民同期存款利率和国债利息计息。失业保险基金的利息并入失业保险基金。
        失业保险基金专款专用,不得挪作他用,不得用于平衡财政收支。
        """,
        """
        中华人民共和国民事诉讼法:第二编 审判程序:第十二章 第一审普通程序:第五节 判决和裁定:【第一百五十八条】 最高人民法院的判决、裁定,以及依法不准上诉或者超过上诉期没有上诉的判决、裁定,是发生法律效力的判决、裁定。
        """,
        """
        中华人民共和国建筑法:第三章建筑工程发包与承包:第二节发 包:【第十九条】建筑工程依法实行招标发包,对不适于招标发包的可以直接发包。
        """
);
Pattern p4 = Pattern.compile("^.*?:.*?:【.*?】\\s*(?s)(.*)");
texts.forEach(text -> {
    Matcher matcher = p4.matcher(text);
    if (matcher.find()) {
        System.out.println(matcher.group(1));
    } else {
        System.out.println("未匹配");
    }
});

正则 ^.*?:.*?:【.*?】\\s*(?s)(.*) 解释: ^ 匹配开头, .*? 表示使用惰性模式匹配任意字符串,这是为了避免把两个中括号一起匹配了,例如 "【第二条】 【适用范围】" 只匹配第一个中括号 \\s* 匹配零个或多个空白 (?s) 表示后面的 (.*) 是 DOTALL 模式可匹配换行的,用来匹配法条是多行的。

Pattern.MULTILINE(?m) ^$多行匹配

在 Java 的正则表达式中,Pattern.MULTILINE(?m) 都是用来改变 ^$ 的匹配行为的。 默认 ^$ 只匹配整个字符串的开始和结束,而不是每一行的开始。如果想要匹配每一行的开始和结束,需要使用 Pattern.MULTILINE 标志或 (?m) Pattern.MULTILINE 会改变整个正则表达式的行为,而 (?m) 只会改变后面的一部分正则表达式的行为。

// 匹配每行的 第一章/节 开头
Pattern pattern = Pattern.compile("(?m)^\\s*第一(章|节)");   
// 或
Pattern pattern = Pattern.compile("^\\s*第一(章|节)", Pattern.MULTILINE); 

// 只匹配整个文档的 第一章/节 开头
Pattern pattern = Pattern.compile("^\\s*第一(章|节)");   

例如下面这段话通过 Pattern.compile("^\\s*第一(章|节)") 是匹配不到的,因为 第一章 不在整个字符串开头,需要改为 Pattern.compile("^\\s*第一(章|节)", Pattern.MULTILINE)

(1995年3月29日河北省大厂回族自治县第十一届人民代表大会第三次会议通过1995年7月8日河北省第八届人民代表大会常务委员会第十五次会议批准)

第一章总则

第一条为发展自治县教育事业,提高各族人民科学文化素质,巩固和发展平等、团结、互助的社会主义民族关系,促进经济发展和社会进步,根据《中华人民共和国宪法》和《中华人民共和国民族区域自治法》的规定,结合本县实际,制定本条例。

Matcher

java.util.regex.Matcher

public final class Matcher
extends Object
implements MatchResult

匹配方法

matches() 整体是否匹配

public boolean matches() 尝试将整个区域与模式匹配。 如果匹配成功,则可以通过 start、end 和 group 方法获取更多信息。 返回: 当且仅当整个区域序列匹配此匹配器的模式时才返回 true。

lookingAt() 从头开始匹配(但不需要完整匹配)

public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。 与 matches 方法类似,此方法始终从区域的开头开始;与之不同的是,它不需要匹配整个区域。 如果匹配成功,则可以通过 start、end 和 group 方法获取更多信息。 返回:当且仅当输入序列的前缀匹配此匹配器的模式时才返回 true。

results() 匹配结果流(9+)

java 9开始引入 public Stream<MatchResult> results() 返回匹配结果流

返回匹配的全部 “第一章” 字符串: Pattern.compile("\n第一章").matcher("文本").results().map(MatchResult::group).toList()

使用 results() 方法找出一个字符串中所有的单词:

String text = "This is a test";
Pattern pattern = Pattern.compile("\\w+");
Matcher matcher = pattern.matcher(text);
matcher.results().forEach(matchResult -> System.out.println(matchResult.group()));

replaceAll() 匹配替换

public String replaceAll(String replacement) 将全部匹配的子序列替换为 replacement

find() 查找下个匹配序列

public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。 此方法从匹配器区域的开头开始,如果该方法的前一次调用成功了并且从那时开始匹配器没有被重置,则从以前匹配操作没有匹配的第一个字符开始。 如果匹配成功,则可以通过 start、end 和 group 方法获取更多信息。 返回: 当且仅当输入序列的子序列匹配此匹配器的模式时才返回 true。

matches()/find()/lookingAt()的区别

通过调用 Patternmatcher() 方法从模式创建匹配器。创建匹配器后,可以使用它执行三种不同的匹配操作: matches() 方法尝试将整个输入序列与该模式匹配。 lookingAt() 尝试将输入序列从头开始与该模式匹配。 find() 方法扫描输入序列以查找与该模式匹配的下一个子序列。 每个方法都返回一个表示成功或失败的布尔值。通过查询匹配器的状态可以获取关于成功匹配的更多信息。

matches() 对整个字符串进行匹配,只有整个字符串都匹配了才返回true lookingAt() 从输入的头开始找,只有字符串的前缀满足模式才返回true find() 方法在部分匹配时和完全匹配时返回true,匹配不上返回false;

matches() 是整个匹配,只有整个字符序列完全匹配成功,才返回True,否则返回False。但如果前部分匹配成功,将移动下次匹配的位置。 lookingAt() 是部分匹配,总是从第一个字符进行匹配,匹配成功了不再继续匹配,匹配失败了,也不继续匹配。 find() 是部分匹配,从当前位置开始匹配,找到一个匹配的子串,将移动下次匹配的位置。

Mathcher 的 find() 和 lookingAt() 方法执行成功之后,会影响后续的 find() 的执行,因为下一次 find() 会从上次匹配成功的位置开始继续查找,如果不想这样可以使用 reset() 方法复原匹配器的状态

reset()

public Matcher reset() 重置匹配器。 重置匹配器将放弃其所有显式状态信息并将其添加位置设置为零。匹配器的区域被设置为默认区域,默认区域就是其整个字符序列。此匹配器的区域边界的定位和透明度都不受影响。 返回: 匹配器。


分组捕获

正则中,每个用于匹配字符的 () 都是一个子表达式,可以被捕获分组捕获到。 对于所有的正则表达式,捕获组0都是正则表达式匹配的全部内容,然后第一对括号内包含的匹配内容是捕获组1,第二对括号内是捕获组2,以此类推。

捕获组编号

捕获组就是括号 () 匹配到的内容,按照 () 子表达式划分成若干组。 小括号 () 可以达到对正则表达式进行分组的效果,称为捕获组。

捕获组通过从左到右计算其开括号来编号,编号规则是左括号(从左到右出现的顺序,从1开始编号,也适用于嵌套的捕获组,比如 (A(B)) 中 (A(B))编号1,(B)编号2

例如,在表达式 ((A)(B(C))) 中,存在四个这样的组:

0 ((A)(B(C)))
1 ((A)(B(C)))
2 (A)
3 (B(C))
4 (C)

0 组始终代表整个表达式 可以通过调用 matcher 对象的 groupCount 方法来查看表达式有多少个分组。groupCount 方法返回一个 int 值,表示matcher对象当前有多个捕获组。 还有一个特殊的组(group(0)),它总是代表整个表达式。该组不包括在 groupCount 的返回值中。

@Test
public void captureNumber() {
    Pattern pattern = Pattern.compile("((((.*省).*市).*县).*街)(.*路)");
    Matcher matcher = pattern.matcher("河北省石家庄市高邑县北大街平安路");
    if (matcher.find()) {
        log.info("捕获组个数:{}", matcher.groupCount());
        for (int i = 0; i <= matcher.groupCount(); i++) {
            log.info("group({}): {}, start:{}, end:{}", i, matcher.group(i), matcher.start(i), matcher.end(i));
        }
    }
}

结果:

捕获组个数:5
group(0): 河北省石家庄市高邑县北大街平安路, start:0, end:16
group(1): 河北省石家庄市高邑县北大街, start:0, end:13
group(2): 河北省石家庄市高邑县, start:0, end:10
group(3): 河北省石家庄市, start:0, end:7
group(4): 河北省, start:0, end:3
group(5): 平安路, start:13, end:16

groupCount() 捕获组个数

public int groupCount() 返回此匹配器模式中的捕获组数。 根据惯例, 零组表示整个模式。它不包括在此计数中 任何小于等于此方法返回值的非负整数保证是此匹配器的有效组索引。 指定者:接口 MatchResult 中的 groupCount 返回: 此匹配器模式中的捕获组数。

group() = group(0)

public String group() {
    return group(0);
}

返回由以前匹配操作所匹配的输入子序列。 对于具有输入序列 s 的匹配器 m,表达式 m.group() 和 s.substring(m.start(), m.end()) 是等效的。

注意,某些模式(例如,a*)匹配空字符串。当模式成功匹配输入中的空字符串时,此方法将返回空字符串。

返回:以前匹配操作所匹配的字符串形式的子序列(可能为空)。 抛出:IllegalStateException - 如果没有尝试任何匹配,或者以前的匹配操作失败。

调用 group()/group(i) 前必须先调用 find() 方法,否则报错

group(i) 第i个捕获组内容

public String group(int group) 返回在以前匹配操作期间由给定组捕获的输入子序列。 对于匹配器 m、输入序列 s 和组索引 g,表达式 m.group(g) 和 s.substring(m.start(g), m.end(g)) 是等效的。

捕获组是从 1 开始从左到右的索引。组零表示整个模式,因此表达式 m.group(0) 等效于 m.group()。

如果该匹配成功了,但指定组未能匹配输入序列的任何部分,则返回 null。注意,某些组(例如,(a*))匹配空字符串。当这些的组成功匹配输入中的空字符串时,此方法将返回空字符串。

参数:group - 此匹配器模式中捕获组的索引。 返回:在以前的匹配期间组所捕获的子序列(可能为空);如果组未能匹配输入的部分,则返回 null。 抛出: IllegalStateException - 如果没有尝试任何匹配,或者以前的匹配操作失败。 IndexOutOfBoundsException - 如果在给定索引的模式中不存在捕获组。

调用 group()/group(i) 前必须先调用 find() 方法,否则报错

start() = start(0) 整个模式的匹配结果

public int start() 返回以前匹配的初始索引。

返回:第一个匹配字符的索引。 抛出:IllegalStateException - 如果没有尝试任何匹配,或者以前的匹配操作失败。

start(i) 第i个捕获组的开始下标

public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引。 捕获组是从 1 开始从左到右的索引。组零表示整个模式,因此表达式 m.start(0) 等效于 m.start()。

参数:group - 此匹配器模式中捕获组的索引。 返回:组捕获的首个字符的索引;如果匹配成功但组本身没有任何匹配项,则返回 -1。 抛出: IllegalStateException - 如果没有尝试任何匹配,或者以前的匹配操作失败。 IndexOutOfBoundsException - 如果在给定索引的模式中不存在捕获组。

end() = end(0)

public int end() 返回最后匹配字符之后的偏移量。 返回:最后匹配字符之后的偏移量。 抛出:IllegalStateException - 如果没有尝试任何匹配,或者以前的匹配操作失败。

end(i) 第i个捕获组的结束下标

返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。 捕获组是从 1 开始从左到右的索引。组零表示整个模式,因此表达式 m.end(0) 等效于 m.end()。

参数:group - 此匹配器模式中捕获组的索引。 返回:组捕获的最后字符之后的偏移量;如果匹配成功但组本身没有任何匹配项,则返回 -1。 抛出: IllegalStateException - 如果没有尝试任何匹配,或者以前的匹配操作失败。 IndexOutOfBoundsException - 如果在给定索引的模式中不存在捕获组。


非捕获组 (?:...)

在小括号 () 内的模式开头加入 ?: 表示这个括号仅做分组,但不创建反向引用,即不做捕获

例如 第1个括号是非捕获的:

@Test
public void nonCaptureBracket() {
    Pattern pattern = Pattern.compile("(?:\\d+)(\\w+)(.+)");
    Matcher matcher = pattern.matcher("123213english中文");
    if (matcher.find()) {
        log.info("捕获组个数:{}", matcher.groupCount());
        for (int i = 0; i <= matcher.groupCount(); i++) {
            log.info("group({}): {}, start:{}, end:{}", i, matcher.group(i), matcher.start(i), matcher.end(i));
        }
    }
}

结果:

捕获组个数:2
group(0): 123213english中文, start:0, end:15
group(1): english, start:6, end:13
group(2): 中文, start:13, end:15

命名捕获组 (?<name>)

可以对捕获组命名,在()内添加捕获组的名字,格式为 (?<name>expression),后续可以通过 group("name") 获取捕获组

@Test
public void namedCaptureGroup() {
    Pattern pattern = Pattern.compile("(?<num>\\d+)(?<eng>\\w+)(?<chi>.+)");
    Matcher matcher = pattern.matcher("123213english中文");
    if (matcher.find()) {
        log.info("捕获组个数:{}", matcher.groupCount());
        log.info("group(num): {}", matcher.group("num"));
        log.info("group(eng): {}", matcher.group("eng"));
        log.info("group(chi): {}", matcher.group("chi"));
    }
}

结果

捕获组个数:3
group(num): 123213
group(eng): english
group(chi): 中文

反向引用 \i

反向引用 Back References 正则表达式中可以使用 \i 引用前面捕获组的内容,其中 i 是正则表达式中捕获组的序号位置。 例如 \2 匹配第2个捕获组的内容。

/**
 * 反向引用
 */
@Test
public void backReference() {
    Pattern pattern = Pattern.compile("(\\d+)(\\w+)(\\1)");
    Matcher matcher = pattern.matcher("123english123");
    if (matcher.find()) {
        log.info("捕获组个数:{}", matcher.groupCount());
        for (int i = 0; i <= matcher.groupCount(); i++) {
            log.info("group({}): {}, start:{}, end:{}", i, matcher.group(i), matcher.start(i), matcher.end(i));
        }
    }
}

结果:

捕获组个数:3
group(0): 123english123, start:0, end:13
group(1): 123, start:0, end:3
group(2): english, start:3, end:10
group(3): 123, start:10, end:13

通过反向引用匹配连续重复单词

\\b(\\w+)\\s+\\1\\b 连续重复的单词,group(1) 内容就是重复的单词 (\\w+) 匹配一个或多个单词字符,即匹配一个单词,并通过捕获组1捕获 \\s+ 匹配一个或多个空白字符 \\1 匹配和第1个捕获组 (\\w+) 相同的内容,即一个相同的单词 \\b 匹配单词边界,前后都加上单词边界匹配是为了防止把前缀和后缀匹配的也当做重复单词。

/**
 * 连续重复单词
 */
@Test
public void duplicatedWord() {
    Pattern pattern = Pattern.compile("\\b(\\w+)\\s+\\1\\b");
    List<String> sentences = List.of(
            "he's a stupid stupid man",
            "he's a stupid stupid stupid man",
            "he's a stupid  stupid stupid stupider man",
            "he's astupid stupid man",
            "he's a stupid stupider man"
    );
    sentences.forEach(sentence -> {
        Matcher matcher = pattern.matcher(sentence);
        if (matcher.find()) {
            log.info("句子: {},重复单词:{}", sentence, matcher.group(1));
        } else {
            log.info("句子: {},没有重复单词", sentence);
        }
    });
}

结果:

句子: he's a stupid stupid man,重复单词:stupid
句子: he's a stupid stupid stupid man,重复单词:stupid
句子: he's a stupid  stupid stupid stupider man,重复单词:stupid
句子: he's astupid stupid man,没有重复单词
句子: he's a stupid stupider man,没有重复单词

命名反向引用 \k<name>

可以通过 (?<name>) 给捕获组命名,后续可以 \k<name> 通过名字反向引用捕获组

/**
 * 命名反向引用
 */
@Test
public void namedBackReference() {
    Pattern pattern = Pattern.compile("(?<dup>\\d+)(\\w+)(\\k<dup>)");
    Matcher matcher = pattern.matcher("123english123");
    if (matcher.find()) {
        log.info("捕获组个数:{}", matcher.groupCount());
        for (int i = 0; i <= matcher.groupCount(); i++) {
            log.info("group({}): {}, start:{}, end:{}", i, matcher.group(i), matcher.start(i), matcher.end(i));
        }
        log.info("group(dup): {}", matcher.group("dup"));
    }
}

结果:

捕获组个数:3
group(0): 123english123, start:0, end:13
group(1): 123, start:0, end:3
group(2): english, start:3, end:10
group(3): 123, start:10, end:13
group(dup): 123

零宽断言

正则表达式中的零宽断言是一种特殊的结构,它在匹配的时候不会消耗字符,只是对匹配位置进行条件判断。这对于一些复杂的模式匹配非常有用,因为它允许你在匹配位置前面或后面添加条件,从而更精确地控制匹配。 零宽断言,即前瞻和后顾,用于匹配在指定内容之前或之后的部分,根据前后是否匹配决定是否捕获当前组。

零宽(Zero-width): 只匹配位置,零宽意味着断言在匹配时不会"消耗"字符串,它只是对位置进行条件判断,不包括匹配位置之前或之后的字符在匹配结果中,例如 (?=hel) 并不消耗后面的 hel 这3个字符。 先行(Lookahead): 表示断言发生在匹配位置之前,从左往右匹配,所以前指的是更右边的字符串。 后行(Lookbehind): 表示断言发生在匹配位置之后,即左边的字符串 正向(Positive): 匹配括号中的表达式,即断言所作的条件判断是肯定的,即只有当条件成立时,匹配才成功。 负向(Negative): 不匹配括号中的表达式,即​断言所作的条件判断是否定的,即只有当条件不成立时,匹配才成功。

前瞻(?=) Lookahead

前瞻(?=) (?=pattern) 零宽正向先行断言(zero-width positive lookahead assertion) exp1(?=exp2) 查找后面是exp2的exp1 例如 AI(?=DU)

负前瞻(?!) Neg Lookahead

负前瞻(?!) (?!pattern) 零宽负向先行断言(zero-width negative lookahead assertion) 否定前视查找(Negative Lookahead) exp1(?!exp2) 查找后面不是exp2的exp1

后顾(?<=) Lookbehind

后顾(?<=) (?<=pattern) 零宽正向后行断言(zero-width positive lookbehind assertion) (?<=exp2)exp1 查找前面是exp2的exp1

负后顾(?

负后顾(?

@Test
public void testNegLookBehind() {
    // 提取冒号之后部分,负后顾断言确保冒号不在 http 或 https 之后
    Pattern COLON_NOT_AFTER_HTTP_PATTERN = Pattern.compile("(?<!http)(?<!https):(.*)");
    List.of(
            "PROCESSING_FLOW-f1efecded9574a45bc17234b83357aad-:ab9d61d9019a41f38758da8eedcd0862-file.txt",
            "PROCESSING_FLOW-2ab1b007443d4dc984d79a3c6a6d2e54-:fa09148ecbc741f4a50014b6f6c4e7d9-file.xlsx",
            "PROCESSING_FLOW-b1e2aa4a4b06419994ab246e48dabb27-https://www.baidu.org/xxxx.html?aa"
    ).forEach(str -> {
        Matcher matcher = COLON_NOT_AFTER_HTTP_PATTERN.matcher(str);
        String ret = matcher.find() ? matcher.group(1) : str;
        System.out.println(ret);
    });
}

正则表达式构造

Java 正则中需转义的字符

Java 正则中有些字符有特殊含义,比如 * 表示重复前一子表达式的任意次,. 表示除 \r\n 之外的任何单个字符,+ 表示前一子表达式的一次或多次,等等。 正则需要转义字符: '$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|' 如果要匹配这些特殊字符,就需要转义,Java 中使用反斜杠 \ 进行转义。

字符类

[abc] 复选集定义,匹配字母 a 或 b 或 c [^abc] 任何字符,除了 a、b 或 c(否定) [a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围) [a-d[m-p]] a 到 d 或 m 到 p(并集),等价于 [a-dm-p] [a-z&&[def]] d、e 或 f(交集) [a-z&&[^bc]] a 到 z,除了 b 和 c(减去),等价于 [ad-z] [a-z&&[^m-p]] a 到 z,而非 m 到 p(减去),等价于 [a-lq-z]

预定义字符类(元字符)

注意元字符在正则表达式中的写法,不能直接用元字符来编写我们的正则程序 . 匹配所有单个字符,除了换行符(Linux 中换行是 \n,Windows 中换行是 \r\n ) 注意:方框内 . 没有特殊含义,只是普通字符, [.][\\.] 是完全相同的,都只和字符.匹配;但方框外 . 匹配任意字符,\\. 才匹配普通.字符

元字符 在正则表达式中的写法 意义
. . 任何字符(除换行符以外,开启 DOTALL 模式后可匹配换行符)
\d \\d 0-9之间的任意一个数字,等价于 [0-9]
\D \\D 非数字,等价于 [^0-9]
\s \\s 空白字符,等价于 [ \t\n\x0B\f\r]
\S \\S 非空白字符,等价于 [^\s]
\w \\w 单词字符,等价于 [a-zA-Z_0-9]
\W \\W 非单词字符,等价于 [^\w]
\p{Lower} \\p[Lower] 小写字母[a~z]
\p{Upper} \\p{Upper} 大写字母[A~Z]
\p{ASCII} \\p{ACSII} ASCII字符
\p{Alpha} \\p{Alpha} 字母
\p{digit} \\p{digit} 数字字符[0~9]
\p{Alnum} \\p{Alnum} 字母或数字
\p{Punct} \\p{Punct} 标点符号
\p{graph} \\p{graph} 可视字符
\p{Print} \\p{Print} 可打印字符
\p{Blank} \\p{Blank} 空格或制表符
\p{Cntrl} \\p{Cntrl} 控制字符

边界匹配器

^ 行的开头,注意这里的 ^ 一定要和 [^] 中的 ^ 区分 $ 行的结尾 \b 单词边界 \B 非单词边界 \A 输入的开头 \G 上一个匹配的结尾 \Z 输入的结尾,仅用于最后的结束符(如果有的话) \z 输入的结尾

贪婪模式量词(Greedy quantifiers)/最大匹配(默认)

贪婪模式和非贪婪模式指的是在正则匹配过程中的行为,在贪婪模式下,匹配最长的匹配值。非贪婪模式下,匹配最短的匹配值。 Java 正则表达式默认用的是 greedy 贪婪匹配模式

匹配过程: 先看看整个字符串是否存在匹配,如果未发现匹配,则去掉字符串中的最后一个字符,再次尝试匹配,如果还是未发现匹配再去掉最后一个字符,循环往复直到发现一个匹配或者字符串不剩任何字符串。

X? X,一次或一次也没有, 是 {0,1} 的简写 X* X,零次或多次, 是 {0,} 的简写, .* 表示匹配任何字符串 X+ X,一次或多次, 是 {1,} 的简写 X{n} X,恰好 n 次 X{n,} X,至少 n 次 X{n,m} X,至少 n 次,但是不超过 m 次

惰性模式量词(Reluctant quantifiers)/最小匹配(*?)

Reluctant quantifiers, 称为 非贪婪模式,或勉强模式,或惰性模式,或最小匹配 如果 ? 是限定符 *+?{} 后面的第一个字符,那么表示非贪婪模式(尽可能少的匹配字符),而不是默认的贪婪模式 匹配过程: 先看看字符串的第一个字母是否存在匹配,如果未发现匹配,则读入下一个字符,再次尝试匹配,如果还是未发现匹配则再读取下一个字符,循环往复直到发现一个匹配或者整个字符串都检查过也没有发现匹配。

X?? X,一次或一次也没有 X*? X,零次或多次 X+? X,一次或多次 X{n}? X,恰好 n 次 X{n,}? X,至少 n 次 X{n,m}? X,至少 n 次,但是不超过 m 次

占有模式量词(Possessive quantifiers)/完全匹配(*+)

只尝试匹配整个字符串。如果整个字符串不匹配,则不做进一步。 意思就是只做贪婪匹配的第一步。 如果 + 是限定符 *+?{} 后面的第一个字符,那么表示完全匹配。 X?+ X,一次或一次也没有 X*+ X,零次或多次 X++ X,一次或多次 X{n}+ X,恰好 n 次 X{n,}+ X,至少 n 次 X{n,m}+ X,至少 n 次,但是不超过 m 次

贪婪量词 惰性量词 支配量词 描述
* *? *+ 可以不出现,也可以出现任意次
? ?? ?+ 可以出现0次或1次
+ +? ++ 至少出现1次或以上
{n} {n}? {n}+ 有且只能出现n次
{n,m} {n,m}? {n,m}+ 至少出现n次,至多出现m次
{n,} {n,}? {n,}+ 至少出现n次或以上

Java 正则表达式——贪婪匹配、惰性匹配、支配匹配 https://blog.csdn.net/chy555chy/article/details/53368506

Java 正则表达式 量词 --- 三种匹配模式【贪婪型、勉强型、占有型】 https://www.cnblogs.com/lavenderzh/p/5379406.html

Logical运算符

XY X 后跟 Y X|Y X 或 Y (X) X,作为捕获组


Redundant Character Escape ... in RegExp

方括号中的很多转移符都是没必要的,如果有多余的转移符,IDE会提示 "Redundant Character Escape ... in RegExp"

例如 [.][\\.] 是完全相同的:

Pattern pattern = Pattern.compile("[\\.]");
System.out.println(pattern.matcher("").find());
System.out.println(pattern.matcher(".").find());
System.out.println(pattern.matcher("a").find());

结果为 false true false

pattern = Pattern.compile("[.]");
System.out.println(pattern.matcher("").find());
System.out.println(pattern.matcher(".").find());
System.out.println(pattern.matcher("a").find());

结果为 false true false 但如果在方括号之外,想要匹配 . 的话,就需要转义符 \\. 了,否则 点 . 会匹配所有字符

Redundant Character Escape of .(dot) in java regex https://stackoverflow.com/questions/49084230/redundant-character-escape-of-dot-in-java-regex


Java 正则表达式应用举例

正则表达式用于String.split()分隔字符串

用空格来分隔单词时,可能单词间有多个空格,直接使用正则表达式 \\s++ 匹配一个或多个空格即可。

@Test
public void testSplit() {
    String input = "hello   world, i am  Java.";
    String[] words = input.split("\\s+");
    System.out.print(String.join(" ", words));
}

结果

hello world, i am Java.

java正则表达式匹配手机号

匹配 86-11位手机号

package com.masikkk.common;
import java.util.regex.Pattern;
public class test {
    public static void main(String[] args){
        String mobileRegExp = "^[0-9]{1,4}-[0-9]{11}$";
        Pattern p = Pattern.compile(mobileRegExp);
        System.out.println(p.matcher("68-13610101010").matches());
    }
}

Java 正则表达式 http://www.runoob.com/java/java-regular-expressions.html


单词字符及中横线

匹配 单词(大小写字母,下划线)、中横线.zip

Pattern FILE_PATH_PATTERN = Pattern.compile("[\\w-]+\\.zip")

java正则表达式匹配IPv4地址

IPv4 地址正则 ^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$

这个匹配不太精确,精确的匹配看网上其他文章。

@Test
public void testIpV4() {
    Pattern pattern = Pattern.compile("\\d{1,3}(\\.\\d{1,3}){3}");
    // true
    System.out.println(pattern.matcher("50.73.68.235").matches());
    System.out.println(pattern.matcher("0.0.0.0").matches());
    System.out.println(pattern.matcher("1.2.3.4").matches());
    System.out.println(pattern.matcher("999.999.999.999").matches());
    System.out.println(pattern.matcher("255.255.255.255").matches());
    // false
    System.out.println(pattern.matcher("255.255.255.255.").matches());
    System.out.println(pattern.matcher("255.255.255255").matches());
    System.out.println(pattern.matcher("255.255.255").matches());
    System.out.println(pattern.matcher("255.255.255.255.23").matches());
    System.out.println(pattern.matcher("1000.255.255.255").matches());
}

\\d{1,3}(\\.\\d{1,3}){3} 解释 \\d 是单个数字0-9 {1,3} 重复1到3次 \\. 转义匹配英文点


java正则表达式匹配邮箱地址

public static boolean isEmail(String string) {
        if (string == null)
            return false;
        String regEx1 = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
        Pattern p;
        Matcher m;
        p = Pattern.compile(regEx1);
        m = p.matcher(string);
        if (m.matches())
            return true;
        else
            return false;
    }

java判断邮箱是否合法 https://blog.csdn.net/u012934325/article/details/73558084 Java邮箱正则表达式 http://stevenjohn.iteye.com/blog/1058739


子串匹配并指定格式捕获数据

public static void main(String[] args) {
    Pattern p = Pattern.compile("(\\d+,)(\\d+)");
    String s = "123,456-34,345-55678";
    Matcher m = p.matcher(s);
    while (m.find()) {
        System.out.println("捕获个数:groupCount()=" + m.groupCount());
        System.out.println("m.group():" + m.group()); //打印所有
        System.out.println("m.group(1):" + m.group(1)); //打印第一组
        System.out.println("m.group(2):" + m.group(2)); //打印第二组
        System.out.println();
    }
}

每次while循环匹配逗号分隔的纯数字子串: 第一轮while循环 find() 匹配 "123,456", 第二轮while循环 find() 匹配 "34,345", 剩下的"-55678"无法和模式匹配 每次while循环中输出捕获组的捕获情况: group(0)与group()等价,表示整个匹配的子串。 group(1)等价于第一个括号内的表达式返回的字符串,以此类推。 上述程序的执行结果如下:

捕获个数:groupCount()=2
m.group():123,456
m.group(1):123,
m.group(2):456

捕获个数:groupCount()=2
m.group():34,345
m.group(1):34,
m.group(2):345

子串匹配并指定格式捕获数据

过滤一个有规律性的字符串,得到想要的结果,如下面的字符串,最终想得到结果:25分 43分 100分

public static void main(String[] args) {
    String filetext = "张小名=25分|李小花=43分|王力=100分|";
    Pattern p = Pattern.compile("\\=(.*?)\\|");//正则表达式,取=和|之间的字符串,不包括=和|
    Matcher m = p.matcher(filetext);
    System.out.println("捕获组个数:" + m.groupCount());
    while(m.find()) {
        System.out.println(m.group(1));
    }
}

每个while循环输匹配竖线|分隔的一部分子串,循环内通过group输出捕获的编号为1的组。 结果:

捕获组个数:1
25分
43分
100分

提取两个分隔符之间的数据

比如要提取"["与"]"之间的字符串

public static void main(String[] args) {
    String str = "([长度] + [高度]) * [倍数] - [减号] / [除号] > [大于号] < [小于号] == [等号] ";
    String regx = "\\[(.*?)]";
    Pattern pattern = Pattern.compile(regx);
    Matcher matcher = pattern.matcher(str);
    while(matcher.find()) {
        System.out.println(matcher.group(1));
    }
}

结果为: 长度 高度 倍数 减号 除号 大于号 小于号 等号


判断字符串中是否包含中文字符

Java判断一个字符串是否有中文一般情况是利用Unicode编码(CJK统一汉字的编码区间:0x4e00–0x9fbb)的正则来做判断,但是其实这个区间来判断中文不是非常精确,因为有些中文的标点符号比如:,。等等是不能识别的。

public static boolean isContainChinese(String str) {
    Pattern p = Pattern.compile("[\u4e00-\u9fa5]");
    Matcher m = p.matcher(str);
    if (m.find()) {
        return true;
    }
    return false;
}

Java 完美判断中文字符 http://www.micmiu.com/lang/java/java-check-chinese/


拆分斜杠分隔的中英文

员工的title是由斜线分隔的中英文 规则: 1、如果既有中文又有英文,则肯定英文在前,中文在后 2、可能只有中文,也可能只有英文 3、中文或英文中还可能有斜线,且不止一个 4、中文或英文中还可能有的字符包括: 空白、中英文逗号、数字、下划线、&

处理过程解释: 一、正则 ([\\w\\s&,,/]+)/([\\u4e00-\\u9fa5\\w\\s&,,/]+) 1、有两个捕获组(两个括号),中间用斜线分隔,分别捕获斜线前的英文部分和斜线后的中文部分 2、每个捕获组内是至少一次的贪婪量词+,表示中括号内的字符至少得出现一次,不能为空 3、中括号内是中英文部分可出现的字符或预定义字符类 二、title是固定的最多一次的斜线分割的中英文组合,所以捕获组只需捕获一次,不需要循环出现,所以不用 while(m.find())

package com.masikkk.test;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.assertj.core.util.Lists;
import org.junit.Test;

public class TitleSplitTest {
    @Test
    public void testSplitTitle() {
        Pattern pattern = Pattern.compile("([\\w\\s&,,/]+)/([\\u4e00-\\u9fa5\\w\\s&,,/]+)");
        String s1 = "Senior Designer, UI/UX Design & Manager / 高级设计师,UI/UX设计与管理";
        String s2 = "Designer, UI/UX Design / 设计师,UI/UX 设计";
        String s3 = "Assistant Manager, Swapping/Charging  Quality Team / 助理经理,充换电质量团队";
        String s33 = "Purchasing Assistant Manager, Body & Exterior / 采购助理经理,车身外饰 /";
        String s34 = "Engineer, Robot/Simulation / 工程师, 机器人/仿真";
        String s35 = "Engineer, Product Quality Engineering, INT/EXT / 工程师,产品质量工程";
        String s4 = "only english title";
        String s5 = "只有中文title";
        String s6 = "中文title带着斜杠/";
        String s7 = "english/中文 yingwen/中中";

        List<String> titles = Lists.newArrayList(s1, s2, s3, s33, s34, s35, s4, s5, s6, s7);
        for (String title : titles) {
            System.out.println("title   : " + title);
            String enTitle = "";
            String cnTitle = "";
            Matcher m = pattern.matcher(title);
            if (m.find()) {
                System.out.println("captured: " + m.group());
                enTitle = m.group(1).trim();
                cnTitle = m.group(2).trim();
            } else {
                Matcher m2 = Pattern.compile("[\\u4e00-\\u9fa5]").matcher(title);
                if (m2.find()) {
                    cnTitle = title;
                } else {
                    enTitle = title;
                }
            }
            System.out.println("en_title: " + enTitle);
            System.out.println("cn_title: " + cnTitle);
            System.out.println();
        }
    }
}

结果:

title   : Senior Designer, UI/UX Design & Manager / 高级设计师,UI/UX设计与管理
captured: Senior Designer, UI/UX Design & Manager / 高级设计师,UI/UX设计与管理
en_title: Senior Designer, UI/UX Design & Manager
cn_title: 高级设计师,UI/UX设计与管理

title   : Designer, UI/UX Design / 设计师,UI/UX 设计
captured: Designer, UI/UX Design / 设计师,UI/UX 设计
en_title: Designer, UI/UX Design
cn_title: 设计师,UI/UX 设计

title   : Assistant Manager, Swapping/Charging  Quality Team / 助理经理,充换电质量团队
captured: Assistant Manager, Swapping/Charging  Quality Team / 助理经理,充换电质量团队
en_title: Assistant Manager, Swapping/Charging  Quality Team
cn_title: 助理经理,充换电质量团队

title   : Purchasing Assistant Manager, Body & Exterior / 采购助理经理,车身外饰 /
captured: Purchasing Assistant Manager, Body & Exterior / 采购助理经理,车身外饰 /
en_title: Purchasing Assistant Manager, Body & Exterior
cn_title: 采购助理经理,车身外饰 /

title   : Engineer, Robot/Simulation / 工程师, 机器人/仿真
captured: Engineer, Robot/Simulation / 工程师, 机器人/仿真
en_title: Engineer, Robot/Simulation
cn_title: 工程师, 机器人/仿真

title   : Engineer, Product Quality Engineering, INT/EXT / 工程师,产品质量工程
captured: Engineer, Product Quality Engineering, INT/EXT / 工程师,产品质量工程
en_title: Engineer, Product Quality Engineering, INT/EXT
cn_title: 工程师,产品质量工程

title   : only english title
en_title: only english title
cn_title:

title   : 只有中文title
en_title:
cn_title: 只有中文title

title   : 中文title带着斜杠/
en_title:
cn_title: 中文title带着斜杠/

title   : english/中文 yingwen/中中
captured: english/中文 yingwen/中中
en_title: english
cn_title: 中文 yingwen/中中

Java正则表达式解决字符串转整型atoi

不推荐这种解法,很慢,比直接从前往后扫描处理慢一个数量级。 时间复杂度也不好分析,但作为复习Java正则表达式,还是可以写一下的,当练手了。

public int myAtoi2(String str) {
    // ( *) 匹配0个或多个空格,[+-]? 匹配0个或1个符号位, \d+ 匹配1个或多个数字, .* 匹配剩余后缀字符
    Pattern pattern = Pattern.compile("( *)([+-]?\\d+).*");
    Matcher matcher = pattern.matcher(str);
    // 先用 matches() 对整个串进行完全匹配
    if (!matcher.matches()) {
        return 0;
    }
    // 重置匹配器后,使用 find() 进行部分匹配,其中的第2个捕获组就是合法整数对应的字符串
    matcher.reset();
    if (matcher.find()) {
        String validIntStr = matcher.group(2);
        try {
            return Integer.parseInt(validIntStr);
        } catch (NumberFormatException e) {
            return validIntStr.startsWith("-") ? Integer.MIN_VALUE : Integer.MAX_VALUE;
        }
    }
    return 0;
}

只编码url中的中文和空格

https://blog.csdn.net/qq_35341203/article/details/109334607

https://lanlan2017.github.io/ReadingNotes/7865fbca/

可以用 hutool 工具直接 URLUtil.encode(link),只编码中文和空格


判断是否包含 api|接口

private static final Pattern API_PATTERN = Pattern.compile("api|接口", Pattern.CASE_INSENSITIVE);
if (API_PATTERN.matcher("api文档").find()) {
    // 包含
}

提取接口文档中的Path/路径

Pattern PATH_PATTERN = Pattern.compile("(?:路径|Path)[:: ]*(.*)", CASE_INSENSITIVE);
Matcher matcher = PATH_PATTERN.matcher(doc);
if (matcher.find()) {
    return matcher.group(1).trim();
}

可匹配提取: api Path: POST /api/v1/user 路径: POST /api/v2/user


RegExUtils

removeFirst() 按正则删除第一个匹配

public static String removeFirst(final String text, final Pattern regex)

StringUtils.removeFirst(null, *)      = null
StringUtils.removeFirst("any", (Pattern) null)  = "any"
StringUtils.removeFirst("any", Pattern.compile(""))    = "any"
StringUtils.removeFirst("any", Pattern.compile(".*"))  = ""
StringUtils.removeFirst("any", Pattern.compile(".+"))  = ""
StringUtils.removeFirst("abc", Pattern.compile(".?"))  = "bc"
StringUtils.removeFirst("A&lt;__&gt;\n&lt;__&gt;B", Pattern.compile("&lt;.*&gt;"))      = "A\n&lt;__&gt;B"
StringUtils.removeFirst("A&lt;__&gt;\n&lt;__&gt;B", Pattern.compile("(?s)&lt;.*&gt;"))  = "AB"
StringUtils.removeFirst("ABCabc123", Pattern.compile("[a-z]"))          = "ABCbc123"
StringUtils.removeFirst("ABCabc123abc", Pattern.compile("[a-z]+"))      = "ABC123abc"