正则表达式语法:查表就像查字典
正则语法看起来符号一堆,其实拆解开来就四类:字符、量词、边界、分组。
这一节不废话,直接给你一张表,查完就能用。
字符类:指定匹配哪些字符
精确指定 [abc]
java
public class CharClassDemo {
public static void main(String[] args) {
// [abc] - 匹配 a、b 或 c 中的任意一个
System.out.println("[abc] 匹配 a: " + "a".matches("[abc]")); // true
System.out.println("[abc] 匹配 d: " + "d".matches("[abc]")); // false
// [^abc] - 匹配除了 a、b、c 以外的所有字符
System.out.println("[^abc] 匹配 d: " + "d".matches("[^abc]")); // true
// [a-z] - 匹配 a 到 z 的任意小写字母
System.out.println("[a-z] 匹配 m: " + "m".matches("[a-z]")); // true
System.out.println("[a-z] 匹配 A: " + "A".matches("[a-z]")); // false
// [A-Za-z] - 匹配所有英文字母(大小写都行)
System.out.println("[A-Za-z] 匹配 Z: " + "Z".matches("[A-Za-z]")); // true
// [0-9] - 匹配数字
System.out.println("[0-9] 匹配 5: " + "5".matches("[0-9]")); // true
}
}预定义字符类:常用缩写
| 写法 | 含义 | 等价于 | 匹配示例 | 不匹配示例 |
|---|---|---|---|---|
\d | 数字 | [0-9] | 0, 5, 9 | a, _ |
\D | 非数字 | [^0-9] | a, Z, _ | 0, 5 |
\w | 单词字符 | [a-zA-Z0-9_] | a, Z, 5, _ | -, @ |
\W | 非单词字符 | [^a-zA-Z0-9_] | -, @, # | a, 5 |
\s | 空白字符 | [ \t\n\x0B\f\r] | 空格, Tab, 换行 | a, _ |
\S | 非空白字符 | [^\s] | a, Z, - | 空格 |
. | 任意字符 | [^\n\r] | a, 5, - | 换行 |
实战技巧:
java
public class CharClassTrick {
public static void main(String[] args) {
// 匹配十六进制字符(0-9, a-f, A-F)
System.out.println("十六进制: " + "B".matches("[0-9a-fA-F]")); // true
// 匹配中文(Unicode范围)
System.out.println("中文: " + "中".matches("[\\u4e00-\\u9fa5]")); // true
// 匹配字母或数字
System.out.println("字母数字: " + "a5".matches("[a-zA-Z0-9]")); // false(注意:这是单个字符)
System.out.println("字母数字: " + "a5".matches("[a-zA-Z0-9]+")); // true(+表示多个)
}
}量词:指定字符出现多少次
量词一览表
| 量词 | 含义 | 贪婪 | 惰性 | 匹配 "aaa" | 不匹配 |
|---|---|---|---|---|---|
* | 零个或多个 | 贪婪 | *? | aaa, "" | - |
+ | 一个或多个 | 贪婪 | +? | aaa | "" |
? | 零个或一个 | 贪婪 | ?? | a, "" | aa |
{n} | 恰好 n 个 | - | - | aa (n=2) | a, aaa |
{n,} | 至少 n 个 | - | - | aaa, aaaa | a |
{n,m} | n 到 m 个 | 贪婪 | {n,m}? | aaa (n=2,m=4) | a, aaaaa |
贪婪 vs 惰性:关键区别
这是正则里最容易踩坑的地方。
java
public class GreedyVsLazy {
public static void main(String[] args) {
String html = "<tag>content</tag>";
// ❌ 贪婪模式:`.+` 会尽可能多地匹配
// 匹配过程:`<` → 吃掉所有字符 → 遇到 `$` → 回溯找 `>`
// 结果:匹配了整段 `<tag>content</tag>`
System.out.println("贪婪: " + html.matches("<.+>")); // false(因为后面没有字符了)
// 看看 find() 的效果
String text = "hello <tag>content</tag> world";
System.out.println("贪婪 find: " + Pattern.compile("<.+>").matcher(text).find());
// 输出: true,匹配到的是 `<tag>content</tag>` 而不是 `<tag>`
// ✅ 惰性模式:`.+?` 尽可能少地匹配
// 每次只吃一个字符,然后检查是否满足条件
System.out.println("惰性 find: " + Pattern.compile("<.+?>").matcher(text).find());
// 输出: true,匹配到的是 `<tag>`
}
}记忆口诀:贪婪是"能吃多少吃多少",惰性是"先尝尝再说"。
什么时候用惰性?
java
public class WhenUseLazy {
public static void main(String[] args) {
// 场景1:匹配 HTML 标签内容
String html = "<div>第一段</div><div>第二段</div>";
// ❌ 贪婪:会匹配从第一个 <div> 到最后一个 </div>
Pattern greedy = Pattern.compile("<div>.+</div>");
System.out.println("贪婪: " + greedy.matcher(html).find());
// 结果: true,匹配到的是 `<div>第一段</div><div>第二段</div>`
// ✅ 惰性:匹配一个标签对
Pattern lazy = Pattern.compile("<div>.+?</div>");
Matcher m = lazy.matcher(html);
while (m.find()) {
System.out.println("惰性匹配: " + m.group());
}
// 输出: <div>第一段</div> 和 <div>第二段</div>
// 场景2:匹配引号内的内容
String quote = "他说'你好',我说'再见'";
// ❌ 贪婪:匹配 `'你好',我说'再见'`
System.out.println("贪婪引号: " + Pattern.compile("'.+'").matcher(quote).find());
// ✅ 惰性:匹配 `'你好'`
System.out.println("惰性引号: " + Pattern.compile("'.+?'").matcher(quote).find());
}
}边界匹配:指定位置
边界符号表
| 符号 | 含义 | 示例 | 匹配 | 不匹配 |
|---|---|---|---|---|
^ | 行/字符串开头 | ^abc | abc123 | 123abc |
$ | 行/字符串结尾 | abc$ | 123abc | abc123 |
\b | 单词边界 | \bword\b | a word b | sword, worda |
\B | 非单词边界 | \Bword | sword | a word |
实战演示
java
public class BoundaryDemo {
public static void main(String[] args) {
// ^ 和 $ 的作用
String phone = "13812345678";
// ❌ 常见错误:没有锚点
System.out.println("误判: " + phone.matches("1[3-9]\\d{9}")); // true(但不够严格)
// ✅ 正确:加锚点
System.out.println("正确验证: " + phone.matches("^1[3-9]\\d{9}$")); // true
// 错误号码也通过了?不加锚点,字符串中间的数字也会被匹配
String badPhone = "x13812345678x";
System.out.println("误判坏号码: " + badPhone.matches("1[3-9]\\d{9}")); // true!错误!
System.out.println("正确拒绝: " + badPhone.matches("^1[3-9]\\d{9}$")); // false!正确!
// \b 单词边界:精确匹配单词
String text = "a sword and a word";
System.out.println("完整单词: " + text.matches(".*\\bsword\\b.*")); // true(sword 是完整单词)
System.out.println("完整单词: " + text.matches(".*\\bword\\b.*")); // true(word 是完整单词)
System.out.println("部分匹配: " + text.matches(".*sword.*")); // true(sword 是子串)
}
}分组:捕获和复用
普通分组 (...)
java
public class GroupDemo {
public static void main(String[] args) {
String date = "2026-03-22";
// 用 () 分组
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher(date);
if (m.matches()) {
System.out.println("全部: " + m.group(0)); // 2026-03-22(整个匹配)
System.out.println("年: " + m.group(1)); // 2026
System.out.println("月: " + m.group(2)); // 03
System.out.println("日: " + m.group(3)); // 22
}
// 分组编号按左括号出现顺序
String complex = "abc123def";
Pattern cp = Pattern.compile("(a(b(c)))(\\d+)");
Matcher cm = cp.matcher(complex);
if (cm.matches()) {
System.out.println("组1 (abc): " + cm.group(1)); // abc
System.out.println("组2 (bc): " + cm.group(2)); // bc
System.out.println("组3 (c): " + cm.group(3)); // c
System.out.println("组4 (123): " + cm.group(4)); // 123
}
}
}命名分组 (?<name>...)
java
public class NamedGroupDemo {
public static void main(String[] args) {
String date = "2026-03-22";
// 语法:(?<name>pattern)
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher(date);
if (m.matches()) {
System.out.println("年: " + m.group("year")); // 2026
System.out.println("月: " + m.group("month")); // 03
System.out.println("日: " + m.group("day")); // 22
}
}
}反向引用 \1 \2
匹配"重复出现"的模式,比如回文或者配对。
java
public class BackReferenceDemo {
public static void main(String[] args) {
// 匹配引号内的内容(支持单引号或双引号)
// 第一个括号捕获引号类型,第二个括号捕获内容
String quoted = "他说:'你好',然后说:'再见'";
Pattern p = Pattern.compile("(['\"])(.*?)\\1");
Matcher m = p.matcher(quoted);
while (m.find()) {
System.out.println("引号类型: " + m.group(1) + ", 内容: " + m.group(2));
}
// 输出: 引号类型: ', 内容: 你好
// 引号类型: ', 内容: 再见
// 匹配重复单词
String repeated = "hello hello world";
System.out.println("重复单词: " + repeated.matches("\\b(\\w+) \\1\\b")); // true
System.out.println("重复单词: " + "hello world".matches("\\b(\\w+) \\1\\b")); // false
}
}非捕获组 (?:...)
只分组但不捕获,省内存。
java
public class NonCaptureDemo {
public static void main(String[] args) {
// 场景:提取域名
String email = "user@example.com";
// ❌ 捕获组:多占用了 group(1)
Pattern capture = Pattern.compile("(\\w+)@(\\w+\\.\\w+)");
// ✅ 非捕获组:不需要捕获用户名部分
Pattern nonCapture = Pattern.compile("\\w+@(\\w+\\.\\w+)");
Matcher m = nonCapture.matcher(email);
if (m.matches()) {
System.out.println("域名: " + m.group(1)); // example.com(只有一组)
}
// 常用场景:或运算结合非捕获组
String text = "2026-03-22 或 2026/03/22";
// 不需要捕获分隔符
Pattern datePattern = Pattern.compile("\\d{4}([-/])\\d{2}\\1\\d{2}");
System.out.println("日期匹配: " + datePattern.matcher(text).find());
// 匹配 2026-03-22(分隔符一致),不匹配 2026/03-22(分隔符不一致)
}
}或运算 |
java
public class AlternationDemo {
public static void main(String[] args) {
// 简单或运算
System.out.println("cat: " + "cat".matches("cat|dog|bird")); // true
System.out.println("dog: " + "dog".matches("cat|dog|bird")); // true
// 带分组的或运算
System.out.println("cat123: " + "cat123".matches("cat(\\d+)?")); // true
System.out.println("dog123: " + "dog123".matches("cat(\\d+)?")); // false
// 优先级陷阱
System.out.println("abd: " + "abd".matches("a(b|c)d")); // true
System.out.println("acd: " + "acd".matches("a(b|c)d")); // false(匹配的是 abd,不是 acd)
// 实际场景:提取图片扩展名
String file = "photo.jpg";
System.out.println("图片: " + file.matches(".+\\.(jpg|png|gif|bmp)")); // true
}
}总结速查表
字符类
─────────────────────────────────────────
[abc] 匹配 a、b、c 之一
[^abc] 匹配除 a、b、c 以外
[a-z] 匹配 a 到 z
\d 匹配数字 [0-9]
\w 匹配单词 [a-zA-Z0-9_]
\s 匹配空白
. 匹配任意字符(除换行)
量词
─────────────────────────────────────────
* 0 个或多个
+ 1 个或多个
? 0 个或 1 个
{n} 恰好 n 个
{n,} 至少 n 个
{n,m} n 到 m 个
*? +? ?? 惰性版本
边界
─────────────────────────────────────────
^ 开头
$ 结尾
\b 单词边界
\B 非单词边界
分组
─────────────────────────────────────────
(...) 捕获组
(?<name>..) 命名捕获
(?:...) 非捕获组
\1 \2 反向引用
| 或运算下一节我们来看如何使用 Pattern 和 Matcher:Pattern 和 Matcher
