Java 正则表达式入门:从元字符到反向引用的实用读法
systems, engineering
systems, engineering
正则表达式很容易学成“查表式记忆”:看到 *、+、?、[]、() 就去翻文档,但一到真实场景里写校验、提取和替换规则,还是容易乱。
更有效的方式不是背更多符号,而是先建立一套读法:这条表达式到底在描述什么范围、重复次数、边界和复用关系。只要这套顺序稳定了,正则就不会只剩下玄学试错。
Java 正则最常见的内容,其实可以压成四层:
按这个顺序读,比从左到右死抠每个符号更容易稳定理解。
最常见的量词是:
*:前一个模式出现 0 次或多次。+:前一个模式出现 1 次或多次。?:前一个模式出现 0 次或 1 次。{n}:恰好出现 n 次。{n,}:至少出现 n 次。{n,m}:出现 n 到 m 次。例如:
Pattern.compile("\\d{3}(\\d)?");
它表达的是:先匹配 3 个数字,然后可选地再多一个数字。比起直接盯着反斜杠和大括号,更重要的是先把它翻译成一句人话。
字符类负责定义“可接受的候选字符集合”。最常见的写法包括:
[abc]:匹配 a、b、c 之一。[^abc]:匹配除了 a、b、c 之外的任意字符。[A-Z]:匹配一个大写英文字母。\\d / \\D:数字 / 非数字。\\w / \\W:单词字符 / 非单词字符。\\s / \\S:空白字符 / 非空白字符。这里一个很常见的 Java 细节是:字符串字面量里的反斜杠本身还要再转义一次。
所以正则里想写一个 \d,在 Java 字符串里必须写成 "\\d"。这不是正则本身复杂,而是 Java 字符串字面量先参与了一次解析。
分组最直观的作用,是把一段子模式当成整体来看:
(\d)(\d)\2\1
这条规则表示:
因此它能匹配像 1221、3443 这样的四位串。
这里的关键不是“会不会写反向引用”,而是理解:捕获组把前面已经匹配到的内容存了下来,后续模式可以直接复用它。
在 Java 里,这也常见于替换操作:
String str = "11333323467";
Pattern pattern = Pattern.compile("(\\d)\\1+");
Matcher matcher = pattern.matcher(str);
str = matcher.replaceAll("$1");
这段代码会把连续重复的数字压缩成一个,例如把 3333 压回成 3。
贪婪匹配和非贪婪匹配常被单独记忆,但真正实用的理解是:它们描述的是量词在存在多个可行结果时,优先拿多长的一段。
例如:
Pattern.compile("a{3,4}");
面对 aaaaaa 时,默认会优先拿更长的那一段,因此第一次匹配通常是 aaaa。
而当量词后面加上 ?,例如:
+?*?{n,m}?它就会变成非贪婪模式,优先尝试更短的结果。
这一点在处理 HTML 片段、日志切片或被分隔符包裹的内容时特别重要。否则你经常会得到“能匹配,但吃得太多”的结果。
正则里还有一类很容易忽略的内容:它们并不匹配字符,而是描述匹配位置。
最常见的包括:
^:行首。$:行尾。\\b:单词边界。\\B:非单词边界。(?=pattern):正向前瞻。(?!pattern):负向前瞻。例如:
Pattern.compile("^(http[s]?://)([\\w-]+\\.)+([\\w]){2,}(:\\d{1,5})?(/.*)?$");
这类 URL 校验表达式之所以长,不是因为规则神秘,而是它同时在描述:
如果你能把它拆回这些约束,长表达式就不再可怕。
原始笔记里比较典型的几个场景,其实都很适合作为正则训练:
(?i)。这类例子有一个共同点:它们不是为了展示“这个符号也存在”,而是让你检查表达式是否真的和业务约束一致。
如果一条正则写完后,你没法用一句自然语言说明它的约束,那它大概率还不够稳。
如果你在项目里排查一条复杂正则,建议按这个顺序做:
不要一上来就整体读完整条表达式,那样最容易丢掉结构。
Java 正则真正难的不是语法数量,而是没有建立稳定的拆解顺序。只要你把表达式拆成字符类、量词、分组和边界四层去看,很多原本像乱码的规则都会开始变得可解释。
等你能稳定解释一条正则为什么这样写、为什么会匹配这么长、为什么替换能引用前面的分组时,正则就已经从“试出来的技巧”变成“可维护的规则”了。
日志还会继续,欢迎常来。
返回首页