Skip to content

正则表达式语法:查表就像查字典

正则语法看起来符号一堆,其实拆解开来就四类:字符、量词、边界、分组

这一节不废话,直接给你一张表,查完就能用。


字符类:指定匹配哪些字符

精确指定 [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, 9a, _
\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, aaaaa
{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());
    }
}

边界匹配:指定位置

边界符号表

符号含义示例匹配不匹配
^行/字符串开头^abcabc123123abc
$行/字符串结尾abc$123abcabc123
\b单词边界\bword\ba word bsword, worda
\B非单词边界\Bwordsworda 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

基于 VitePress 构建