Why this matters

正则表达式很容易学成“查表式记忆”:看到 *、+、?、[]、() 就去翻文档,但一到真实场景里写校验、提取和替换规则,还是容易乱。

更有效的方式不是背更多符号,而是先建立一套读法:这条表达式到底在描述什么范围、重复次数、边界和复用关系。只要这套顺序稳定了,正则就不会只剩下玄学试错。

Regex is easier when you split it into four layers

Java 正则最常见的内容,其实可以压成四层:

  • 字符本身和字符类:到底匹配哪些字符。
  • 量词:前一个模式允许重复多少次。
  • 分组与捕获:哪些片段需要被整体看待或复用。
  • 边界与断言:匹配发生在什么位置,而不是匹配什么字符。

按这个顺序读,比从左到右死抠每个符号更容易稳定理解。

Quantifiers define how many times a pattern may appear

最常见的量词是:

  • *:前一个模式出现 0 次或多次。
  • +:前一个模式出现 1 次或多次。
  • ?:前一个模式出现 0 次或 1 次。
  • {n}:恰好出现 n 次。
  • {n,}:至少出现 n 次。
  • {n,m}:出现 n 到 m 次。

例如:

Java
Pattern.compile("\\d{3}(\\d)?");

它表达的是:先匹配 3 个数字,然后可选地再多一个数字。比起直接盯着反斜杠和大括号,更重要的是先把它翻译成一句人话。

Character classes decide the matching universe

字符类负责定义“可接受的候选字符集合”。最常见的写法包括:

  • [abc]:匹配 a、b、c 之一。
  • [^abc]:匹配除了 a、b、c 之外的任意字符。
  • [A-Z]:匹配一个大写英文字母。
  • \\d / \\D:数字 / 非数字。
  • \\w / \\W:单词字符 / 非单词字符。
  • \\s / \\S:空白字符 / 非空白字符。

这里一个很常见的 Java 细节是:字符串字面量里的反斜杠本身还要再转义一次。

所以正则里想写一个 \d,在 Java 字符串里必须写成 "\\d"。这不是正则本身复杂,而是 Java 字符串字面量先参与了一次解析。

Grouping and backreferences are what make regex reusable

分组最直观的作用,是把一段子模式当成整体来看:

Java
(\d)(\d)\2\1

这条规则表示:

  1. 先捕获第一位数字。
  2. 再捕获第二位数字。
  3. 第三位必须重复第二组。
  4. 第四位必须重复第一组。

因此它能匹配像 1221、3443 这样的四位串。

这里的关键不是“会不会写反向引用”,而是理解:捕获组把前面已经匹配到的内容存了下来,后续模式可以直接复用它。

在 Java 里,这也常见于替换操作:

Java
String str = "11333323467";
Pattern pattern = Pattern.compile("(\\d)\\1+");
Matcher matcher = pattern.matcher(str);
str = matcher.replaceAll("$1");

这段代码会把连续重复的数字压缩成一个,例如把 3333 压回成 3。

Greedy and reluctant matching only make sense with context

贪婪匹配和非贪婪匹配常被单独记忆,但真正实用的理解是:它们描述的是量词在存在多个可行结果时,优先拿多长的一段。

例如:

Java
Pattern.compile("a{3,4}");

面对 aaaaaa 时,默认会优先拿更长的那一段,因此第一次匹配通常是 aaaa。

而当量词后面加上 ?,例如:

  • +?
  • *?
  • {n,m}?

它就会变成非贪婪模式,优先尝试更短的结果。

这一点在处理 HTML 片段、日志切片或被分隔符包裹的内容时特别重要。否则你经常会得到“能匹配,但吃得太多”的结果。

Boundaries and assertions describe position, not characters

正则里还有一类很容易忽略的内容:它们并不匹配字符,而是描述匹配位置。

最常见的包括:

  • ^:行首。
  • $:行尾。
  • \\b:单词边界。
  • \\B:非单词边界。
  • (?=pattern):正向前瞻。
  • (?!pattern):负向前瞻。

例如:

Java
Pattern.compile("^(http[s]?://)([\\w-]+\\.)+([\\w]){2,}(:\\d{1,5})?(/.*)?$");

这类 URL 校验表达式之所以长,不是因为规则神秘,而是它同时在描述:

  • 开头必须是什么协议。
  • 中间主机名由哪些片段组成。
  • 端口是不是可选。
  • 路径是不是可选。
  • 整体必须从头匹配到尾。

如果你能把它拆回这些约束,长表达式就不再可怕。

Practical examples are where regex becomes trustworthy

原始笔记里比较典型的几个场景,其实都很适合作为正则训练:

  • 匹配重复内容并替换,例如口吃文本清洗。
  • 验证汉字、邮箱、整数或小数格式。
  • 编写 URL 校验规则。
  • 控制大小写敏感性,例如 (?i)。

这类例子有一个共同点:它们不是为了展示“这个符号也存在”,而是让你检查表达式是否真的和业务约束一致。

如果一条正则写完后,你没法用一句自然语言说明它的约束,那它大概率还不够稳。

Practical checks

如果你在项目里排查一条复杂正则,建议按这个顺序做:

  • 先确认字符类到底放宽或收紧了什么输入范围。
  • 再确认量词是不是把匹配长度放得过大。
  • 然后看有没有用分组或反向引用表达“前后必须一致”这类约束。
  • 最后再看边界和前瞻,确认匹配发生的位置是否正确。

不要一上来就整体读完整条表达式,那样最容易丢掉结构。

Closing notes

Java 正则真正难的不是语法数量,而是没有建立稳定的拆解顺序。只要你把表达式拆成字符类、量词、分组和边界四层去看,很多原本像乱码的规则都会开始变得可解释。

等你能稳定解释一条正则为什么这样写、为什么会匹配这么长、为什么替换能引用前面的分组时,正则就已经从“试出来的技巧”变成“可维护的规则”了。

感谢读到这里

日志还会继续,欢迎常来。

返回首页