Skip to content

正则性能优化:让匹配飞起来

正则写对了能跑,但写得好才能跑得快。

这一节不是教你"怎么写正则",而是教你"怎么写更快的正则"。

踩过的坑多了,才知道哪些写法是性能杀手。


性能杀手一号:重复编译

症状:每次调用都重新编译正则表达式。

java
public class RepeatCompileProblem {
    public static void main(String[] args) {
        // ❌ 每次调用都编译(性能杀手)
        public static boolean validatePhone(String phone) {
            return phone.matches("^1[3-9]\\d{9}$"); // 每次都编译 Pattern
        }
        
        // ✅ 只编译一次
        private static final Pattern PHONE = Pattern.compile("^1[3-9]\\d{9}$");
        public static boolean validatePhone(String phone) {
            return PHONE.matcher(phone).matches();
        }
    }
}

性能差距:循环调用 10 万次,预编译比每次编译快 50-100 倍

验证:预编译真的快

java
public class CompileBenchmark {
    public static void main(String[] args) {
        String phone = "13812345678";
        
        // 预热
        for (int i = 0; i < 1000; i++) {
            phone.matches("^1[3-9]\\d{9}$");
            Pattern.compile("^1[3-9]\\d{9}$").matcher(phone).matches();
        }
        
        // 测试:不预编译
        long start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            phone.matches("^1[3-9]\\d{9}$");
        }
        long withoutCompile = System.nanoTime() - start;
        
        // 测试:预编译
        Pattern compiled = Pattern.compile("^1[3-9]\\d{9}$");
        start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            compiled.matcher(phone).matches();
        }
        long withCompile = System.nanoTime() - start;
        
        System.out.println("不预编译: " + withoutCompile / 1_000_000 + " ms");
        System.out.println("预编译: " + withCompile / 1_000_000 + " ms");
        System.out.println("加速比: " + (double) withoutCompile / withCompile + "x");
        // 不预编译: ~800 ms
        // 预编译: ~15 ms
        // 加速比: ~50x
    }
}

性能杀手二号:灾难性回溯

症状:嵌套量词导致指数级回溯。

java
public class BacktrackingProblem {
    public static void main(String[] args) {
        // ❌ 嵌套量词:灾难性回溯
        String evil = "aaaaaaaaaaaaaaaaax"; // 18 个 a + x
        
        long start = System.nanoTime();
        boolean matches = Pattern.compile("(a+)+b").matcher(evil).matches();
        long time = System.nanoTime() - start;
        
        System.out.println("匹配结果: " + matches); // false
        System.out.println("耗时: " + time / 1_000_000 + " ms"); // 几百毫秒!
        
        // 原因分析:
        // (a+)+ 要匹配 18 个 a,然后发现没有 b
        // 正则引擎会尝试各种分组方式
        // 2^18 = 262,144 种可能!每次失败都要回溯
        
        // ✅ 解决方案:不要嵌套量词
        // 改成 (a+)?b 或 a+b
    }
}

常见的灾难性回溯模式

java
public class EvilPatterns {
    // 这些都要避免
    // (a+)+      - 嵌套加号
    // (a*)+      - 嵌套星号
    // (a|b)+     - 嵌套或
    // (\d+)+     - 数字嵌套
    // (.*)*      - 双重贪婪
    
    public static void main(String[] args) {
        // 场景1:匹配 HTML 标签
        String html = "<div>内容</div>";
        
        // ❌ 灾难性回溯
        Pattern evil1 = Pattern.compile("<(.+)+>");
        System.out.println("匹配: " + evil1.matcher(html).find()); // true,但慢
        
        // ✅ 正确写法
        Pattern good1 = Pattern.compile("<[^>]+>");
        System.out.println("匹配: " + good1.matcher(html).find()); // true,且快
        
        // 场景2:邮箱验证
        // ❌ 不要写成这样
        Pattern evil2 = Pattern.compile("(\\w+)+@\\w+\\.\\w+");
        
        // ✅ 用具体数量代替
        Pattern good2 = Pattern.compile("\\w+@\\w+\\.\\w+");
    }
}

如何检测灾难性回溯

用边界测试用例检查执行时间:

java
public class BacktrackingTest {
    public static void main(String[] args) {
        testPattern("(a+)+b", "aaaaaaaaaaaaaaaaax");  // 应该很快返回 false
        testPattern("(\\d+)+", "1234567890x");        // 应该很快返回 false
        testPattern("(a*)+", "aaaaaaaax");            // 应该很快返回 false
    }
    
    static void testPattern(String regex, String input) {
        long start = System.nanoTime();
        try {
            Pattern.compile(regex).matcher(input).matches();
            long time = System.nanoTime() - start;
            System.out.printf("正则: %-15s 输入长度: %-3d 耗时: %d ms%n", 
                regex, input.length(), time / 1_000_000);
        } catch (TimeoutException e) {
            System.out.println("正则: " + regex + " - 超时!可能是灾难性回溯");
        }
    }
}

性能杀手三号:无锚点扫描

症状:正则没有锚点,导致扫描整个字符串。

java
public class NoAnchorProblem {
    public static void main(String[] args) {
        String text = "abcdefghijklmnopqrstuvwxyz";
        
        // ❌ 无锚点:需要扫描整个字符串
        String noAnchor = "\\d{3}"; // 要找三个连续数字
        long start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            Pattern.compile(noAnchor).matcher(text).find();
        }
        System.out.println("无锚点: " + (System.nanoTime() - start) / 1_000_000 + " ms");
        
        // ✅ 有锚点:快速失败
        String withAnchor = "^\\d{3}"; // 字符串开头必须是三个数字
        start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            Pattern.compile(withAnchor).matcher(text).find();
        }
        System.out.println("有锚点: " + (System.nanoTime() - start) / 1_000_000 + " ms");
        
        // ✅ 完全锚定:最快的验证
        String fullAnchor = "^\\d{3}$";
        start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            Pattern.compile(fullAnchor).matcher(text).matches();
        }
        System.out.println("完全锚定: " + (System.nanoTime() - start) / 1_000_000 + " ms");
    }
}

锚点优化实战

java
public class AnchorOptimization {
    public static void main(String[] args) {
        // 场景:验证手机号
        
        // ❌ 慢
        String slow = "1[3-9]\\d{9}"; // 没有锚点
        
        // ✅ 快:加开头锚点
        String fast = "^1[3-9]\\d{9}"; // 开头锚点
        
        // ✅ 最快:完全锚定
        String fastest = "^1[3-9]\\d{9}$"; // 开头+结尾锚点
    }
}

性能杀手四号:贪婪陷阱

症状:贪婪量词匹配过多内容,导致大量回溯。

java
public class GreedyTrap {
    public static void main(String[] args) {
        String text = "hello \"world\" and 'universe'";
        
        // ❌ 贪婪:匹配过多
        // ".+" 匹配到 "world" and 'universe'
        // 然后发现后面没有引号
        // 回溯,逐字符退让
        Pattern greedy = Pattern.compile("\"(.+)\"");
        System.out.println("贪婪匹配: " + greedy.matcher(text).find());
        // 输出: true,匹配 "world" and 'universe'
        
        // ✅ 惰性:按需匹配
        // ".+?" 匹配 "world",然后发现后面是 "
        Pattern lazy = Pattern.compile("\"(.+?)\"");
        Matcher m = lazy.matcher(text);
        while (m.find()) {
            System.out.println("惰性匹配: " + m.group(1));
        }
        // 输出: world
        //       universe
        
        // ✅ 字符类(最快)
        Pattern charClass = Pattern.compile("\"([^\"]+)\"");
        m = charClass.matcher(text);
        while (m.find()) {
            System.out.println("字符类: " + m.group(1));
        }
        // 输出: world
        //       universe
    }
}

最佳实践:字符类优先

java
public class CharClassBestPractice {
    public static void main(String[] args) {
        // 匹配引号内的内容
        
        // 最慢:`.+`
        // 较快:`.*?`
        // 最快:`[^"]+`(字符类)
        
        // 匹配标签内的内容
        
        // 最慢:`<.+>`
        // 较快:`<.+?>`
        // 最快:`<[^>]+>`
        
        // 匹配数字
        
        // 最慢:`[0-9]+`(字符集)
        // 最快:`\\d+`(预定义字符类)
    }
}

性能杀手五号:分支顺序

症状:或运算的分支没有按频率排序。

java
public class BranchOrderProblem {
    public static void main(String[] args) {
        // ❌ 低效:常见分支放后面
        String inefficient = "(rare_word1|rare_word2|common_word|very_common)";
        
        // ✅ 高效:常见分支放前面
        String efficient = "(very_common|common_word|rare_word2|rare_word1)";
        
        String input = "very_common";
        
        long start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            Pattern.matches(inefficient, input);
        }
        System.out.println("低效分支: " + (System.nanoTime() - start) / 1_000_000 + " ms");
        
        start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            Pattern.matches(efficient, input);
        }
        System.out.println("高效分支: " + (System.nanoTime() - start) / 1_000_000 + " ms");
    }
}

分支优化技巧

java
public class BranchOptimization {
    public static void main(String[] args) {
        // ❌ 低效:重复前缀
        String bad = "(catalog|category|catch|case)";
        
        // ✅ 优化:提取公共前缀
        String good = "cat(alog|egory|ch|se)";
        
        // ✅ 更好:让公共前缀本身也能匹配
        String better = "cat(?:egory|alog|ch|se)?";
        
        String input = "category";
        System.out.println("优化后: " + Pattern.matches(good, input)); // true
    }
}

实战优化:日志分析场景

java
public class LogAnalysisOptimization {
    // ❌ 优化前
    private static final String SLOW_PATTERN = ".*\\[(\\w+)\\] (ERROR|WARN|INFO|DEBUG).*";
    
    // ✅ 优化后
    private static final Pattern FAST_PATTERN = Pattern.compile(
        "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} (ERROR|WARN|INFO|DEBUG) \\[(\\w+)\\] .*"
    );
    
    public static void main(String[] args) {
        String[] logs = {
            "2026-03-22 10:30:15 ERROR [OrderService] 订单失败",
            "2026-03-22 10:30:16 INFO [UserService] 用户登录",
            "2026-03-22 10:30:17 WARN [PaymentService] 支付超时"
        };
        
        // 预热
        for (int i = 0; i < 1000; i++) {
            for (String log : logs) {
                Pattern.matches(SLOW_PATTERN, log);
                FAST_PATTERN.matcher(log).matches();
            }
        }
        
        // 基准测试
        long iterations = 100_000;
        
        long start = System.nanoTime();
        for (long i = 0; i < iterations; i++) {
            for (String log : logs) {
                Pattern.matches(SLOW_PATTERN, log);
            }
        }
        long slow = System.nanoTime() - start;
        
        start = System.nanoTime();
        for (long i = 0; i < iterations; i++) {
            for (String log : logs) {
                FAST_PATTERN.matcher(log).matches();
            }
        }
        long fast = System.nanoTime() - start;
        
        System.out.println("优化前: " + slow / 1_000_000 + " ms");
        System.out.println("优化后: " + fast / 1_000_000 + " ms");
        System.out.println("提升: " + (double) slow / fast + "x");
    }
}

性能检查清单

□ 预编译了吗?
  └─ Pattern.compile() 只做一次,放到 static 字段
  
□ 有灾难性回溯吗?
  └─ 避免 (a+)+, (a*)+, (.*)* 等嵌套量词
  
□ 有锚点吗?
  └─ 验证场景加 ^ 和 $
  
□ 量词是贪婪还是惰性?
  └─ 能用字符类就不用 .+
  
□ 分支顺序合理吗?
  └─ 常见选项放前面,提取公共前缀
  
□ 用的是预定义字符类吗?
  └─ \d 优于 [0-9],\w 优于 [a-zA-Z0-9_]
  
□ 避免不必要的捕获了吗?
  └─ 不需要捕获时用 (?:...)

总结

正则性能优化的核心:

  1. 预编译是王道 — 重复使用的正则一定要预编译
  2. 避免回溯 — 不要嵌套量词
  3. 锚点加速 — 验证时加 ^$
  4. 惰性替换贪婪 — 用 +?[^"]+ 代替 .+
  5. 顺序优化 — 常见分支放前面
  6. 预定义优先\d 优于 [0-9]

记住:正则性能问题往往是被「正常数据」放大 100 倍的极端案例。

基于 VitePress 构建