正则表达式(Regular Expression,简称RegEx)是一种用于描述字符串匹配模式的表达式语言,是一种强大的文本处理工具。通过使用特定的语法规则,我们可以:
正则表达式被广泛应用于文本处理、数据验证、爬虫开发、日志分析等场景,是程序员必备的技能之一。
假设你需要验证用户输入的是否是有效的电子邮件地址。不使用正则表达式,你可能需要编写很多复杂的条件判断代码:
boolean isValidEmail(String email) {
// 检查是否包含@符号
if (!email.contains("@")) return false;
// 检查@前后是否有内容
String[] parts = email.split("@");
if (parts.length != 2) return false;
if (parts[0].length() == 0 || parts[1].length() == 0) return false;
// 检查域名部分是否包含至少一个点
if (!parts[1].contains(".")) return false;
// 检查用户名部分是否有效(不包含特殊字符等)
// ...更多复杂的检查
return true;
}
而使用正则表达式,只需一行代码:
boolean isValidEmail(String email) {
return email.matches("^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,4}$");
}
正则表达式不仅使代码更简洁,而且通常比手写的字符串处理逻辑更高效、更可靠。
Java通过java.util.regex
包提供了对正则表达式的支持,主要包含以下核心类:
Pattern
:编译后的正则表达式对象Matcher
:执行匹配操作的引擎PatternSyntaxException
:表示正则表达式语法错误的异常此外,String
类也提供了几个直接使用正则表达式的方法:
matches()
:判断字符串是否匹配正则表达式split()
:使用正则表达式分割字符串replaceAll()
和replaceFirst()
:使用正则表达式替换字符串内容最简单的正则表达式就是普通字符,它们表示字面值匹配:
String text = "Hello, World!";
boolean matches = text.matches("Hello, World!"); // 返回true
这里的正则表达式"Hello, World!"
只匹配完全相同的字符串。
元字符是正则表达式中具有特殊含义的字符,是正则表达式强大功能的基础。
元字符 | 描述 | 示例 |
---|---|---|
. |
匹配任意单个字符(除了换行符) | "a.c" 匹配 “abc”, “adc”, “a1c” 等 |
^ |
匹配字符串的开始 | "^Hello" 匹配以Hello开头的字符串 |
$ |
匹配字符串的结束 | "World$" 匹配以World结尾的字符串 |
* |
匹配前面的表达式0次或多次 | "a*" 匹配 “”, “a”, “aa”, “aaa” 等 |
+ |
匹配前面的表达式1次或多次 | "a+" 匹配 “a”, “aa”, “aaa” 等 |
? |
匹配前面的表达式0次或1次 | "colou?r" 匹配 “color” 和 “colour” |
\ |
转义字符 | "\\." 匹配 “.” 字符本身 |
Java示例:
// 匹配任意字符
String text = "cat";
boolean matches = text.matches("c.t"); // 返回true,因为"cat"符合"c任意字符t"的模式
// 匹配开头和结尾
String text1 = "Hello, World!";
boolean startsWithHello = text1.matches("^Hello.*"); // 返回true,因为字符串以Hello开头
boolean endsWithWorld = text1.matches(".*World!$"); // 返回true,因为字符串以World!结尾
// 使用量词
String text2 = "abbbc";
boolean matches2 = text2.matches("ab*c"); // 返回true,因为"abbbc"包含"a"后跟多个"b"再跟"c"
在Java字符串和正则表达式中,反斜杠\
都是特殊字符,因此在Java代码中表示正则表达式的反斜杠时,需要使用两个反斜杠\\
:
// 匹配字面上的点号(.)
String text = "example.com";
boolean matches = text.matches("example\\.com"); // 错误:在Java字符串中,\会被解释为转义字符
boolean correctMatches = text.matches("example\\\\.com"); // 错误:过多的反斜杠
boolean properMatches = text.matches("example\\.com"); // 正确:两个反斜杠表示正则表达式中的一个反斜杠
字符类允许匹配一组字符中的任意一个:
字符类 | 描述 | 示例 |
---|---|---|
[abc] |
匹配方括号内的任意一个字符 | [abc] 匹配 “a”, “b”, 或 “c” |
[^abc] |
匹配除了方括号内的任意一个字符 | [^abc] 匹配任何除了 “a”, “b”, 或 “c” 的字符 |
[a-z] |
匹配指定范围内的任意一个字符 | [a-z] 匹配任何小写字母 |
[a-zA-Z] |
可以组合多个范围 | [a-zA-Z] 匹配任何英文字母 |
Java示例:
// 匹配字符集中的任意字符
String text = "cat";
boolean matches = text.matches("[bcd]at"); // 返回false,因为"cat"的第一个字符不在[bcd]中
text = "bat";
matches = text.matches("[bcd]at"); // 返回true
// 匹配字符范围
String digit = "5";
boolean isDigit = digit.matches("[0-9]"); // 返回true,因为"5"在数字范围内
// 匹配多个范围
String letter = "K";
boolean isLetter = letter.matches("[a-zA-Z]"); // 返回true,因为"K"是字母
为了方便,正则表达式提供了一些预定义的字符类:
预定义类 | 描述 | 等价于 |
---|---|---|
\d |
匹配任意数字 | [0-9] |
\D |
匹配任意非数字 | [^0-9] |
\w |
匹配任意字母、数字或下划线 | [a-zA-Z0-9_] |
\W |
匹配任意非字母、数字或下划线 | [^a-zA-Z0-9_] |
\s |
匹配任意空白字符 | [ \t\n\r\f] |
\S |
匹配任意非空白字符 | [^ \t\n\r\f] |
在Java中使用这些预定义类时,需要使用双反斜杠:
// 检查是否为单个数字
String text = "7";
boolean isDigit = text.matches("\\d"); // 返回true
// 检查是否由字母、数字和下划线组成
String username = "user_123";
boolean isValidUsername = username.matches("\\w+"); // 返回true
// 检查是否包含空白字符
String hasSpace = "Hello World";
boolean containsSpace = hasSpace.matches(".*\\s.*"); // 返回true
量词用于指定前面的表达式应该匹配多少次:
量词 | 描述 | 示例 |
---|---|---|
* |
匹配0次或多次 | a* 匹配 “”, “a”, “aa”, … |
+ |
匹配1次或多次 | a+ 匹配 “a”, “aa”, … |
? |
匹配0次或1次 | a? 匹配 “” 或 “a” |
{n} |
精确匹配n次 | a{3} 匹配 “aaa” |
{n,} |
匹配至少n次 | a{2,} 匹配 “aa”, “aaa”, … |
{n,m} |
匹配n到m次 | a{1,3} 匹配 “a”, “aa”, “aaa” |
Java示例:
// 精确匹配次数
String text = "aaa";
boolean matches = text.matches("a{3}"); // 返回true,因为正好有3个a
// 匹配范围次数
String numbers = "123";
boolean isThreeDigits = numbers.matches("\\d{1,3}"); // 返回true,因为有1到3个数字
// 组合使用
String phoneNumber = "123-456-7890";
boolean isValidPhone = phoneNumber.matches("\\d{3}-\\d{3}-\\d{4}"); // 返回true
默认情况下,量词是贪婪的,意味着它们会尽可能多地匹配字符:
String text = "abcdefg";
String pattern = "a.*f"; // 贪婪匹配
// 结果将匹配"abcdef",因为.*会尽可能多地匹配
可以通过在量词后添加?
使其变成非贪婪(懒惰)匹配:
String text = "abcdefg";
String pattern = "a.*?f"; // 非贪婪匹配
// 结果将匹配尽可能少的字符,只要能匹配到f就停止
Java示例:
String html = "内容1内容2";
// 贪婪匹配:匹配从第一个到最后一个的所有内容
Pattern greedyPattern = Pattern.compile(".*");
Matcher greedyMatcher = greedyPattern.matcher(html);
if (greedyMatcher.find()) {
System.out.println("贪婪匹配: " + greedyMatcher.group()); // 输出:内容1内容2
}
// 非贪婪匹配:匹配尽可能短的内容
Pattern lazyPattern = Pattern.compile(".*?");
Matcher lazyMatcher = lazyPattern.matcher(html);
if (lazyMatcher.find()) {
System.out.println("非贪婪匹配: " + lazyMatcher.group()); // 输出:内容1
}
括号()
在正则表达式中用于分组,这不仅可以应用量词到整个组,还可以捕获匹配的文本:
String text = "John Smith";
Pattern pattern = Pattern.compile("(\\w+)\\s(\\w+)");
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) {
String firstName = matcher.group(1); // "John"
String lastName = matcher.group(2); // "Smith"
System.out.println("First name: " + firstName);
System.out.println("Last name: " + lastName);
}
Java 7及以上版本支持命名捕获组,使代码更具可读性:
String text = "John Smith";
Pattern pattern = Pattern.compile("(?\\w+)\\s(?\\w+)" );
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) {
String firstName = matcher.group("firstName"); // "John"
String lastName = matcher.group("lastName"); // "Smith"
System.out.println("First name: " + firstName);
System.out.println("Last name: " + lastName);
}
如果只想使用括号进行分组,但不需要捕获匹配的文本,可以使用非捕获分组(?:...)
:
String text = "abc123def456";
Pattern pattern = Pattern.compile("(?:\\d+)([a-z]+)");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
// matcher.group(1)将是"def",不会捕获数字部分
System.out.println(matcher.group(1));
}
边界匹配器不匹配实际字符,而是匹配位置:
边界 | 描述 |
---|---|
^ |
匹配行的开始 |
$ |
匹配行的结束 |
\b |
匹配单词边界(一个\w与一个\W之间的位置,或字符串的开始或结束) |
\B |
匹配非单词边界 |
Java示例:
// 匹配整个单词
String text = "This is a simple example";
Pattern pattern = Pattern.compile("\\bsimple\\b");
Matcher matcher = pattern.matcher(text);
boolean found = matcher.find(); // 返回true,因为"simple"是一个完整的单词
// 匹配以特定文本开头的行
String multiLine = "First line\nSecond line\nThird line";
Pattern startPattern = Pattern.compile("^Second", Pattern.MULTILINE);
Matcher startMatcher = startPattern.matcher(multiLine);
boolean matchesStart = startMatcher.find(); // 返回true,因为有一行以"Second"开头
零宽断言用于查找特定模式前后的位置,而不消耗任何字符: