Java 正则表达式入门:学会最常用的部分

用 Java 讲解正则的基本用法,字符类、量词、锚点、捕获组,以及 matches、find、group、replaceAll、split 五个方法,基本满足日常八成使用场景

阅读 -- 次 参与评论

正则表达式入门

我自己被正则表达式多次劝退过。一打开代码和教程,都是看不懂的\\d*[]$、结果就是入门了几年连门都没摸到

但其实日常写代码高频用到的部分,其实是很小的一块。下面这篇文章用 Java 语言作为例子讲解这部分。

每个例子输出都写在注释里,照着敲一遍能加深印象,原地入门。

第一关,Java 的转义

正则里表示”一个数字”要写 \d。但把这个字符串写进 Java 源码,要写成 "\\d"

Java 字符串有自己的一套转义规则,\n 是换行,\t 是制表符。Java 编译器会先读取字符串字面量,源码里的 \\ 经过这一层后,会变成字符串中的一个 \。所以 "\\d" 在程序运行时实际只有两个字符,一个反斜杠和一个字母 d。

随后,正则引擎才会读取这个字符串,并把 \d 识别成“一个数字”。如果在 Java 源码里直接写 "\d",编译器会把它当成 Java 的转义序列处理,但 Java 没有 \d 这种转义写法,代码会直接编译失败,正则引擎根本收不到它。

这也是新手第一次写 Java 正则时最容易困惑的地方。编辑器里看到的两个反斜杠属于 Java 源码,正则引擎最终收到的只有一个。记住这个处理顺序,遇到 \d\s\b 这类带反斜杠的正则写法时,就知道为什么要在 Java 字符串里写成 "\\d""\\s""\\b" 了。

先跑一个完整的例子,校验手机号。

import java.util.regex.Pattern;

public class PhoneCheck {

    public static void main(String[] args) {
        // 编译正则,手机号规则是 1 开头,第二位 3 到 9,后面再跟 9 位数字
        Pattern pattern = Pattern.compile("^1[3-9]\\d{9}$");

        // matches 要求整串都符合规则,返回布尔值
        System.out.println(pattern.matcher("13800138000").matches()); // true
        System.out.println(pattern.matcher("12800138000").matches()); // false,第二位是 2
        System.out.println(pattern.matcher("1380013800").matches());  // false,少一位
    }
}

现在分析一下这个例子。首先^$ 是锚点,一个在开头,一个在结尾,意思是匹配必须覆盖整串。中间的 1 是普通字符,只匹配数字 1。[3-9] 是字符类,表示这一位取 3 到 9 的任意一个。\d 代表任意数字,{9} 是量词,表示前面的数字出现 9 次。

手机号规则这里我做了简化,只限制第一位是 1、第二位是 3 到 9,剩下的交给位数约束。如果要按运营商号段逐条核对,规则会越写越长,还容易过时。先大致看懂就行。

第二关,认识三个积木

正则的大部分语法可以归为三样,分别是字符类、量词、锚点。先记住这三样,下面一个一个介绍。

首先是字符类,用中括号包起来。[0-9] 是数字,[a-zA-Z] 是英文字母,[^0-9] 是”不是数字”的任意字符,中括号开头的 ^ 表示取反。几个常用写法有现成缩写,\d 是数字,\w 是字母、数字、下划线,\s 是空白字符,包括空格、制表符和换行,. 是任意字符。

量词控制前面的东西出现几次。* 是零次或多次,+ 是一次或多次,? 是零次或一次,{n} 是恰好 n 次,{n,m} 是 n 到 m 次。锚点负责位置,除了开头结尾的 ^ $,还有个 \b 表示单词边界,中文文本里用处不大,先不用管。

光靠这三样,已经能写不少正经东西。比如从日志里把所有数字捞出来。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ExtractNumbers {

    public static void main(String[] args) {
        // 一段日志文本,目标是把里面所有数字都提取出来
        String text = "订单号 2026-0805,数量 3,金额 128.5 元";
        Pattern pattern = Pattern.compile("\\d+");

        // find 在文本里找子串,找到一次返回 true
        // 配合 while 循环可以把所有匹配挨个取出来
        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            // group 返回这一次匹配到的原文
            System.out.println(matcher.group());
        }
    }
}

输出是 2026080531285 五行。注意 128.5 被拆成了 1285,点号不属于 \d。想连小数点一起抓,正则改成 \d+(\.\d+)?,括号加问号的意思是”小数点加数字,这一整段可以不出现”。

第三关,四个操作

积木认识了,接下来看它们在 Java 里怎么用。Java 给正则准备了两类入口,一类在 java.util.regex 包里的 PatternMatcher,另一类是 String 类上直接就能调的方法。日常使用分四种情况。

校验用 matches。String.matchesPattern.matcher(...).matches() 效果一样,区别在 String.matches 每次调用都会重新编译正则,循环里反复用要先把 Pattern 编译好,这个习惯值得养成。

提取用 find 加 group。想把文本里的日期拆成年月日,用括号把各部分圈起来,group 就能按括号取。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CaptureDate {

    public static void main(String[] args) {
        // 把文本里所有日期拆成年、月、日三部分
        String text = "今天 2024-03-15 发货,明天 2024-04-01 到货";
        Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");

        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            // group(0) 是整段匹配,group(1) 开始对应各个括号,按从左到右数
            String year = matcher.group(1);   // 年
            String month = matcher.group(2);  // 月
            String day = matcher.group(3);    // 日
            System.out.println(year + "年" + month + "月" + day + "日");
        }
    }
}

输出两行,2024年03月15日2024年04月01日。括号的编号从左往右数,第几个左括号就是第几组,这个编号规则后面替换也要用。

替换用 replaceAll 和 replaceFirst。替换串里能用 $1$2 引用捕获组,把横线日期改成斜杠日期,一行代码。

public class ReformatDate {

    public static void main(String[] args) {
        String date = "2024-03-15";
        // $1 对应第一个括号,也就是年份,$2 是月份,$3 是日期
        String result = date.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$1/$2/$3");
        System.out.println(result); // 输出 2024/03/15
    }
}

切分用 split,分隔规则本身就是一个正则。连续空白想当成一个分隔符,就写 \\s+

public class SplitText {

    public static void main(String[] args) {
        String line = "苹果 香蕉\t橙子\n葡萄";
        // \\s+ 表示一个或多个空白字符,连续空白合并成一个分隔符
        String[] fruits = line.split("\\s+");
        for (String fruit : fruits) {
            System.out.println(fruit);
        }
    }
}

输出四行,每个水果一行。有个细节容易忽略,split 默认会丢掉结尾的空串,"a,b,".split(",") 只得到两个元素。想保留全部,给 split 传一个负数参数,"a,b,".split(",", -1) 得到三个。

第四关,两个高频坑

第一个坑是 matches 和 find 的区别。matches 要求整串都符合规则,find 只要求找到一段子串。"abc".matches("b") 返回 false,"abc" 里找 b 用 find 却能找到。判断字符串合不合法用 matches,从文本里捞内容用 find,两个各自的正则也要配不同的写法,matches 通常要加 ^$ 把边界钉死。

第二个坑是贪婪。量词默认都贪婪,能多吃就多吃。看一段 HTML。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class GreedyVsLazy {

    public static void main(String[] args) {
        String html = "<a>text</a>";

        // 默认贪婪,.+ 会一路吃到最后一个 >,把整段都吞进去
        Matcher greedy = Pattern.compile("<.+>").matcher(html);
        greedy.find();
        System.out.println(greedy.group()); // <a>text</a>

        // 量词后面加 ? 变懒惰,吃到第一个 > 就停
        Matcher lazy = Pattern.compile("<.+?>").matcher(html);
        lazy.find();
        System.out.println(lazy.group()); // <a>

        // 更稳的写法,用取反字符类,[^>] 表示"不是 > 的任意字符"
        Matcher negated = Pattern.compile("<[^>]+>").matcher(html);
        negated.find();
        System.out.println(negated.group()); // <a>
    }
}

三种写法,只有第一种会出错。想抓标签之间的文本,<[^>]+> 是实战里最常用的招,比写懒惰量词更不容易踩坑。

实战清单

把积木拼起来,几个最常见的需求基本都能直接套。

先说数据清洗。把多余空白合并成一个空格,再去掉首尾空白。

// 先把首尾空白去掉,再把中间连续的空白压成一个空格
String cleaned = "  a   b\t c  ".trim().replaceAll("\\s+", " ");
System.out.println(cleaned); // 输出 a b c

去掉 HTML 标签,只留下可见文字。

// [^>] 表示"不是 > 的字符",把 <...> 整体替换成空串
String plain = "<p>你好<strong>世界</strong></p>".replaceAll("<[^>]+>", "");
System.out.println(plain); // 输出 你好世界

给手机号打码,常见于日志脱敏。

// 把每个数字都替换成 #,比逐个字符判断省事
String masked = "电话 13800138000".replaceAll("\\d", "#");
System.out.println(masked); // 输出 电话 ###########

校验邮箱。下面这版是入门够用的简化写法,真实系统里最靠谱的验证是发一封确认邮件,正则写得再复杂,也拦不住用户把邮箱输错。

// 简化版邮箱规则,用户名部分允许字母数字和 . + -,域名部分类似
boolean ok = "tom@example.com".matches("^[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+$");
System.out.println(ok); // true

还有一条中文处理,Java 里 [\u4e00-\u9fa5] 能匹配一个汉字,判断整段都是中文写成 ^[\u4e00-\u9fa5]+$

收尾

这篇文章用到的语法,数得过来。字符类、量词、锚点、括号捕获,加上 matches、find、group、replaceAll、split 这五个方法。这些就是正则的常用部分,剩下的还有命名组、反向引用、前瞻后顾这些进阶内容,等真遇到场景再学也不迟。

遇到新需求,先问自己一句,这活属于校验、提取、替换、切分里的哪一类。分类定下来,套路就有了。八成的情况,你不会卡在正则上。

评论

使用 GitHub 登录参与讨论。