正则表达式入门到实战

0 阅读5分钟

正则表达式入门到实战

前言

正则表达式(Regular Expression)是处理字符串的强大工具,被誉为程序员必备的"瑞士军刀"。无论是表单验证、日志分析,还是数据清洗、URL处理,正则表达式都能发挥重要作用。本文将从零开始,循序渐进地讲解正则表达式的核心概念和实战技巧。

一、正则表达式基础

1.1 什么是正则表达式

正则表达式是一种描述字符模式的语法规则,用于在文本中查找、匹配、替换符合特定模式的字符串。在Java中,我们主要通过java.util.regex包中的PatternMatcher类来使用正则表达式。

1.2 系统架构

在Java中,正则表达式的工作流程如下:

  1. 输入字符串 - 待处理的文本内容
  2. Pattern编译器 - 将正则模式编译成内部状态机
  3. Matcher匹配器 - 执行具体的匹配操作
  4. 匹配结果 - 返回匹配结果和分组信息
// 基本用法示例
Pattern pattern = Pattern.compile("\\d+");  // 编译正则
Matcher matcher = pattern.matcher("abc123def");  // 创建匹配器
while (matcher.find()) {  // 查找匹配
    System.out.println(matcher.group());  // 输出: 123
}

1.3 核心语法

元字符

正则表达式由普通字符和元字符组成。元字符有特殊含义:

元字符说明示例匹配结果
.任意单个字符a.cabc, adc, a1c
*零次或多次ab*a, ab, abb
+一次或多次ab+ab, abb
?零次或一次ab?a, ab
^行首^HelloHello World
$行尾World$Hello World
字符类
  • [abc] - 匹配a、b或c
  • [a-z] - 匹配任意小写字母
  • [0-9]\d - 匹配数字
  • \w - 匹配单词字符(字母、数字、下划线)
  • \s - 匹配空白字符
边界锚点
  • \b - 单词边界
  • ^ - 字符串开头
  • $ - 字符串结尾

1.4 实战示例:常用验证

// 邮箱验证
String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
boolean isValidEmail = Pattern.matches(emailPattern, "test@example.com");

// 手机号验证(中国)
String phonePattern = "^1[3-9]\\d{9}$";
boolean isValidPhone = Pattern.matches(phonePattern, "13800138000");

// 身份证验证
String idPattern = "^\\d{17}[\\dXx]$";
boolean isValidIdCard = Pattern.matches(idPattern, "123456789012345678");

二、正则表达式进阶

2.1 贪婪 vs 非贪婪匹配

默认情况下,正则表达式是贪婪的,会尽可能多地匹配字符。使用*?+???可以实现非贪婪匹配。

String text = "<div>内容1</div><div>内容2</div>";

// 贪婪匹配 - 匹配所有
Pattern greedy = Pattern.compile("<div>.*</div>");
// 结果: <div>内容1</div><div>内容2</div>

// 非贪婪匹配 - 匹配单个
Pattern lazy = Pattern.compile("<div>.*?</div>");
// 结果: <div>内容1</div>

2.2 零宽断言

零宽断言是一种特殊的匹配方式,它只判断位置,不消耗字符。

类型语法说明
正向先行断言(?=...)后面跟着...
负向先行断言(?!...)后面不跟着...
正向后行断言(?<=...)前面是...
负向后行断言(?<!...)前面不是...
// 匹配后面跟着数字的单词
String pattern = "\\w+(?=\\d)";
// apple1 banana2 cherry -> 匹配 apple, banana

2.3 分组与回溯引用

使用圆括号()可以创建分组,通过\1\2等方式引用前面的分组。

// 匹配重复的单词
Pattern pattern = Pattern.compile("\\b(\\w+)\\s+\\1\\b");
// hello hello world -> 匹配 "hello hello"

// 匹配HTML标签对
Pattern htmlPattern = Pattern.compile("<(\\w+)>.*?</\\1>");
// <div>内容</div> -> 匹配成功

2.4 命名分组(Java 9+)

Pattern pattern = Pattern.compile(
    "\\[(?<timestamp>[^\\]]+)\\]\\s+" +
    "\\[(?<level>[^\\]]+)\\]"
);
Matcher matcher = pattern.matcher("[2024-03-05] [INFO] log message");
String timestamp = matcher.group("timestamp");  // 2024-03-05
String level = matcher.group("level");         // INFO

三、匹配流程

正则表达式的匹配过程遵循以下流程:

  1. 开始 - 编译正则模式
  2. 创建Matcher - 将模式应用到输入字符串
  3. 执行匹配 - 从字符串开头开始匹配
  4. 判断结果 - 匹配成功则返回结果,否则移动到下一个位置继续尝试
  5. 继续 - 直到匹配成功或字符串遍历完毕

四、性能优化

4.1 预编译正则表达式

频繁使用的正则表达式应该预编译,避免重复编译开销。

// 好的做法 - 预编译
public class Validator {
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");

    public static boolean isValidEmail(String email) {
        return EMAIL_PATTERN.matcher(email).matches();
    }
}

// 不好的做法 - 每次编译
boolean isValid = Pattern.compile("...").matcher(email).matches();

4.2 避免回溯爆炸

避免使用嵌套量词,如(a+)+,这种模式在某些输入下会导致指数级的时间复杂度。

// 危险 - 可能导致回溯爆炸
Pattern dangerous = Pattern.compile("(a+)+");

// 安全 - 使用字符类
Pattern safe = Pattern.compile("a+");

五、最佳实践

实践说明
OK 预编译正则频繁使用的正则应预编译
OK 非贪婪匹配*?+?代替*+
OK 避免回溯避免嵌套量词
OK 使用字符类[abc]代替a|b|c
OK 边界锚点^$\b限定匹配范围
X 避免复杂超长正则难以维护
X 测试边界特殊字符、空值都需要测试

六、开源框架中的应用

6.1 Spring MVC - 路径匹配

Spring的AntPathMatcher将Ant风格的路径模式转换为正则表达式进行匹配。

AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/api/**", "/api/users/123");  // true

6.2 MyBatis - 动态SQL解析

MyBatis使用正则表达式解析XML中的动态SQL标签。

<if test="name != null">AND name = #{name}</if>

6.3 Logback - 日志过滤

Logback使用正则表达式过滤特定模式的日志。

<!-- 只记录ERROR级别的日志 -->
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
  <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
    <expression>level == ERROR</expression>
  </evaluator>
</filter>

/**
     * 示例4:Logback日志过滤
     * 使用正则表达式匹配日志内容进行过滤
     */
    public static class LogbackFilter {

        private Pattern includePattern;
        private Pattern excludePattern;

        public LogbackFilter(String includeRegex, String excludeRegex) {
            this.includePattern = includeRegex != null ? Pattern.compile(includeRegex) : null;
            this.excludePattern = excludeRegex != null ? Pattern.compile(excludeRegex) : null;
        }

        public boolean shouldLog(String logLine) {
            // 检查排除规则
            if (excludePattern != null && excludePattern.matcher(logLine).find()) {
                return false;
            }
            // 检查包含规则
            if (includePattern == null || includePattern.matcher(logLine).find()) {
                return true;
            }
            return false;
        }

        public static void demo() {
            System.out.println("【框架4】Logback日志过滤");

            // 只记录包含ERROR的日志,排除health check日志
            LogbackFilter filter = new LogbackFilter(".*ERROR.*", ".*health.*");

            String[] logs = {
                "[ERROR] Connection failed",
                "[INFO] Health check passed",
                "[ERROR] Database timeout",
                "[WARN] Slow query detected"
            };

            for (String log : logs) {
                boolean logIt = filter.shouldLog(log);
                System.out.printf("  %s %s%n", logIt ? "记录" : "忽略", log);
            }
            System.out.println();
        }
    }

6.4 Hibernate - 参数验证

Hibernate Validator的@Pattern注解使用正则表达式进行数据校验。

@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;

七、应用场景

7.1 表单验证

// 密码强度验证:至少8位,包含大小写字母和数字
Pattern passwordPattern = Pattern.compile(
    "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$"
);

7.2 日志分析

// 解析日志格式 [时间] [级别] 类名 - 消息
Pattern logPattern = Pattern.compile(
    "\\[(?<timestamp>[^\\]]+)\\]\\s+" +
    "\\[(?<level>[^\\]]+)\\]\\s+" +
    "(?<className>[A-Za-z0-9.$]+)\\s+-\\s+" +
    "(?<message>.*)"
);

7.3 敏感信息脱敏

// 手机号脱敏: 13800138000 -> 138****8000
String masked = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");

// 身份证脱敏: 123456789012345678 -> 123456********5678
String masked = idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");

7.4 SQL注入检测

// 检测常见SQL注入模式
Pattern[] injectionPatterns = {
    Pattern.compile("(?i).*(union\\s+select)."),
    Pattern.compile("(?i).*(or\\s+1\\s*=\\s*1)."),
    Pattern.compile("(?i).*(drop\\s+table).")
};

public static boolean containsSqlInjection(String input) {
    for (Pattern pattern : injectionPatterns) {
        if (pattern.matcher(input).find()) {
            return true;
        }
    }
    return false;
}

八、总结

正则表达式是处理文本的强大工具,掌握它能大大提高开发效率。从基础的字符匹配到高级的零宽断言,从简单的表单验证到复杂的数据分析,正则表达式都能胜任。

记住以下要点:

  1. 预编译频繁使用的正则表达式

  2. 使用非贪婪匹配提高性能

  3. 添加边界锚点限定匹配范围

  4. 编写单元测试验证各种边界情况