文章目录
- 1. 正则表达式基础
- 1.1 什么是正则表达式
- 1.2 为什么需要学习正则表达式
- 1.3 Java中的正则表达式支持
- 2. 正则表达式语法
- 2.1 基本匹配
- 2.2 元字符
- 2.2.1 常用元字符
- 2.2.2 转义字符
- 2.2.3 字符类
- 2.2.4 预定义字符类
- 2.2.5 量词
- 2.3 贪婪与非贪婪匹配
- 2.4 分组与捕获
- 2.4.1 命名分组
- 2.4.2 非捕获分组
- 2.5 边界匹配器
- 3. 高级正则表达式特性
- 3.1 零宽断言(Look-around)
- 3.2 条件匹配
- 3.3 模式标志
- 3.4 反向引用
- 3.5 递归匹配
- 4. Java中的正则表达式API
- 4.1 使用Pattern和Matcher类
- 4.2 重要的Pattern方法
- 4.3 重要的Matcher方法
- 4.4 String类中的正则表达式方法
- 4.5 特殊替换序列
- 5. 常见正则表达式示例与解析
- 5.1 数据验证正则表达式
- 电子邮件验证
- 电话号码验证(美国格式)
- URL验证
- 日期验证(YYYY-MM-DD格式)
- 密码强度验证
- 5.2 文本处理正则表达式
- 提取所有HTML标签
- 提取所有URL
- 删除HTML标签
- 格式化电话号码
- 5.3 高级示例
- 解析CSV数据
- 提取文本中的所有日期
- 6. 正则表达式在Java中的实际应用
- 6.1 处理配置文件
- 6.2 日志解析
- 6.3 网页爬虫
- 6.4 表单验证
- 6.5 文本替换处理
- 7. 正则表达式性能优化
- 7.1 常见性能问题
- 7.2 优化技巧
- 7.2.1 缓存编译后的Pattern对象
- 7.2.2 使用非捕获组
- 7.2.3 优化量词
- 7.2.4 使用原子组
- 7.2.5 避免过度使用点通配符和贪婪量词
- 7.3 性能测试
- 8. 正则表达式调试与故障排除
- 8.1 常见问题与解决方案
- 8.1.1 匹配失败
- 8.1.2 性能问题
- 8.2 正则表达式可视化工具
- 8.3 单元测试正则表达式
- 9. 正则表达式的局限性与替代方案
- 9.1 何时不使用正则表达式
- 9.2 替代方案
- 9.2.1 专用解析器
- 9.2.2 字符串处理库
- 9.2.3 状态机
- 10. 总结与最佳实践
- 10.1 正则表达式的核心概念回顾
- 10.2 正则表达式最佳实践
- 10.3 持续学习资源
- 附录:正则表达式速查表
- 元字符
- 字符类
- 预定义字符类
- 量词
- 零宽断言
- 其他
1. 正则表达式基础
1.1 什么是正则表达式
正则表达式(Regular Expression,简称RegEx)是一种用于描述字符串匹配模式的表达式语言,是一种强大的文本处理工具。通过使用特定的语法规则,我们可以:
- 在文本中查找特定的模式
- 验证字符串是否符合特定格式(如邮箱、电话号码等)
- 替换文本中的特定内容
- 从文本中提取特定的信息
正则表达式被广泛应用于文本处理、数据验证、爬虫开发、日志分析等场景,是程序员必备的技能之一。
1.2 为什么需要学习正则表达式
假设你需要验证用户输入的是否是有效的电子邮件地址。不使用正则表达式,你可能需要编写很多复杂的条件判断代码:
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}$");
}
正则表达式不仅使代码更简洁,而且通常比手写的字符串处理逻辑更高效、更可靠。
1.3 Java中的正则表达式支持
Java通过java.util.regex
包提供了对正则表达式的支持,主要包含以下核心类:
Pattern
:编译后的正则表达式对象Matcher
:执行匹配操作的引擎PatternSyntaxException
:表示正则表达式语法错误的异常
此外,String
类也提供了几个直接使用正则表达式的方法:
matches()
:判断字符串是否匹配正则表达式split()
:使用正则表达式分割字符串replaceAll()
和replaceFirst()
:使用正则表达式替换字符串内容
2. 正则表达式语法
2.1 基本匹配
最简单的正则表达式就是普通字符,它们表示字面值匹配:
String text = "Hello, World!";
boolean matches = text.matches("Hello, World!"); // 返回true
这里的正则表达式"Hello, World!"
只匹配完全相同的字符串。
2.2 元字符
元字符是正则表达式中具有特殊含义的字符,是正则表达式强大功能的基础。
2.2.1 常用元字符
元字符 | 描述 | 示例 |
---|---|---|
. | 匹配任意单个字符(除了换行符) | "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"
2.2.2 转义字符
在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"); // 正确:两个反斜杠表示正则表达式中的一个反斜杠
2.2.3 字符类
字符类允许匹配一组字符中的任意一个:
字符类 | 描述 | 示例 |
---|---|---|
[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"是字母
2.2.4 预定义字符类
为了方便,正则表达式提供了一些预定义的字符类:
预定义类 | 描述 | 等价于 |
---|---|---|
\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
2.2.5 量词
量词用于指定前面的表达式应该匹配多少次:
量词 | 描述 | 示例 |
---|---|---|
* | 匹配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
2.3 贪婪与非贪婪匹配
默认情况下,量词是贪婪的,意味着它们会尽可能多地匹配字符:
String text = "abcdefg";
String pattern = "a.*f"; // 贪婪匹配
// 结果将匹配"abcdef",因为.*会尽可能多地匹配
可以通过在量词后添加?
使其变成非贪婪(懒惰)匹配:
String text = "abcdefg";
String pattern = "a.*?f"; // 非贪婪匹配
// 结果将匹配尽可能少的字符,只要能匹配到f就停止
Java示例:
String html = "<div>内容1</div><div>内容2</div>";// 贪婪匹配:匹配从第一个<div>到最后一个</div>的所有内容
Pattern greedyPattern = Pattern.compile("<div>.*</div>");
Matcher greedyMatcher = greedyPattern.matcher(html);
if (greedyMatcher.find()) {System.out.println("贪婪匹配: " + greedyMatcher.group()); // 输出:<div>内容1</div><div>内容2</div>
}// 非贪婪匹配:匹配尽可能短的内容
Pattern lazyPattern = Pattern.compile("<div>.*?</div>");
Matcher lazyMatcher = lazyPattern.matcher(html);
if (lazyMatcher.find()) {System.out.println("非贪婪匹配: " + lazyMatcher.group()); // 输出:<div>内容1</div>
}
2.4 分组与捕获
括号()
在正则表达式中用于分组,这不仅可以应用量词到整个组,还可以捕获匹配的文本:
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);
}
2.4.1 命名分组
Java 7及以上版本支持命名捕获组,使代码更具可读性:
String text = "John Smith";
Pattern pattern = Pattern.compile("(?<firstName>\\w+)\\s(?<lastName>\\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);
}
2.4.2 非捕获分组
如果只想使用括号进行分组,但不需要捕获匹配的文本,可以使用非捕获分组(?:...)
:
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));
}
2.5 边界匹配器
边界匹配器不匹配实际字符,而是匹配位置:
边界 | 描述 |
---|---|
^ | 匹配行的开始 |
$ | 匹配行的结束 |
\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"开头
3. 高级正则表达式特性
3.1 零宽断言(Look-around)
零宽断言用于查找特定模式前后的位置,而不消耗任何字符:
- 零宽正向先行断言(?=pattern):断言当前位置后面跟着特定模式
- 零宽负向先行断言(?!pattern):断言当前位置后面不跟着特定模式
- 零宽正向后行断言(?<=pattern):断言当前位置前面跟着特定模式
- 零宽负向后行断言(?<!pattern):断言当前位置前面不跟着特定模式
Java示例:
// 匹配后面跟着数字的单词
String text = "abc123 def456 xyz";
Pattern pattern = Pattern.compile("\\w+(?=\\d+)");
Matcher matcher = pattern.matcher(text);while (matcher.find()) {System.out.println(matcher.group()); // 输出:abc, def
}// 匹配前面是数字的单词
String text2 = "123abc 456def";
Pattern pattern2 = Pattern.compile("(?<=\\d+)\\w+");
Matcher matcher2 = pattern2.matcher(text2);while (matcher2.find()) {System.out.println(matcher2.group()); // 输出:abc, def
}// 匹配不在html标签内的单词
String html = "This is <span>some</span> text";
Pattern pattern3 = Pattern.compile("\\b\\w+\\b(?!([^<]*>))");
Matcher matcher3 = pattern3.matcher(html);while (matcher3.find()) {System.out.println(matcher3.group()); // 输出:This, is, text
}
3.2 条件匹配
正则表达式还可以使用条件判断:
// 如果捕获组1匹配了内容,则匹配'foo',否则匹配'bar'
String pattern = "(condition)?(?(1)foo|bar)";
Java示例:
// 如果字符串包含数字,则后面必须是字母,否则后面必须是特殊字符
String pattern = "(\\d+)?(?(1)[a-z]+|[!@#$%^&*]+)";
Pattern p = Pattern.compile(pattern);// 会匹配:123abc、!@#
// 不会匹配:123!@#、abc
3.3 模式标志
正则表达式可以使用一些标志来改变其行为:
标志 | 描述 | Java中的常量 |
---|---|---|
i | 忽略大小写 | Pattern.CASE_INSENSITIVE |
m | 多行模式:^和$匹配每行的开始和结束 | Pattern.MULTILINE |
s | 单行模式:.匹配所有字符,包括换行符 | Pattern.DOTALL |
x | 忽略正则表达式中的空白字符和注释 | Pattern.COMMENTS |
u | Unicode支持 | Pattern.UNICODE_CASE |
Java示例:
// 忽略大小写
Pattern pattern = Pattern.compile("hello", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher("Hello World");
boolean matches = matcher.find(); // 返回true// 多行模式
String multiLine = "Line 1\nLine 2\nLine 3";
Pattern pattern2 = Pattern.compile("^Line", Pattern.MULTILINE);
Matcher matcher2 = pattern2.matcher(multiLine);
int count = 0;
while (matcher2.find()) count++;
System.out.println(count); // 输出:3,因为每行的开始都匹配"Line"// 单行模式(.匹配所有字符包括换行符)
String multiLine2 = "Start\nMiddle\nEnd";
// 不使用DOTALL,下面会失败,因为.不会匹配换行符
Pattern pattern3 = Pattern.compile("Start.*End");
// 使用DOTALL,下面会成功匹配
Pattern pattern4 = Pattern.compile("Start.*End", Pattern.DOTALL);
3.4 反向引用
反向引用允许在正则表达式中引用之前捕获的组:
// 匹配重复单词
String text = "The the dog barked at the cat";
Pattern pattern = Pattern.compile("\\b(\\w+)\\s+\\1\\b", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);if (matcher.find()) {System.out.println("重复单词: " + matcher.group(1)); // 输出:The
}
3.5 递归匹配
某些复杂模式(如嵌套括号的匹配)可以使用递归表达式:
// 匹配嵌套括号
String pattern = "\\((?:[^()]++|(?R))*+\\)";
// 该模式可以匹配嵌套的括号,如 "()", "(())", "((()))", 等
Java不直接支持递归表达式,但可以通过其他方式模拟:
// 使用Java代码递归处理嵌套括号
public static boolean isBalancedParentheses(String text) {if (text.isEmpty()) {return true;}// 使用栈来跟踪括号Stack<Character> stack = new Stack<>();for (char c : text.toCharArray()) {if (c == '(') {stack.push(c);} else if (c == ')') {if (stack.isEmpty() || stack.pop() != '(') {return false;}}}return stack.isEmpty();
}
4. Java中的正则表达式API
4.1 使用Pattern和Matcher类
使用Pattern
和Matcher
类提供了比String.matches()
更灵活的方式来处理正则表达式:
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class RegexExample {public static void main(String[] args) {// 编译正则表达式Pattern pattern = Pattern.compile("\\b[A-Z]\\w*\\b");// 创建Matcher对象String text = "This is a Test String with Some Capitalized Words";Matcher matcher = pattern.matcher(text);// 查找所有匹配项while (matcher.find()) {System.out.println("Found match: " + matcher.group() + " at position: " + matcher.start());}}
}
4.2 重要的Pattern方法
方法 | 描述 |
---|---|
compile(String regex) | 编译正则表达式并返回Pattern对象 |
compile(String regex, int flags) | 使用指定的标志编译正则表达式 |
matcher(CharSequence input) | 创建一个Matcher对象 |
split(CharSequence input) | 使用正则表达式分割字符串 |
split(CharSequence input, int limit) | 使用正则表达式分割字符串,限制结果数量 |
pattern() | 返回正则表达式字符串 |
4.3 重要的Matcher方法
方法 | 描述 |
---|---|
find() | 查找下一个匹配项 |
find(int start) | 从指定位置开始查找下一个匹配项 |
matches() | 判断整个输入是否匹配模式 |
lookingAt() | 判断输入的开头是否匹配模式 |
group() | 返回上一次匹配操作的组0(整个匹配) |
group(int group) | 返回指定捕获组 |
group(String name) | 返回指定命名捕获组 |
start() | 返回上一次匹配的起始索引 |
end() | 返回上一次匹配的结束索引 |
replaceAll(String replacement) | 替换所有匹配项 |
replaceFirst(String replacement) | 替换第一个匹配项 |
reset() | 重置matcher状态 |
4.4 String类中的正则表达式方法
String类提供了几个直接使用正则表达式的便捷方法:
String text = "apple,banana,orange";// 使用正则表达式分割字符串
String[] fruits = text.split(",");// 替换所有匹配项
String replaced = text.replaceAll("a", "A");// 替换第一个匹配项
String replacedFirst = text.replaceFirst("a", "A");// 检查是否匹配正则表达式
boolean matches = text.matches(".*banana.*");
4.5 特殊替换序列
在replaceAll()
和replaceFirst()
中,可以使用特殊的替换序列引用捕获组:
String text = "John Smith";
String result = text.replaceAll("(\\w+)\\s(\\w+)", "$2, $1");
System.out.println(result); // 输出:Smith, John// 使用Pattern和Matcher
Pattern pattern = Pattern.compile("(\\w+)\\s(\\w+)");
Matcher matcher = pattern.matcher(text);
String result2 = matcher.replaceAll("$2, $1");
System.out.println(result2); // 输出:Smith, John
5. 常见正则表达式示例与解析
5.1 数据验证正则表达式
电子邮件验证
public static boolean isValidEmail(String email) {String regex = "^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,4}$";return email.matches(regex);
}
该正则表达式的解析:
^
- 匹配字符串开始[\w.-]+
- 匹配一个或多个字母、数字、下划线、点或连字符(用户名部分)@
- 匹配@符号([\w-]+\.)+
- 匹配一个或多个由点分隔的单词(域名部分)[\w-]{2,4}$
- 匹配2到4个字母、数字或连字符(顶级域名)
电话号码验证(美国格式)
public static boolean isValidPhoneNumber(String phoneNumber) {String regex = "^(\\+1\\s?)?(\\d{3}|\\(\\d{3}\\))[\\s.-]?\\d{3}[\\s.-]?\\d{4}$";return phoneNumber.matches(regex);
}
该正则表达式匹配以下格式:
- +1 123-456-7890
- (123) 456-7890
- 123.456.7890
- 123 456 7890
- 1234567890
URL验证
public static boolean isValidUrl(String url) {String regex = "^(https?|ftp)://([\\w.-]+)(:\\d+)?(/[\\w./]*)?$";return url.matches(regex);
}
这个正则表达式匹配常见的URL格式,包括协议(http, https, ftp)、域名、可选的端口号和路径。
日期验证(YYYY-MM-DD格式)
public static boolean isValidDate(String date) {String regex = "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$";return date.matches(regex);
}
该正则表达式匹配格式为YYYY-MM-DD的日期,并进行基本的范围验证。
密码强度验证
public static boolean isStrongPassword(String password) {// 至少8个字符,至少包含一个大写字母,一个小写字母,一个数字和一个特殊字符String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";return password.matches(regex);
}
这个正则表达式使用了零宽断言来确保密码包含所有必要的字符类型。
5.2 文本处理正则表达式
提取所有HTML标签
public static List<String> extractHtmlTags(String html) {List<String> tags = new ArrayList<>();Pattern pattern = Pattern.compile("<[^>]+>");Matcher matcher = pattern.matcher(html);while (matcher.find()) {tags.add(matcher.group());}return tags;
}
提取所有URL
public static List<String> extractUrls(String text) {List<String> urls = new ArrayList<>();Pattern pattern = Pattern.compile("https?://[^\\s]+");Matcher matcher = pattern.matcher(text);while (matcher.find()) {urls.add(matcher.group());}return urls;
}
删除HTML标签
public static String stripHtmlTags(String html) {return html.replaceAll("<[^>]*>", "");
}
格式化电话号码
public static String formatPhoneNumber(String phoneNumber) {// 提取数字,忽略所有其他字符String digits = phoneNumber.replaceAll("\\D", "");// 格式化为(XXX) XXX-XXXXreturn digits.replaceFirst("(\\d{3})(\\d{3})(\\d{4})", "($1) $2-$3");
}
5.3 高级示例
解析CSV数据
以下示例展示了如何使用正则表达式解析CSV数据,处理引号中的逗号:
public static List<String[]> parseCSV(String csvText) {List<String[]> result = new ArrayList<>();// 匹配CSV的一行:引号内的字段可能包含逗号,引号内两个连续引号表示转义Pattern pattern = Pattern.compile("(?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)");// 按行处理String[] lines = csvText.split("\n");for (String line : lines) {List<String> fields = new ArrayList<>();Matcher matcher = pattern.matcher(line);while (matcher.find()) {String field = matcher.group(1);// 处理引号if (field.startsWith("\"") && field.endsWith("\"")) {field = field.substring(1, field.length() - 1).replace("\"\"", "\"");}fields.add(field);}result.add(fields.toArray(new String[0]));}return result;
}
提取文本中的所有日期
public static List<String> extractDates(String text) {List<String> dates = new ArrayList<>();// 匹配常见日期格式:MM/DD/YYYY, MM-DD-YYYY, YYYY-MM-DD等Pattern pattern = Pattern.compile("\\b(0?[1-9]|1[0-2])[-/](0?[1-9]|[12]\\d|3[01])[-/](\\d{4}|\\d{2})\\b|\\b(\\d{4})[-/](0?[1-9]|1[0-2])[-/](0?[1-9]|[12]\\d|3[01])\\b");Matcher matcher = pattern.matcher(text);while (matcher.find()) {dates.add(matcher.group());}return dates;
}
6. 正则表达式在Java中的实际应用
6.1 处理配置文件
解析配置文件是正则表达式的常见应用,例如解析properties文件:
public static Map<String, String> parsePropertiesFile(String content) {Map<String, String> properties = new HashMap<>();Pattern pattern = Pattern.compile("^\\s*(\\w+)\\s*=\\s*(.*)\\s*$", Pattern.MULTILINE);Matcher matcher = pattern.matcher(content);while (matcher.find()) {String key = matcher.group(1);String value = matcher.group(2);properties.put(key, value);}return properties;
}
6.2 日志解析
日志文件分析是正则表达式的另一个常见应用场景:
public static List<LogEntry> parseLogFile(String logContent) {List<LogEntry> entries = new ArrayList<>();// 匹配格式: [日期 时间] [级别] [类名] - 消息Pattern pattern = Pattern.compile("\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\] \\[(\\w+)\\] \\[([^\\]]+)\\] - (.*)");Matcher matcher = pattern.matcher(logContent);while (matcher.find()) {String timestamp = matcher.group(1);String level = matcher.group(2);String className = matcher.group(3);String message = matcher.group(4);entries.add(new LogEntry(timestamp, level, className, message));}return entries;
}// 日志条目类
class LogEntry {private String timestamp;private String level;private String className;private String message;public LogEntry(String timestamp, String level, String className, String message) {this.timestamp = timestamp;this.level = level;this.className = className;this.message = message;}// getter方法...
}
6.3 网页爬虫
正则表达式常用于从网页中提取信息:
public static List<String> extractImageUrls(String htmlContent) {List<String> imageUrls = new ArrayList<>();Pattern pattern = Pattern.compile("<img\\s+[^>]*src=[\"']([^\"']+)[\"'][^>]*>");Matcher matcher = pattern.matcher(htmlContent);while (matcher.find()) {imageUrls.add(matcher.group(1));}return imageUrls;
}
6.4 表单验证
在Web应用程序中,正则表达式常用于客户端和服务器端的表单验证:
public class FormValidator {private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,4}$");private static final Pattern PHONE_PATTERN = Pattern.compile("^(\\+\\d{1,3}[- ]?)?\\d{10}$");private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,20}$");private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$");public static boolean isValidEmail(String email) {return email != null && EMAIL_PATTERN.matcher(email).matches();}public static boolean isValidPhone(String phone) {return phone != null && PHONE_PATTERN.matcher(phone).matches();}public static boolean isValidUsername(String username) {return username != null && USERNAME_PATTERN.matcher(username).matches();}public static boolean isValidPassword(String password) {return password != null && PASSWORD_PATTERN.matcher(password).matches();}
}
6.5 文本替换处理
正则表达式在文本处理中非常有用,例如替换或格式化文本:
public class TextProcessor {// 将驼峰命名法转换为下划线命名法public static String camelToSnake(String text) {return text.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();}// 将下划线命名法转换为驼峰命名法public static String snakeToCamel(String text) {Pattern pattern = Pattern.compile("_([a-z])");Matcher matcher = pattern.matcher(text);StringBuffer result = new StringBuffer();while (matcher.find()) {matcher.appendReplacement(result, matcher.group(1).toUpperCase());}matcher.appendTail(result);return result.toString();}// 对HTML文本进行转义public static String escapeHtml(String html) {return html.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");}
}
7. 正则表达式性能优化
7.1 常见性能问题
正则表达式功能强大,但如果使用不当可能导致严重的性能问题:
- 回溯爆炸:某些正则表达式在特定输入下可能导致指数级别的回溯次数
- 过度使用贪婪量词:可能导致不必要的回溯
- 大量使用环视断言:可能降低匹配效率
- 不必要的复杂性:复杂的正则表达式难以维护且可能较慢
7.2 优化技巧
7.2.1 缓存编译后的Pattern对象
Pattern.compile()
方法是相对昂贵的操作,应该尽可能重用Pattern对象:
// 不推荐:每次调用都编译正则表达式
public boolean validateEmail(String email) {return email.matches("^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,4}$");
}// 推荐:预编译并重用Pattern对象
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,4}$");public boolean validateEmail(String email) {return EMAIL_PATTERN.matcher(email).matches();
}
7.2.2 使用非捕获组
当不需要引用分组内容时,使用非捕获组可以提高性能:
// 使用捕获组(较慢)
Pattern pattern1 = Pattern.compile("(abc|def)\\d+");// 使用非捕获组(较快)
Pattern pattern2 = Pattern.compile("(?:abc|def)\\d+");
7.2.3 优化量词
使用具体范围的量词而不是通用的*
或+
可以减少回溯:
// 可能导致过多回溯
Pattern pattern1 = Pattern.compile("\\w+@\\w+\\.\\w+");// 使用合理的范围限制,减少回溯
Pattern pattern2 = Pattern.compile("\\w{1,20}@\\w{1,20}\\.\\w{2,10}");
7.2.4 使用原子组
原子组(atomic groups)可以防止回溯,提高性能:
// 正常组,允许回溯
Pattern pattern1 = Pattern.compile("(\\d+)");// 原子组,不允许回溯(Java不直接支持,可以用零宽度正向先行断言模拟)
Pattern pattern2 = Pattern.compile("(?>\\d+)");
7.2.5 避免过度使用点通配符和贪婪量词
组合.
和*
或+
可能导致不必要的匹配尝试:
// 效率低,会尝试匹配过多可能性
Pattern pattern1 = Pattern.compile(".*end");// 更高效,限制匹配字符集
Pattern pattern2 = Pattern.compile("[^\\r\\n]*end");
7.3 性能测试
在使用正则表达式处理大量数据或在性能关键应用中,应该进行性能测试:
public static void testRegexPerformance(Pattern pattern, String input, int iterations) {long startTime = System.nanoTime();for (int i = 0; i < iterations; i++) {Matcher matcher = pattern.matcher(input);matcher.find();}long endTime = System.nanoTime();long duration = (endTime - startTime) / 1_000_000; // 转换为毫秒System.out.println("Pattern: " + pattern.pattern());System.out.println("Execution time: " + duration + " ms for " + iterations + " iterations");
}
8. 正则表达式调试与故障排除
8.1 常见问题与解决方案
8.1.1 匹配失败
当正则表达式不匹配预期的文本时:
- 检查语法:确保正则表达式语法正确
- 输出实际与期望:打印实际处理的字符串和期望的匹配结果
- 分步测试:将复杂的正则表达式分解为简单部分单独测试
- 使用调试工具:使用在线正则表达式测试工具
public static void debugRegex(String regex, String input) {System.out.println("Regex: " + regex);System.out.println("Input: " + input);System.out.println("Input length: " + input.length());// 显示输入的字符代码,帮助发现不可见字符System.out.print("Character codes: ");for (char c : input.toCharArray()) {System.out.print((int)c + " ");}System.out.println();// 测试匹配Pattern pattern = Pattern.compile(regex);Matcher matcher = pattern.matcher(input);boolean matches = matcher.matches();System.out.println("Matches entire input: " + matches);// 重置并查找部分匹配matcher.reset();System.out.println("Partial matches:");int count = 0;while (matcher.find()) {count++;System.out.println(count + ": " + matcher.group() + " at position " + matcher.start());}if (count == 0) {System.out.println("No partial matches found");}
}
8.1.2 性能问题
对于性能问题:
- 检查回溯:识别可能导致过度回溯的模式
- 简化正则表达式:使用更简单的表达式或分多步处理
- 限制输入大小:对大文本分块处理
- 使用StringBuffer:处理大量替换时使用
StringBuffer
和appendReplacement
/appendTail
方法
// 处理大文本的替换
public static String efficientReplace(String input, String regex, String replacement) {Pattern pattern = Pattern.compile(regex);Matcher matcher = pattern.matcher(input);StringBuffer result = new StringBuffer();while (matcher.find()) {matcher.appendReplacement(result, replacement);}matcher.appendTail(result);return result.toString();
}
8.2 正则表达式可视化工具
有多种在线工具可以帮助可视化和调试正则表达式:
- Regex101:提供详细的解释和调试功能
- RegExr:交互式正则表达式测试器
- Regexper:将正则表达式可视化为图表
- Debuggex:提供正则表达式的视觉调试
8.3 单元测试正则表达式
正则表达式应该像其他代码一样进行测试:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;public class RegexTest {@Testpublic void testEmailValidator() {Pattern pattern = Pattern.compile("^[\\w.-]+@([\\w-]+\\.)+[\\w-]{2,4}$");// 有效的电子邮件地址assertTrue(pattern.matcher("user@example.com").matches());assertTrue(pattern.matcher("user.name@example.co.uk").matches());assertTrue(pattern.matcher("user-name@subdomain.example.org").matches());// 无效的电子邮件地址assertFalse(pattern.matcher("user@").matches());assertFalse(pattern.matcher("@example.com").matches());assertFalse(pattern.matcher("user@example").matches());assertFalse(pattern.matcher("user@.com").matches());}@Testpublic void testPhoneNumberValidator() {Pattern pattern = Pattern.compile("^(\\+\\d{1,3}[- ]?)?\\d{10}$");// 有效的电话号码assertTrue(pattern.matcher("1234567890").matches());assertTrue(pattern.matcher("+1 1234567890").matches());assertTrue(pattern.matcher("+91-1234567890").matches());// 无效的电话号码assertFalse(pattern.matcher("123456789").matches()); // 太短assertFalse(pattern.matcher("12345678901").matches()); // 太长assertFalse(pattern.matcher("abcdefghij").matches()); // 非数字}
}
9. 正则表达式的局限性与替代方案
9.1 何时不使用正则表达式
正则表达式并非万能的,在以下情况下应考虑其他解决方案:
- 解析嵌套结构:如HTML或XML,应使用专用解析器
- 处理大量数据:可能导致性能问题
- 过于复杂的匹配需求:导致难以维护的复杂表达式
- 需要上下文感知的匹配:如匹配成对的标签
9.2 替代方案
9.2.1 专用解析器
对于HTML、XML、JSON等结构化数据,使用专用解析器:
// 解析HTML
public static List<String> extractLinksUsingJsoup(String html) throws IOException {Document doc = Jsoup.parse(html);Elements links = doc.select("a[href]");List<String> result = new ArrayList<>();for (Element link : links) {result.add(link.attr("href"));}return result;
}// 解析XML
public static List<String> extractValuesUsingXPath(String xml, String xpathExpression) throws Exception {DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();DocumentBuilder builder = factory.newDocumentBuilder();Document doc = builder.parse(new InputSource(new StringReader(xml)));XPathFactory xPathFactory = XPathFactory.newInstance();XPath xpath = xPathFactory.newXPath();NodeList nodes = (NodeList) xpath.evaluate(xpathExpression, doc, XPathConstants.NODESET);List<String> result = new ArrayList<>();for (int i = 0; i < nodes.getLength(); i++) {result.add(nodes.item(i).getTextContent());}return result;
}
9.2.2 字符串处理库
对于简单的字符串操作,使用Java的内置方法通常更高效:
// 使用String.indexOf()和String.substring()
public static List<String> extractEmails(String text) {List<String> emails = new ArrayList<>();int start = 0;while (true) {int atPos = text.indexOf('@', start);if (atPos == -1) break;// 向前查找邮箱用户名的开始int nameStart = atPos;while (nameStart > 0 && isValidEmailChar(text.charAt(nameStart - 1))) {nameStart--;}// 向后查找域名的结束int domainEnd = atPos;while (domainEnd < text.length() && isValidEmailChar(text.charAt(domainEnd))) {domainEnd++;}// 检查是否找到有效的电子邮件if (nameStart < atPos && domainEnd > atPos + 1) {emails.add(text.substring(nameStart, domainEnd));}start = domainEnd;}return emails;
}private static boolean isValidEmailChar(char c) {return Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '_' || c == '@';
}
9.2.3 状态机
对于复杂的文本解析,状态机可能是更好的选择:
public enum ParserState {NORMAL, TAG_OPEN, TAG_NAME, TAG_CLOSE
}public static List<String> extractTagsUsingStateMachine(String html) {List<String> tags = new ArrayList<>();ParserState state = ParserState.NORMAL;StringBuilder currentTag = new StringBuilder();for (char c : html.toCharArray()) {switch (state) {case NORMAL:if (c == '<') {state = ParserState.TAG_OPEN;}break;case TAG_OPEN:if (c == '/') {// Closing tag, ignore} else if (c == '>') {state = ParserState.NORMAL;} else {state = ParserState.TAG_NAME;currentTag.append(c);}break;case TAG_NAME:if (c == ' ' || c == '>') {tags.add(currentTag.toString());currentTag.setLength(0);state = c == '>' ? ParserState.NORMAL : ParserState.TAG_CLOSE;} else {currentTag.append(c);}break;case TAG_CLOSE:if (c == '>') {state = ParserState.NORMAL;}break;}}return tags;
}
10. 总结与最佳实践
10.1 正则表达式的核心概念回顾
- 基本匹配:字面字符匹配和元字符
- 字符类:匹配一组字符中的任意一个
- 量词:指定表达式匹配的次数
- 分组与捕获:分组匹配内容并提取匹配文本
- 边界匹配:匹配文本中的特定位置
- 零宽断言:不消耗字符的匹配条件
- 贪婪与非贪婪匹配:匹配策略的不同行为
10.2 正则表达式最佳实践
- 保持简单:编写简单、明确的正则表达式
- 分解复杂表达式:将复杂表达式分解为多个简单表达式
- 添加注释:使用
(?x)
模式或代码注释解释复杂表达式 - 缓存编译的Pattern:重用Pattern对象以提高性能
- 测试极端情况:测试空输入、非预期输入等边缘情况
- 编写单元测试:验证正则表达式的行为
// 使用Pattern.COMMENTS模式添加注释
Pattern pattern = Pattern.compile("(?x) # 启用注释模式\n" +"^ # 开始\n" +"(?=.*[a-z]) # 至少一个小写字母\n" +"(?=.*[A-Z]) # 至少一个大写字母\n" +"(?=.*\\d) # 至少一个数字\n" +"(?=.*[@#$%^&+=]) # 至少一个特殊字符\n" +".{8,} # 至少8个字符\n" +"$ # 结束"
);
10.3 持续学习资源
- 官方文档:Java Pattern类和正则表达式语法的官方文档
- 正则表达式书籍:《精通正则表达式》等专业书籍
- 在线工具:Regex101, RegExr等在线正则表达式测试工具
- 实践:通过解决实际问题来提高正则表达式技能
附录:正则表达式速查表
元字符
元字符 | 描述 |
---|---|
. | 匹配任意单个字符(除了换行符) |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
* | 匹配前面的表达式0次或多次 |
+ | 匹配前面的表达式1次或多次 |
? | 匹配前面的表达式0次或1次 |
\ | 转义字符 |
` | ` |
() | 分组 |
[] | 字符类 |
{} | 量词 |
字符类
字符类 | 描述 |
---|---|
[abc] | 匹配a、b或c |
[^abc] | 匹配除了a、b和c的任意字符 |
[a-z] | 匹配a到z的任意小写字母 |
[A-Z] | 匹配A到Z的任意大写字母 |
[0-9] | 匹配0到9的任意数字 |
预定义字符类
预定义类 | 描述 |
---|---|
\d | 匹配任意数字 |
\D | 匹配任意非数字 |
\w | 匹配任意字母、数字或下划线 |
\W | 匹配任意非字母、数字或下划线 |
\s | 匹配任意空白字符 |
\S | 匹配任意非空白字符 |
量词
量词 | 描述 |
---|---|
* | 匹配0次或多次 |
+ | 匹配1次或多次 |
? | 匹配0次或1次 |
{n} | 精确匹配n次 |
{n,} | 匹配至少n次 |
{n,m} | 匹配n到m次 |
*? | 非贪婪匹配0次或多次 |
+? | 非贪婪匹配1次或多次 |
?? | 非贪婪匹配0次或1次 |
{n,m}? | 非贪婪匹配n到m次 |
零宽断言
断言 | 描述 |
---|---|
(?=...) | 零宽正向先行断言 |
(?!...) | 零宽负向先行断言 |
(?<=...) | 零宽正向后行断言 |
(?<!...) | 零宽负向后行断言 |
其他
符号 | 描述 |
---|---|
(?:...) | 非捕获组 |
(?<name>...) | 命名捕获组 |
\1, \2, ... | 反向引用 |
\b | 单词边界 |
\B | 非单词边界 |