正则表达式与文本匹配
学习目标
- 读懂字符类、量词、分组、边界和转义等正则表达式基本语法。
- 使用
Pattern、Matcher完成整串校验、局部查找和捕获组提取。 - 区分
String.matches、Matcher.matches、find与lookingAt的匹配范围。 - 知道正则适合的输入规模和复杂度边界,避免用正则解析嵌套语言。
核心知识点
专业术语
- 正则表达式(regular expression):描述文本模式的规则语言。
- 字符类(character class):如
[A-Z]、\d,表示某一位置允许的字符集合。 - 量词(quantifier):如
*、+、?、{2,4},表示前一个元素重复次数。 - 捕获组(capturing group):用圆括号保存匹配片段,可通过编号或名称取回。
- 锚点(anchor):
^、$、边界等限制匹配位置的符号;整串校验通常使用 API 的matches语义。
白话解释与边界
正则像一张“文本筛子”:字符类决定允许什么字符,量词决定重复几次,分组决定要不要把片段记下来。Java 字符串本身也使用反斜杠转义,所以正则里的 \d 通常要写成 Java 字符串 "\\d"。先在小样本上写测试,再把模式编译成 Pattern 重复使用。
正则适合邮箱片段、编号、日志字段等平面文本;HTML、JSON、SQL 或带递归嵌套的语言应使用专门解析器。来源不可信的模式可能造成回溯开销,服务端要限制输入长度、模式来源和执行时间。
常用用法
Pattern.compile:编译可复用模式
编译会检查正则语法并生成可复用对象;循环或请求处理中不要每次都重复编译同一个固定模式。
import java.util.regex.Pattern;
Pattern orderId = Pattern.compile("ORD-\\d{4}");
// 初始状态:orderId 当前为 Pattern.compile("ORD-\\d{4}")。
// 返回:Pattern.compile("ORD-\\d{4}") 创建可复用模式,matcher("ORD-2026").matches() 为 true。
// 作用:编译会检查正则语法并生成可复用对象。
System.out.println(orderId.matcher("ORD-2026").matches());
// 输出:trueString.matches:校验整个字符串
String.matches 要求整个字符串匹配,不是“只要包含一段即可”;一次性校验可以使用它,重复匹配则优先缓存 Pattern。
boolean valid = "user_42".matches("[a-z]+_\\d+");
// 初始状态:valid 当前为 "user_42".matches("[a-z]+_\\d+")。
// 返回:"user_42".matches("[a-z]+_\\d+") 整串匹配成功,valid 为 true。
// 作用:String.matches 要求整个字符串匹配,不是“只要包含一段即可”。
System.out.println(valid);
// 输出:trueMatcher.matches:按完整模式校验
它与 String.matches 的范围语义相同,但能继续使用同一个 Matcher 做捕获和定位;模式只描述格式,不自动验证月份天数的真实日历合法性。
import java.util.regex.Pattern;
Pattern date = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
// 初始状态:date 当前为 Pattern.compile("\\d{4}-\\d{2}-\\d{2}")。
System.out.println(date.matcher("2026-09-27").matches());
// 输出:true
// 作用:它与 String.matches 的范围语义相同,但能继续使用同一个 Matcher 做捕获和定位。Matcher.find:查找任意匹配片段
find 每次从上次结束位置继续寻找,适合提取一段文本中的多个片段;它不要求剩余文本也符合模式。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Matcher matcher = Pattern.compile("#[a-z]+").matcher("标签 #java 和 #api");
// 初始状态:matcher 当前为 Pattern.compile("#[a-z]+").matcher("标签 #java 和 #api")。
if (matcher.find()) {
// 关键变化:matcher.find() 第一次找到片段 "#java",匹配位置推进到该片段末尾。
System.out.println(matcher.group());
// 输出:#java
}
if (matcher.find()) {
// 关键变化:matcher.find() 第二次从上次位置继续找到片段 "#api"。
System.out.println(matcher.group());
// 输出:#api
}Matcher.lookingAt:匹配文本开头
lookingAt 只要求从当前区域起点开始匹配,适合判断前缀;它与 find 和完整校验的意图不同。
Matcher matcher = java.util.regex.Pattern.compile("https?://").matcher("https://example.com");
// 初始状态:matcher 当前为 java.util.regex.Pattern.compile("https?://").matcher("https://example.com")。
System.out.println(matcher.lookingAt());
// 输出:true
// 作用:lookingAt 只要求从当前区域起点开始匹配,适合判断前缀。Matcher.group:读取捕获组
group(0) 是完整匹配,group(1) 起按左括号顺序编号;可选组未匹配时返回 null,读取前要判断匹配状态。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Matcher matcher = Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026");
// 初始状态:matcher 当前为 Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026")。
if (matcher.matches()) {
System.out.println(matcher.group(1) + ", " + matcher.group(2));
// 输出:ORD, 2026
// 作用:group(0) 是完整匹配,group(1) 起按左括号顺序编号。
}groupCount:查看捕获组数量
需要在匹配成功后确认正则包含多少个捕获组时使用 groupCount,整个匹配本身不计入数量。
var matcher = java.util.regex.Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026");
// 初始状态:matcher 当前为 java.util.regex.Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026")。
matcher.find();
// 关键变化:matcher.find() 找到完整文本 "ORD-2026",后续 groupCount 可读取两个捕获组。
System.out.println(matcher.groupCount());
// 输出:2
// 作用:需要在匹配成功后确认正则包含多少个捕获组时使用 groupCount,整个匹配本身不计入数量。start:读取捕获组起始位置
需要读取某个捕获组在原文本中的起始索引时使用 start,并且必须先让匹配操作成功。
var matcher = java.util.regex.Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026");
// 初始状态:matcher 当前为 java.util.regex.Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026")。
matcher.find();
// 关键变化:matcher.find() 匹配 "ORD-2026",捕获组 2 对应 "2026"。
System.out.println(matcher.start(2));
// 输出:4
// 作用:需要读取某个捕获组在原文本中的起始索引时使用 start,并且必须先让匹配操作成功。end:读取捕获组结束位置
需要读取某个捕获组左闭右开区间的结束索引时使用 end,并且必须先让匹配操作成功。
var matcher = java.util.regex.Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026");
// 初始状态:matcher 当前为 java.util.regex.Pattern.compile("([A-Z]+)-(\\d+)").matcher("ORD-2026")。
matcher.find();
// 关键变化:matcher.find() 匹配成功,捕获组 2 的文本范围已可用。
System.out.println(matcher.end(2));
// 输出:8
// 作用:需要读取某个捕获组左闭右开区间的结束索引时使用 end,并且必须先让匹配操作成功。命名捕获组:用名称读取字段
命名组用 (?<name>...) 声明,适合字段较多的模式;名称必须符合规则且不能重复,替换模板中可用 ${name} 引用。
// 语义:命名组用 (?<name>...) 声明,适合字段较多的模式。
// 初始状态:matcher 初始为 Pattern.compile("(?<kind>[A-Z]+)-(?<number>\\d+)").matcher("ORD-2026")。
import java.util.regex.Pattern;
var matcher = Pattern.compile("(?<kind>[A-Z]+)-(?<number>\\d+)").matcher("ORD-2026");
// 关键变化:matcher 接收表达式 Pattern.compile("(?<kind>[A-Z]+)-(?<number>\\d+)").matcher("ORD-2026") 的计算结果。
// 初始状态:matcher 的初始值为 Pattern.compile("(?<kind>[A-Z]+)-(?<number>\\d+)").matcher("ORD-2026")。
if (matcher.matches()) {
System.out.println(matcher.group("kind") + ", " + matcher.group("number"));
// 输出:ORD, 2026
}replaceAll:替换所有匹配片段
replaceAll 的第一个参数是正则,第二个参数中的 $1、${name} 有组引用含义;需要替换字面文本时用 String.replace。
String text = "电话 138-0000-0000,备用 139-1111-1111";
// 初始状态:text 当前为 "电话 138-0000-0000,备用 139-1111-1111"。
String masked = text.replaceAll("1\\d{2}-\\d{4}-\\d{4}", "***");
// 初始状态:masked 当前为 text.replaceAll("1\\d{2}-\\d{4}-\\d{4}", "***")。
// 返回:replaceAll 将两个手机号片段分别替换为 ***,masked 为 "电话 ***,备用 ***"。
// 作用:replaceAll 的第一个参数是正则,第二个参数中的 $1、${name} 有组引用含义。
System.out.println(masked);
// 输出:电话 ***,备用 ***replaceFirst:只替换首个匹配
只替换第一个正则片段,仍要注意替换字符串的 $ 和反斜杠转义;不需要正则时使用字面替换更清晰。
String text = "A12-B34-C56";
// 初始状态:text 当前为 "A12-B34-C56"。
String replaced = text.replaceFirst("\\d+", "N");
// 初始状态:replaced 当前为 text.replaceFirst("\\d+", "N")。
// 返回:replaceFirst 只把首个数字片段 "12" 换成 "N",replaced 为 "AN-B34-C56"。
// 作用:只替换第一个正则片段,仍要注意替换字符串的 $ 和反斜杠转义。
System.out.println(replaced);
// 输出:AN-B34-C56Matcher.replaceAll:按捕获组重排文本
在已编译模式上执行替换,适合连续处理;模板里的 $1 代表第一个捕获组,不是普通美元字符。
import java.util.regex.Pattern;
String text = "Java,2026";
// 初始状态:text 当前为 "Java,2026"。
String reordered = Pattern.compile("(\\w+),(\\d+)")
.matcher(text)
.replaceAll("$2-$1");
// 返回:按捕获组把 "Java,2026" 重排为 "2026-Java"。
// 作用:在已编译模式上执行替换,适合连续处理。
// 关键变化:Pattern.compile("(\\w+),(\\d+)").matcher(text).replaceAll("$2-$1") 把 text="Java,2026" 重排,reordered 变为 "2026-Java"。
System.out.println(reordered);
// 输出:2026-Javasplit:用正则分割文本
分隔符是正则,竖线、点号、问号等元字符必须转义;负 limit 保留末尾空字段,默认 split(regex) 会丢弃它们。
import java.util.Arrays;
String[] fields = "a|b||".split("\\|", -1);
// 初始状态:fields 当前为 "a|b||".split("\\|", -1)。
// 返回:split("\\|", -1) 按竖线分割并保留末尾空字段,fields 为 ["a", "b", "", ""]。
// 作用:分隔符是正则,竖线、点号、问号等元字符必须转义。
System.out.println(Arrays.toString(fields));
// 输出:[a, b, , ]Pattern.quote:把用户文本当字面量
用户提供的关键词、分隔符或搜索字面量不要直接拼进正则;Pattern.quote 会把元字符按普通字符处理。
import java.util.regex.Pattern;
String separator = "a|b";
// 初始状态:separator 当前为 "a|b"。
String regex = Pattern.quote(separator);
// 初始状态:regex 当前为 Pattern.quote(separator)。
// 返回:Pattern.quote("a|b") 生成字面量模式,"a|b|c".split(regex) 的长度为 2。
// 作用:用户提供的关键词、分隔符或搜索字面量不要直接拼进正则。
System.out.println("a|b|c".split(regex).length);
// 输出:2Pattern.CASE_INSENSITIVE:忽略大小写匹配
编译标志可以集中表达匹配策略;跨语言大小写、Locale 或 Unicode 规则复杂时要专门测试,不要只凭 ASCII 样例判断。
import java.util.regex.Pattern;
Pattern keyword = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
// 初始状态:keyword 当前为 Pattern.compile("java", Pattern.CASE_INSENSITIVE)。
// 返回:带 CASE_INSENSITIVE 标志的模式匹配 "JAVA",matches() 返回 true。
// 作用:编译标志可以集中表达匹配策略。
System.out.println(keyword.matcher("JAVA").matches());
// 输出:true不常用但需要知道
Pattern.MULTILINE:让行锚点逐行生效
默认 ^、$ 更接近整个输入的边界;MULTILINE 让它们在换行处也能匹配每一行,仍要区分 . 是否跨行。
import java.util.regex.Pattern;
Pattern line = Pattern.compile("^ERROR:.*$", Pattern.MULTILINE);
// 初始状态:line 当前为 Pattern.compile("^ERROR:.*$", Pattern.MULTILINE)。
// 返回:MULTILINE 让第二行 "ERROR: failed" 匹配,find() 返回 true。
// 作用:通过 Pattern.MULTILINE 让行锚点逐行生效。
System.out.println(line.matcher("INFO: ok\nERROR: failed").find());
// 输出:true非捕获组 (?:...):分组但不保存
非捕获组可组织选择和量词,又不会占用捕获编号;不需要取值时优先使用它,减少组编号错位。
import java.util.regex.Pattern;
Pattern protocol = Pattern.compile("(?:http|https)://[a-z.]+");
// 初始状态:Pattern.compile("(?:http|https)://[a-z.]+") 编译协议和主机名模式,protocol 保存该 Pattern。
// 返回:protocol.matcher("https://example.com").matches() 完整匹配并返回 true。
// 作用:非捕获组 ?: 只分组不编号,匹配 http:// 或 https:// 前缀。
System.out.println(protocol.matcher("https://example.com").matches());
// 输出:truePattern.asPredicate:把模式转成谓词
返回的谓词使用完整匹配语义,适合 Stream 过滤;需要捕获组、位置或多次提取时仍应直接使用 Matcher。
import java.util.List;
import java.util.regex.Pattern;
Pattern number = Pattern.compile("\\d+");
// 初始状态:number 当前为 Pattern.compile("\\d+")。
long count = List.of("1", "x", "20").stream().filter(number.asPredicate()).count();
// 返回:number.asPredicate() 过滤出 "1" 与 "20" 两个数字文本,count 为 2。
// 作用:通过 Pattern.asPredicate 把模式转成谓词。
System.out.println(count);
// 输出:2Matcher.reset:复用 Matcher 对象
reset 会清除匹配状态并切换输入;只有在明确管理线程隔离和生命周期时才复用,Matcher 本身不是线程安全的。
import java.util.regex.Pattern;
var matcher = Pattern.compile("\\d+").matcher("A1");
// 初始状态:matcher 当前为 Pattern.compile("\\d+").matcher("A1")。
matcher.find();
// 关键变化:matcher.find() 在 "A1" 中找到数字 "1",随后 reset 改用新输入。
matcher.reset("B22");
// 关键变化:matcher.reset("B22") 保留正则并把输入替换为 "B22",匹配位置重置到开头。
// 作用:通过 Matcher.reset 复用 Matcher 对象。
System.out.println(matcher.find() + ", " + matcher.group());
// 输出:true, 22Matcher.usePattern:在同一输入上切换模式
切换模式会清除当前匹配状态;多数代码为每个意图创建清晰的 Matcher 更易读,不要为了少一个对象而牺牲边界。
import java.util.regex.Pattern;
var matcher = Pattern.compile("[A-Z]+").matcher("ABC-123");
// 初始状态:matcher 当前为 Pattern.compile("[A-Z]+").matcher("ABC-123")。
matcher.find();
// 关键变化:matcher.find() 先匹配输入前缀 "ABC",当前位置停在连字符前。
matcher.usePattern(Pattern.compile("\\d+"));
// 关键变化:matcher.usePattern(Pattern.compile("\\d+")) 将同一个 Matcher 的模式换成数字模式。
// 作用:通过 Matcher.usePattern 在同一输入上切换模式。
matcher.reset();
// 关键变化:matcher.reset() 将扫描位置重置到输入开头,新的数字模式可继续查找。
System.out.println(matcher.find() + ", " + matcher.group());
// 输出:true, 123Pattern.CANON_EQ:规范等价匹配(低频)
规范等价匹配可能增加开销,通常先统一 Unicode 规范化形式再比较;只有协议明确要求时才启用。
import java.util.regex.Pattern;
Pattern pattern = Pattern.compile("é", Pattern.CANON_EQ);
// 初始状态:pattern 当前为 Pattern.compile("é", Pattern.CANON_EQ)。
// 作用:通过 Pattern.CANON_EQ 规范等价匹配(低频)。
System.out.println(pattern.matcher("e\u0301").matches());
// 输出:trueMatcher.hitEnd:判断匹配器是否触及输入末尾
适合流式输入或增量解析场景,普通表单校验不需要依赖这些状态;结果必须结合业务协议解释。
import java.util.regex.Pattern;
var matcher = Pattern.compile("abc$").matcher("ab");
// 初始状态:matcher 当前为 Pattern.compile("abc$").matcher("ab")。
boolean found = matcher.find();
System.out.println("found=" + found + ", hitEnd=" + matcher.hitEnd());
// 输出:found=false, hitEnd=true
// 作用:通过 Matcher.hitEnd 判断匹配器是否触及输入末尾。Matcher.requireEnd:判断成功匹配是否依赖输入末尾
requireEnd 只在已找到匹配后判断追加输入是否可能改变结果,应与 hitEnd 分别理解。
import java.util.regex.Pattern;
var matcher = Pattern.compile("abc$").matcher("abc");
// 初始状态:matcher 匹配 "abc",find() 成功后 requireEnd() 返回 true。
System.out.println("find=" + matcher.find());
// 输出:find=true
System.out.println("requireEnd=" + matcher.requireEnd());
// 输出:requireEnd=true
// 作用:模式 "abc$" 的成功依赖当前输入在 c 后立即结束。简单案例
Pattern.matcher:为具体输入创建匹配器
编译后的 Pattern 可复用,但每段输入应获得自己的 Matcher 状态对象。
import java.util.regex.Pattern;
class RegexCase { }
Pattern digits = Pattern.compile("\\d+");
// 初始状态:digits 当前为 Pattern.compile("\\d+")。
var matcher = digits.matcher("id=42");
// 返回:digits.matcher("id=42") 创建绑定输入的 Matcher,find() 可以找到 "42"。
// 作用:通过 Pattern.matcher 为具体输入创建匹配器。
System.out.println(matcher.find());
// 输出:true
System.out.println(matcher.group());
// 输出:42Matcher.appendReplacement:追加匹配前文本和当前替换值
逐个匹配并动态生成替换值时使用;它会把上次匹配末尾到当前匹配开头的文本以及本次替换值写入缓冲区。
import java.util.regex.Pattern;
var matcher = Pattern.compile("\\d+").matcher("id=7; id=9");
// 初始状态:matcher 绑定文本 "id=7; id=9",后续两次 find() 将依次匹配 "7" 和 "9"。
var out = new StringBuffer();
// 初始状态:out 为空,matcher 将依次找到 "7" 和 "9"。
while (matcher.find()) {
matcher.appendReplacement(out, "[" + matcher.group() + "]");
// 关键变化:appendReplacement 首次把 7 替为 [7],第二次把 9 替为 [9],out 最终为 "id=[7]; id=[9]"。
}
System.out.println(out);
// 输出:id=[7]; id=[9]Matcher.quoteReplacement:将动态值按字面量替换
动态替换值含 $ 或反斜杠时使用,避免 appendReplacement 把这些字符解释为捕获组引用或转义语法。
import java.util.regex.Pattern;
var matcher = Pattern.compile("\\{user}").matcher("pay {user}");
// 初始状态:matcher 在 "pay {user}" 中等待匹配,动态替换文本为 "A$1"。
var out = new StringBuffer();
if (matcher.find()) {
matcher.appendReplacement(out, Matcher.quoteReplacement("A$1"));
// 关键变化:Matcher.quoteReplacement("A$1") 保留美元符号字面值,appendReplacement 后 out 为 "pay A$1"。
}
matcher.appendTail(out);
// 关键变化:matcher.appendTail(out) 补齐最后一次匹配后的尾部文本;此例尾部为空,out 仍为 "pay A$1"。
System.out.println(out);
// 输出:pay A$1Matcher.appendTail:追加末次匹配后的剩余文本
替换循环结束后使用,把最后一次匹配之后仍未处理的尾部文本追加到缓冲区。
import java.util.regex.Pattern;
var matcher = Pattern.compile("\\d+").matcher("id=7; status=ok");
// 初始状态:matcher 是 Pattern.compile("\\d+").matcher("id=7; status=ok") 的匹配器,将从该文本中查找数字 7。
var out = new StringBuffer();
if (matcher.find()) {
matcher.appendReplacement(out, "[7]");
// 关键变化:matcher.appendReplacement(out, "[7]") 将 7 替换为 [7],out 暂存 "id=[7]"。
}
matcher.appendTail(out);
// 关键变化:matcher.appendTail(out) 追加未匹配尾部 "; status=ok",out 变为 "id=[7]; status=ok"。
System.out.println(out);
// 输出:id=[7]; status=ok易混点
matches要求整串匹配,find只找任意片段,lookingAt只从当前位置开头匹配。- Java 字符串和正则各有一层反斜杠转义;看到正则
\d时,Java 源码通常要写"\\d"。 replace是字面替换,replaceAll/replaceFirst是正则替换;用户输入要先用Pattern.quote。- 捕获组编号按左括号顺序变化,复杂模式优先使用命名组或非捕获组。
- 正则不能可靠解析任意嵌套 JSON、HTML 或 SQL;输入越长、回溯越复杂,资源风险越高。
课后小问
为什么
"abc123".matches("\\d+")返回false,而find可以找到123? 答案:matches要求整个字符串都由数字组成,find只需要在字符串中找到一段数字。 解析:选择 API 前先确定是整串校验还是局部提取;把二者混用会让校验过宽或过窄。用户输入的关键词是
a.b,直接拼接成正则有什么风险? 答案:.在正则中表示任意字符,直接拼接会扩大匹配范围;应使用Pattern.quote(keyword)把它作为字面量。 解析:搜索条件来自外部时还要限制长度和模式复杂度,避免意外匹配或回溯资源消耗。
本节小结
Pattern保存编译后的规则,Matcher在具体输入上执行匹配。matches、find、lookingAt分别对应整串、局部和前缀语义。- 捕获组、命名组和位置 API 让正则不仅能判断,还能提取结构化字段。
- 字面替换使用
replace,正则替换使用replaceAll;外部文本先quote。
快速回顾
- 能解释字符类、量词、分组、锚点和 Java 反斜杠转义。
- 能为固定模式选择
String.matches或复用Pattern。 - 能用
find、group、start、end提取多条记录。 - 能识别正则解析嵌套语言和恶意回溯的边界。
阿源的知识库