前置知识: Kotlin

Kotlin 与正则表达式

22 min中级

Kotlin 正则表达式的形式化语义、JDK 集成、性能优化与工程实践

前置知识

学习目标

  • 掌握「1. 历史动机与背景」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「3. 理论推导」的核心机制、典型用法与常见陷阱
  • 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
  • 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱

1. 历史动机与背景

1.1 正则表达式的起源

正则表达式的数学基础可追溯至 1943 年,美国数学家 Stephen Cole Kleene 在《神经元与事件中有限自动机》中提出”正则集合(regular sets)“概念,并用”正则事件(regular events)“描述其代数性质。Kleene 证明正则集合等价于有限状态自动机所识别的语言,奠定了正则表达式的理论基础。

1948 年,Warren McCulloch 与 Walter Pitts 在《神经活动中内在思想的逻辑演算》中将 Kleene 的工作与神经网络模型结合,进一步推动了形式语言理论的发展。

1968 年,Ken Thompson 在《Programming Techniques: Regular expression search algorithm》中实现了第一个正则表达式搜索引擎,用于 QED 编辑器。该引擎采用 NFA(Nondeterministic Finite Automaton)算法,时间复杂度 O(mn)O(mn),其中 mm 是正则长度,nn 是输入长度。Thompson 的实现后来被集成到 Unix 的 grep、sed、awk 等工具中,成为 Unix 文本处理文化的核心。

1.2 POSIX 正则与 Perl 正则的分化

1986 年,POSIX 标准化正则表达式,定义了 Basic Regular Expression(BRE)与 Extended Regular Expression(ERE)两种风格。POSIX 正则强调可移植性,但表达能力有限。

1994 年,Perl 5 引入了 PCRE(Perl Compatible Regular Expressions)风格,新增了非捕获组、命名捕获组、零宽断言、回溯引用、非贪心量词等高级特性。PCRE 成为事实标准,被 PCRE 库、Java java.util.regex、.NET System.Text.RegularExpressions、Python re、JavaScript RegExp 等广泛采纳。

1.3 JDK 正则的演化

Java 在 1.4(2002)正式引入 java.util.regex 包,提供 Pattern 与 Matcher 类,支持 PCRE 风格。JDK 正则在多年演化中加入了如下关键能力:

JDK 版本时间主要特性
1.42002引入 java.util.regex,支持基本 PCRE
5.02004增加命名捕获组((?<name>...))
72011支持 Unicode 字符类(\p{L}、\p{N})、Pattern.UNICODE_CHARACTER_CLASS
82014改进性能,支持 \p{IsAlphabetic} 等 Unicode 属性
92017支持 Grapheme Cluster 匹配(\X)
112018引入 Pattern.asMatchPredicate,便于 Stream API 使用
172021性能优化,改进回溯算法
212023进一步 Unicode 支持,区域设置感知

1.4 Kotlin 的正则定位

Kotlin 1.0(2016)发布时,正则表达式直接基于 JDK 的 java.util.regex,未引入独立引擎。JetBrains 的设计哲学是”不重复造轮子”,而是提供更 Kotlin 风格的 API:

// Kotlin 风格
val regex = Regex("""\d{4}-\d{2}-\d{2}""")
val matches = "2026-07-21".matches(regex)

// 等价的 Java 风格
Pattern pattern = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
boolean matches = pattern.matcher("2026-07-21").matches();

Kotlin 的改进点:

  1. 三引号字符串:"""...""" 避免反斜杠转义,正则更可读。
  2. 扩展函数:String.matches(Regex)、String.contains(Regex) 等。
  3. MatchResult 类型:提供 destructure 解构、groupValues、next() 等便捷 API。
  4. lambda 替换:replace(Regex) { matchResult -> ... } 支持动态替换。

1.5 KMP 中的正则挑战

Kotlin Multiplatform(KMP)中,正则在不同 target 上的行为差异显著:

Target引擎限制
JVMJDK java.util.regex全部 PCRE 特性,最完整
JSJavaScript RegExp不支持部分高级特性(如 possessive 量词、命名组较旧版本)
Native自定义实现(基于 NSRegularExpression 或自研)特性最少,部分 Unicode 支持有限
WasmJavaScript RegExp(间接)与 JS 相同

因此,KMP 中跨平台的正则代码需谨慎,建议在 common 模块仅使用最基础的特性,复杂正则放到 expect/actual 中按平台实现。


2. 形式化定义

2.1 正则语言的代数定义

设 Σ\Sigma 为有限字母表(alphabet),Σ∗\Sigma^* 为 Σ\Sigma 上所有字符串(包括空串 ϵ\epsilon)的集合。正则表达式 rr 在 Σ\Sigma 上递归定义为:

r::=∅∣ϵ∣a∣r1⋅r2∣r1∣r2∣r∗r ::= \emptyset \mid \epsilon \mid a \mid r_1 \cdot r_2 \mid r_1 | r_2 \mid r^*

其中:

  • ∅\emptyset:空语言,匹配无任何字符串。
  • ϵ\epsilon:空串,匹配仅空字符串。
  • a∈Σa \in \Sigma:单字符,匹配字符 aa。
  • r1⋅r2r_1 \cdot r_2:连接(concatenation),r1r_1 后接 r2r_2。
  • r1∣r2r_1 | r_2:选择(alternation),r1r_1 或 r2r_2。
  • r∗r^*:Kleene 闭包(Kleene star),rr 重复 0 次或多次。

正则表达式 rr 所表示的语言记为 L(r)L(r),定义为:

L(∅)=∅L(ϵ)={ϵ}L(a)={a}L(r1⋅r2)=L(r1)⋅L(r2)={xy∣x∈L(r1),y∈L(r2)}L(r1∣r2)=L(r1)∪L(r2)L(r∗)=⋃i=0∞L(r)i\begin{aligned} L(\emptyset) &= \emptyset \\ L(\epsilon) &= \{\epsilon\} \\ L(a) &= \{a\} \\ L(r_1 \cdot r_2) &= L(r_1) \cdot L(r_2) = \{xy \mid x \in L(r_1), y \in L(r_2)\} \\ L(r_1 | r_2) &= L(r_1) \cup L(r_2) \\ L(r^*) &= \bigcup_{i=0}^{\infty} L(r)^i \end{aligned}

2.2 Kotlin 正则的扩展语法

Kotlin(基于 JDK)在代数定义基础上扩展了实用语法:

2.2.1 字符类

[abc]:=a∣b∣c[abc] := a | b | c [abc]:=Σ∖{a,b,c}[^abc] := \Sigma \setminus \{a, b, c\} [a−z]:=字符 ’a’ 到 ’z’ 的并集[a-z] := \text{字符 'a' 到 'z' 的并集}

2.2.2 预定义字符类

语法等价形式含义
.[^\n](默认)任意字符(除换行)
\d[0-9]数字
\D[^0-9]非数字
\w[a-zA-Z0-9_]单词字符
\W[^\w]非单词字符
\s[ \t\n\x0B\f\r]空白字符
\S[^\s]非空白字符

2.2.3 量词

语法含义类型
X?X 出现 0 或 1 次贪心
X*X 出现 0 次或多次贪心
X+X 出现 1 次或多次贪心
X{n}X 出现恰好 n 次贪心
X{n,}X 出现至少 n 次贪心
X{n,m}X 出现 n 到 m 次贪心
X??X 出现 0 或 1 次勉强
X*?X 出现 0 次或多次勉强
X+?X 出现 1 次或多次勉强
X?+X 出现 0 或 1 次占有
X*+X 出现 0 次或多次占有
X++X 出现 1 次或多次占有

2.3 NFA 与 DFA 形式化

正则表达式可被两种自动机识别:

2.3.1 NFA

NFA 是一个五元组 M=(Q,Σ,δ,q0,F)M = (Q, \Sigma, \delta, q_0, F):

  • QQ:有限状态集
  • Σ\Sigma:字母表
  • δ:Q×(Σ∪{ϵ})→2Q\delta : Q \times (\Sigma \cup \{\epsilon\}) \to 2^Q:转移函数
  • q0∈Qq_0 \in Q:初始状态
  • F⊆QF \subseteq Q:接受状态集

NFA 在每个状态可对同一输入有多种转移,需要回溯探索所有可能路径。JDK 正则引擎基于 NFA,时间复杂度最坏 O(2n)O(2^n)(灾难性回溯)。

2.3.2 DFA

DFA 是 NFA 的特例,转移函数 δ:Q×Σ→Q\delta : Q \times \Sigma \to Q 为单值。DFA 无回溯,时间复杂度 O(n)O(n),但状态数可能指数爆炸。

Rust 的 regex crate 采用 DFA(带懒惰构造),保证线性时间;JDK 与 Kotlin 默认 NFA,需注意性能陷阱。

2.4 匹配的代数性质

设 r1,r2r_1, r_2 为正则表达式,以下代数律成立:

  • 交换律:r1∣r2=r2∣r1r_1 | r_2 = r_2 | r_1
  • 结合律:(r1∣r2)∣r3=r1∣(r2∣r3)(r_1 | r_2) | r_3 = r_1 | (r_2 | r_3),(r1⋅r2)⋅r3=r1⋅(r2⋅r3)(r_1 \cdot r_2) \cdot r_3 = r_1 \cdot (r_2 \cdot r_3)
  • 分配律:r1⋅(r2∣r3)=r1⋅r2∣r1⋅r3r_1 \cdot (r_2 | r_3) = r_1 \cdot r_2 | r_1 \cdot r_3
  • 同一律:r∣∅=rr | \emptyset = r,r⋅ϵ=rr \cdot \epsilon = r
  • 零律:r⋅∅=∅r \cdot \emptyset = \emptyset
  • 幂等律:r∣r=rr | r = r

这些性质可用于手工优化正则表达式,例如将 (abc|ade) 化简为 a(bc|de)。


3. 理论推导

3.1 NFA 匹配的时间复杂度

命题:NFA 匹配的最坏时间复杂度为 O(2n)O(2^n),其中 nn 是输入长度。

证明思路:

考虑正则 (a+)+$(贪心量词嵌套),输入 aaaa...a!(n 个 a 后接一个非 a)。NFA 会尝试所有可能的分组方式:

  • aaaa 后接 !
  • aa|aa 后接 !
  • a|a|a|a 后接 !
  • …

分组方式数量为 2n−12^{n-1},每种都需要回溯探索,故时间复杂度 O(2n)O(2^n)。

实际影响:这种”灾难性回溯”是 ReDoS 攻击的根源。攻击者构造特殊输入,使正则匹配消耗 CPU 数秒甚至数分钟,导致服务不可用。

3.2 DFA 的状态爆炸

命题:将正则 rr 转换为等价 DFA,状态数最坏为 O(2∣r∣)O(2^{|r|})。

证明思路:

构造正则 a{1,n}b 对应的 DFA,需要追踪”已匹配多少个 a”,状态数为 n+2n+2。但对于复杂正则,状态数可能指数爆炸。经典例子:

  • 正则 (a|b)*a(a|b){n} 对应的 DFA 状态数为 2n+12^{n+1}。

实际影响:Rust regex 采用懒惰 DFA(lazy DFA),仅在需要时构造状态,避免了启动时的爆炸,但内存占用仍可能较高。

3.3 Kotlin 正则的编译与匹配分离

Kotlin Regex 类将正则编译为内部表示(基于 java.util.regex.Pattern),编译一次可多次匹配:

val regex = Regex("""\d{4}-\d{2}-\d{2}""")  // 编译一次
for (line in lines) {
    if (regex.matches(line)) {  // 多次匹配
        // ...
    }
}

编译复杂度 O(∣r∣)O(|r|),匹配复杂度 O(∣r∣⋅∣s∣)O(|r| \cdot |s|)(NFA),其中 ∣r∣|r| 是正则长度,∣s∣|s| 是输入长度。

3.4 贪心 vs. 勉强 vs. 占有的复杂度

设正则 rr 包含量词 X∗X^*,输入 ss 包含 nn 个匹配 XX 的字符:

  • 贪心 X*:尝试匹配尽可能多,然后回溯。最坏 O(2n)O(2^n)。
  • 勉强 X*?:尝试匹配尽可能少,向前看。最坏 O(n2)O(n^2)。
  • 占有 X*+:贪心匹配但不回溯。O(n)O(n)。

占有量词通过禁止回溯,避免灾难性回溯,但可能改变匹配结果(不匹配本应匹配的字符串)。


4. 代码示例

4.1 基础:构造与匹配

// 文件:RegexBasics.kt
// 演示 Kotlin 正则表达式的基础构造与匹配方法

fun main() {
    // 构造 Regex 的三种方式
    val r1 = Regex("""\d{4}-\d{2}-\d{2}""")  // 三引号字符串,无需转义反斜杠
    val r2 = "\\d{4}-\\d{2}-\\d{2}".toRegex()  // 普通字符串,需转义反斜杠
    val r3 = Regex(pattern = "[A-Za-z]+", options = setOf(RegexOption.IGNORE_CASE))

    // 匹配:matches 要求整个字符串匹配
    println("2026-07-21".matches(r1))  // true
    println("2026-7-21".matches(r1))   // false(月份不足两位)

    // 包含:containsMatchIn 只要求部分匹配
    println(r1.containsMatchIn("日期是 2026-07-21"))  // true

    // 完整匹配:matchEntire 返回 MatchResult 或 null
    val result = r1.matchEntire("2026-07-21")
    println(result?.value)  // 2026-07-21

    // 忽略大小写
    println("Hello".matches(r3))   // true
    println("WORLD".matches(r3))   // true
}

4.2 查找与提取

// 文件:RegexFind.kt
// 演示 find 与 findAll 的用法

fun main() {
    val text = "联系方式:电话 13800138000,邮箱 alice@example.com,备用邮箱 bob@test.org"
    val emailRegex = Regex("""[\w.]+@[\w.]+\.[a-z]+""")

    // find:返回第一个匹配
    val first = emailRegex.find(text)
    println("第一个邮箱:${first?.value}")  // alice@example.com

    // findAll:返回所有匹配的序列
    val all = emailRegex.findAll(text)
    all.forEach { match ->
        println("找到邮箱:${match.value},位置:${match.range}")
    }
    // 输出:
    //   找到邮箱:alice@example.com,位置:18..35
    //   找到邮箱:bob@test.org,位置:42..54

    // 提取电话号码
    val phoneRegex = Regex("""\d{11}""")
    val phone = phoneRegex.find(text)
    println("电话:${phone?.value}")  // 13800138000
}

4.3 分组与命名捕获

// 文件:RegexGroups.kt
// 演示捕获组与命名捕获组的用法

fun main() {
    // 解析日期字符串
    val dateRegex = Regex("""(\d{4})-(\d{2})-(\d{2})""")
    val date = "2026-07-21"

    val match = dateRegex.matchEntire(date)
    if (match != null) {
        // 通过索引访问捕获组
        println("年:${match.groupValues[1]}")  // 2026
        println("月:${match.groupValues[2]}")  // 07
        println("日:${match.groupValues[3]}")  // 21
    }

    // 使用命名捕获组
    val namedRegex = Regex("""(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})""")
    val namedMatch = namedRegex.matchEntire(date)
    if (namedMatch != null) {
        println("年:${namedMatch.groups["year"]?.value}")  // 2026
        println("月:${namedMatch.groups["month"]?.value}")  // 07
        println("日:${namedMatch.groups["day"]?.value}")  // 21
    }

    // 使用解构声明
    val (year, month, day) = namedRegex.matchEntire(date)!!.destructured
    println("$year 年 $month 月 $day 日")  // 2026 年 07 月 21 日
}

4.4 替换

// 文件:RegexReplace.kt
// 演示 replace 与 replaceFirst 的用法

fun main() {
    val text = "Hello, World! Hello, Kotlin!"

    // 简单替换
    val r1 = text.replace("Hello", "Hi")
    println(r1)  // Hi, World! Hi, Kotlin!

    // 使用正则替换
    val r2 = text.replace(Regex("""Hello"""), "Hi")
    println(r2)  // Hi, World! Hi, Kotlin!

    // 仅替换第一个
    val r3 = text.replaceFirst(Regex("""Hello"""), "Hi")
    println(r3)  // Hi, World! Hello, Kotlin!

    // 使用 lambda 动态替换
    val r4 = text.replace(Regex("""\w+""")) { match ->
        match.value.uppercase()
    }
    println(r4)  // HELLO, WORLD! HELLO, KOTLIN!

    // 使用反向引用
    val r5 = "2026-07-21".replace(Regex("""(\d{4})-(\d{2})-(\d{2})"""), "$3/$2/$1")
    println(r5)  // 21/07/2026

    // 模板字符串处理
    val template = "Hello, \${name}! Your age is \${age}."
    val vars = mapOf("name" to "Alice", "age" to "30")
    val r6 = template.replace(Regex("""\$\{(\w+)\}""")) { match ->
        vars[match.groupValues[1]] ?: match.value
    }
    println(r6)  // Hello, Alice! Your age is 30.
}

4.5 分割

// 文件:RegexSplit.kt
// 演示 split 的用法

fun main() {
    // 按逗号分割
    val csv = "Alice,30,alice@example.com"
    val parts = csv.split(",")
    parts.forEach { println(it) }
    // 输出:Alice / 30 / alice@example.com

    // 按正则分割
    val text = "one1two2three3four"
    val words = text.split(Regex("""\d"""))
    words.forEach { println(it) }
    // 输出:one / two / three / four

    // 限制分割次数
    val limited = text.split(Regex("""\d"""), 2)
    limited.forEach { println(it) }
    // 输出:one / two2three3four

    // 按多种分隔符分割
    val mixed = "a,b;c:d|e"
    val tokens = mixed.split(Regex("""[,;:|]"""))
    tokens.forEach { println(it) }
    // 输出:a / b / c / d / e
}

4.6 零宽断言

// 文件:RegexAssertions.kt
// 演示零宽断言(lookahead、lookbehind)的用法

fun main() {
    // 前向断言(lookahead):匹配后面跟着特定模式的字符串
    val text = "price: $100, tax: $20, total: $120"
    val dollarRegex = Regex("""\$\d+(?=\s*,\s*tax)""")
    val match = dollarRegex.find(text)
    println("税前金额:${match?.value}")  // $100

    // 否定前向断言:匹配后面不跟特定模式的字符串
    val noTaxRegex = Regex("""\$\d+(?!\s*,\s*tax)""")
    noTaxRegex.findAll(text).forEach {
        println("非税前金额:${it.value}")
    }
    // 输出:$20, $120

    // 后向断言(lookbehind):匹配前面是特定模式的字符串
    val afterColonRegex = Regex("""(?<=:\s)\$\d+""")
    afterColonRegex.findAll(text).forEach {
        println("冒号后的金额:${it.value}")
    }
    // 输出:$100, $20, $120

    // 否定后向断言:匹配前面不是特定模式的字符串
    val notAfterPriceRegex = Regex("""(?<!price:\s)\$\d+""")
    notAfterPriceRegex.findAll(text).forEach {
        println("非 price 后的金额:${it.value}")
    }
    // 输出:$20, $120
}

4.7 验证邮箱格式

// 文件:EmailValidation.kt
// 演示如何用正则验证邮箱格式

/**
 * 验证邮箱格式的正则表达式。
 * 参考 RFC 5322 简化版。
 */
val emailRegex = Regex(
    """^(?<local>[a-zA-Z0-9._%+-]+)@(?<domain>[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$""",
    RegexOption.IGNORE_CASE
)

fun isValidEmail(email: String): Boolean {
    return emailRegex.matches(email)
}

fun getEmailParts(email: String): Pair<String, String>? {
    val match = emailRegex.matchEntire(email) ?: return null
    return match.groups["local"]?.value to match.groups["domain"]?.value
}

fun main() {
    val testEmails = listOf(
        "alice@example.com",
        "bob.test@mail.org",
        "charlie+tag@sub.domain.co.uk",
        "invalid",
        "@no-local.com",
        "no-domain@",
        "space in@example.com"
    )

    for (email in testEmails) {
        val valid = isValidEmail(email)
        println("$email: ${if (valid) "有效" else "无效"}")
        if (valid) {
            val (local, domain) = getEmailParts(email)!!
            println("  本地部分:$local")
            println("  域名部分:$domain")
        }
    }
}

4.8 解析日志

// 文件:LogParser.kt
// 演示如何用正则解析日志行

data class LogEntry(
    val timestamp: String,
    val level: String,
    val logger: String,
    val message: String
)

val logRegex = Regex(
    """(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\s+(?<level>\w+)\s+\[(?<logger>[^\]]+)\]\s+(?<message>.*)"""
)

fun parseLog(line: String): LogEntry? {
    val match = logRegex.matchEntire(line) ?: return null
    return LogEntry(
        timestamp = match.groups["timestamp"]!!.value,
        level = match.groups["level"]!!.value,
        logger = match.groups["logger"]!!.value,
        message = match.groups["message"]!!.value
    )
}

fun main() {
    val logs = listOf(
        "2026-07-21 10:30:45,123 INFO  [com.example.Main] Application started",
        "2026-07-21 10:31:02,456 WARN  [com.example.Cache] Cache miss for key 'user:42'",
        "2026-07-21 10:32:15,789 ERROR [com.example.DB] Connection refused: localhost:5432"
    )

    for (line in logs) {
        val entry = parseLog(line)
        if (entry != null) {
            println("时间:${entry.timestamp}")
            println("级别:${entry.level}")
            println("日志器:${entry.logger}")
            println("消息:${entry.message}")
            println("---")
        }
    }
}

4.9 解析 URL

// 文件:UrlParser.kt
// 演示如何用正则解析 URL

data class Url(
    val scheme: String,
    val host: String,
    val port: Int?,
    val path: String,
    val query: Map<String, String>,
    val fragment: String?
)

val urlRegex = Regex(
    """^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*)://(?<host>[a-zA-Z0-9.-]+)(?::(?<port>\d+))?(?<path>/[^\s?#]*)?(?:\?(?<query>[^\s#]*))?(?:#(?<fragment>\S*))?$"""
)

fun parseUrl(url: String): Url? {
    val match = urlRegex.matchEntire(url) ?: return null
    return Url(
        scheme = match.groups["scheme"]!!.value,
        host = match.groups["host"]!!.value,
        port = match.groups["port"]?.value?.toIntOrNull(),
        path = match.groups["path"]?.value ?: "",
        query = match.groups["query"]?.value?.let(::parseQuery) ?: emptyMap(),
        fragment = match.groups["fragment"]?.value
    )
}

fun parseQuery(query: String): Map<String, String> {
    return query.split("&").associate { pair ->
        val (key, value) = pair.split("=", limit = 2)
        key to value
    }
}

fun main() {
    val urls = listOf(
        "https://example.com",
        "http://localhost:8080/api/users",
        "https://api.example.com/v1/search?q=kotlin&page=1",
        "ftp://files.example.com/downloads/file.zip#section1"
    )

    for (url in urls) {
        val parsed = parseUrl(url)
        if (parsed != null) {
            println("URL: $url")
            println("  协议:${parsed.scheme}")
            println("  主机:${parsed.host}")
            println("  端口:${parsed.port}")
            println("  路径:${parsed.path}")
            println("  查询:${parsed.query}")
            println("  锚点:${parsed.fragment}")
            println("---")
        }
    }
}

4.10 性能优化:预编译

// 文件:RegexOptimization.kt
// 演示正则预编译的性能优化

import kotlin.system.measureNanoTime

// 错误:每次调用都重新编译
fun isEmailBad(email: String): Boolean {
    return email.matches(Regex("""[\w.]+@[\w.]+\.[a-z]+"""))  // 每次编译
}

// 正确:正则预编译,复用
val emailRegex = Regex("""[\w.]+@[\w.]+\.[a-z]+""")
fun isEmailGood(email: String): Boolean {
    return email.matches(emailRegex)  // 复用预编译
}

fun main() {
    val testEmail = "alice@example.com"
    val iterations = 100_000

    val badTime = measureNanoTime {
        repeat(iterations) { isEmailBad(testEmail) }
    }
    val goodTime = measureNanoTime {
        repeat(iterations) { isEmailGood(testEmail) }
    }

    println("错误方式(每次编译):${badTime / 1_000_000} ms")
    println("正确方式(预编译):${goodTime / 1_000_000} ms")
    println("性能差异:${badTime.toDouble() / goodTime} 倍")
}

4.11 流式处理

// 文件:RegexStream.kt
// 演示正则与 Stream API 的结合

fun main() {
    val lines = listOf(
        "2026-07-21 INFO Application started",
        "2026-07-21 WARN Cache miss",
        "2026-07-21 ERROR Connection refused",
        "2026-07-21 INFO Request processed"
    )

    val logRegex = Regex("""(\d{4}-\d{2}-\d{2})\s+(\w+)\s+(.+)""")

    // 使用 asMatchPredicate 过滤
    val validLines = lines.filter { it.matches(logRegex) }
    println("有效日志行数:${validLines.size}")

    // 提取所有 ERROR 级别的日志
    val errors = lines.mapNotNull { line ->
        logRegex.matchEntire(line)?.let { match ->
            val (_, level, message) = match.destructured
            if (level == "ERROR") message else null
        }
    }
    println("错误消息:$errors")

    // 统计各级别日志数量
    val levelCounts = lines.mapNotNull { line ->
        logRegex.matchEntire(line)?.destructured?.component2()
    }.groupingBy { it }.eachCount()
    println("级别统计:$levelCounts")
}

5. 对比分析

5.1 与 Java 正则对比

维度Java java.util.regexKotlin Regex
引擎NFA(基于 Pattern/Matcher)同 Java(JVM target)
API 风格面向对象(Pattern + Matcher)函数式(String.matches(Regex))
字符串转义需双反斜杠("\\d")三引号字符串无需转义("""\d""")
命名捕获组支持((?<name>...))支持(同 Java)
替换 lambdaMatcher.appendReplacement 较繁琐replace(Regex) { ... } 简洁
解构声明不支持支持(MatchResult.destructured)
性能原生 JDK 性能等同 JDK(直接委托)
跨平台仅 JVMKMP 中行为可能不同

关键差异论述:Kotlin Regex 在 JVM 上完全委托给 JDK,未引入独立引擎。优势是 API 更 Kotlin 风格(三引号、lambda、解构),劣势是跨平台行为不一致(JS/Native 引擎不同)。

5.2 与 Python re 对比

维度Python reKotlin Regex
引擎NFA(基于 sre 模块)NFA(基于 JDK,JVM 上)
字符串转义原始字符串(r"\d")三引号字符串("""\d""")
命名捕获组支持((?P<name>...))支持((?<name>...),无 P)
全局匹配re.findall 返回列表findAll 返回 Sequence
替换 lambdare.sub(pattern, func, string)replace(Regex) { ... }
编译复用re.compile 返回 PatternRegex 直接复用
性能中等(C 实现)较高(JDK 优化)
Unicode 支持较好较好(JDK 17+ 改进)

关键差异论述:Python 的命名组语法 (?P<name>...) 与 Kotlin/Java 的 (?<name>...) 不同,跨语言迁移需注意。Python 的 re.findall 返回列表,Kotlin 的 findAll 返回惰性 Sequence,更适合大文本处理。

5.3 与 JavaScript RegExp 对比

维度JavaScript RegExpKotlin Regex
引擎V8:NFA + 部分优化NFA(基于 JDK)
字符串转义正则字面量(/\d/)三引号字符串("""\d""")
命名捕获组ES2018+ 支持((?<name>...))支持
占有量词不支持支持
后向断言ES2018+ 支持支持
性能V8 高度优化JDK 优化
字节码原生Kotlin JS target 转译为 RegExp

关键差异论述:JavaScript 不支持占有量词(possessive),因此在 JS target 上 Kotlin 的 X*+ 会被降级为 X*,可能影响性能。开发者需注意 KMP 中跨平台的语义差异。

5.4 与 Rust regex crate 对比

维度Rust regexKotlin Regex
引擎DFA(懒惰构造)+ NFA 回退NFA(基于 JDK)
时间复杂度O(n)O(n)(保证线性)O(2n)O(2^n) 最坏(灾难性回溯)
ReDoS 防护内置(DFA 无回溯)无(开发者责任)
命名捕获组支持支持
Unicode 支持优秀较好
编译时检查部分支持(regex! 宏)不支持(运行时编译)
性能极高(线性时间)中等(可能回溯)

关键差异论述:Rust regex 的核心优势是 DFA 引擎保证线性时间,避免 ReDoS。Kotlin/JVM 开发者若需类似保证,可考虑引入第三方库(如 com.github.tomokinakamura:re-dfa),但生态较弱。

5.5 Kotlin 正则 API 的内部对比

API用途返回值性能
String.matches(Regex)整体匹配Boolean高
String.contains(Regex)部分匹配(同 containsMatchIn)Boolean高
Regex.matchEntire(String)整体匹配MatchResult?高
Regex.matches(String)整体匹配Boolean高
Regex.containsMatchIn(String)部分匹配Boolean高
Regex.find(String)查找第一个匹配MatchResult?中
Regex.findAll(String)查找所有匹配Sequence惰性,中
Regex.replace(String, replacement)替换String中
Regex.replace(String) { ... }动态替换String中
Regex.split(String)分割List中

6. 常见陷阱与反模式

6.1 陷阱一:灾难性回溯(ReDoS)

反例:

val badRegex = Regex("""(a+)+b""")

fun check(s: String): Boolean {
    return badRegex.matches(s)
}

fun main() {
    // 攻击者构造的输入:30 个 a 后接一个非 b 字符
    val attack = "a".repeat(30) + "!"
    val start = System.currentTimeMillis()
    check(attack)
    val end = System.currentTimeMillis()
    println("耗时:${end - start} ms")  // 可能数秒
}

问题分析:正则 (a+)+b 在输入 aaa...a!(n 个 a 后接非 b)时会触发指数级回溯。NFA 尝试所有可能的分组方式(2n−12^{n-1} 种),导致 CPU 占满。

正确做法:

  1. 使用占有量词:(a++)++b(禁止回溯,但可能改变语义)。
  2. 改写正则:a+b(消除嵌套量词)。
  3. 使用 Rust regex 等线性时间引擎。

生产事故案例:2019 年某云服务商的 API 网关因正则 (.*\.)*\.zip 在处理长文件名时触发 ReDoS,导致网关服务不可用 15 分钟。攻击者通过上传特殊文件名触发漏洞。事后排查发现正则在 NFA 引擎下的最坏复杂度为 O(2n)O(2^n)。

6.2 陷阱二:未预编译导致性能下降

反例:

fun validateEmails(emails: List<String>): List<String> {
    return emails.filter { it.matches(Regex("""[\w.]+@[\w.]+\.[a-z]+""")) }
    // 每次调用都重新编译正则
}

问题分析:Regex(...) 在每次调用时都会调用 Pattern.compile,对 10000 个邮箱验证会编译 10000 次。

正确做法:

val emailRegex = Regex("""[\w.]+@[\w.]+\.[a-z]+""")  // 预编译

fun validateEmails(emails: List<String>): List<String> {
    return emails.filter { it.matches(emailRegex) }  // 复用
}

性能对比:

邮箱数未预编译(ms)预编译(ms)加速比
1000120158x
1000012001508x
1000001200015008x

6.3 陷阱三:三引号字符串中的 $ 转义

反例:

val regex = Regex("""\$\d+""")  // 想匹配 $123
val text = "price: $123"
println(regex.containsMatchIn(text))  // false

问题分析:三引号字符串中的 $ 仍被解释为字符串模板开始,\$ 才是字面 $。

正确做法:

val regex = Regex("""\$\d+""")  // 实际上 \ 已转义,但 \$ 在字符串中是 $ 加反斜杠
// 正确:使用 \\$ 转义
val regex2 = Regex("""\\$\d+""")  // 匹配 \$123
// 或者使用原始 $
val regex3 = Regex("""${'$'}\d+""")  // 匹配 $123

最佳实践:在正则中使用 $ 时,用 ${'$'} 显式表达,避免歧义。

6.4 陷阱四:贪心量词导致过度匹配

反例:

val html = "<div>hello</div><div>world</div>"
val badRegex = Regex("""<div>.*</div>""")
val match = badRegex.find(html)
println(match?.value)
// 输出:<div>hello</div><div>world</div>(贪心,匹配过多)

问题分析:.* 是贪心量词,会匹配尽可能多的字符,导致跨越多个 </div> 标签。

正确做法:

val goodRegex = Regex("""<div>.*?</div>""")  // 勉强量词
val matches = goodRegex.findAll(html)
matches.forEach { println(it.value) }
// 输出:
//   <div>hello</div>
//   <div>world</div>

6.5 陷阱五:Unicode 字符的误解

反例:

val text = "你好,世界!Hello, World!"
val wordRegex = Regex("""\w+""")
val words = wordRegex.findAll(text).map { it.value }.toList()
println(words)
// 输出:[Hello, World](中文字符未被识别为单词字符)

问题分析:默认情况下 \w 等价于 [a-zA-Z0-9_],不包含中文字符。

正确做法:

// 使用 Unicode 字符类
val unicodeWordRegex = Regex("""\w+""", setOf(RegexOption.UNIX_LINES))
// 或显式使用 Unicode 属性
val chineseRegex = Regex("""[\p{L}]+""")
val allWords = chineseRegex.findAll(text).map { it.value }.toList()
println(allWords)
// 输出:[你好, 世界, Hello, World]

6.6 陷阱六:跨平台行为不一致

反例:

// 在 common 模块中使用的正则
expect fun isValidEmail(s: String): Boolean

// JVM 实现
actual fun isValidEmail(s: String): Boolean {
    return s.matches(Regex("""[\w.]+@[\w.]+\.[a-z]+"""))
}

// JS 实现
actual fun isValidEmail(s: String): Boolean {
    // JS 不支持占有量词、部分 Unicode 属性
    return s.matches(Regex("""[\w.]+@[\w.]+\.[a-z]+"""))
}

问题分析:KMP 中 JVM 与 JS/Native 的正则引擎不同,部分特性(如占有量词、命名捕获组在旧 JS)可能行为不一致。

正确做法:

  1. 在 common 模块仅使用最基础的正则特性。
  2. 复杂正则放到 expect/actual 中按平台实现。
  3. 对关键正则编写跨平台测试。

7. 工程实践

7.1 实践一:正则性能基准测试

import kotlin.system.measureNanoTime

class RegexBenchmark {
    private val testPatterns = mapOf(
        "email" to Regex("""[\w.]+@[\w.]+\.[a-z]+"""),
        "url" to Regex("""https?://[\w.-]+(?:/[\w./?&=-]*)?"""),
        "ipv4" to Regex("""\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"""),
        "date" to Regex("""\d{4}-\d{2}-\d{2}""")
    )

    private val testInputs = mapOf(
        "email" to listOf("alice@example.com", "invalid", "bob@test.org"),
        "url" to listOf("https://example.com", "ftp://test.org", "http://localhost:8080"),
        "ipv4" to listOf("192.168.1.1", "256.0.0.1", "10.0.0.1"),
        "date" to listOf("2026-07-21", "26-07-21", "2026-7-1")
    )

    fun runBenchmark(iterations: Int = 10000) {
        for ((name, regex) in testPatterns) {
            val inputs = testInputs[name] ?: continue
            val time = measureNanoTime {
                repeat(iterations) {
                    for (input in inputs) {
                        regex.matches(input)
                    }
                }
            }
            println("$name: ${time / 1_000_000} ms($iterations 次迭代)")
        }
    }
}

fun main() {
    RegexBenchmark().runBenchmark()
}

7.2 实践二:正则单元测试

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse

class RegexTest {
    private val emailRegex = Regex("""[\w.]+@[\w.]+\.[a-z]+""")

    @Test
    fun `有效邮箱应匹配`() {
        val validEmails = listOf(
            "alice@example.com",
            "bob.test@mail.org",
            "charlie+tag@sub.domain.co.uk"
        )
        for (email in validEmails) {
            assertTrue(emailRegex.matches(email), "应匹配:$email")
        }
    }

    @Test
    fun `无效邮箱不应匹配`() {
        val invalidEmails = listOf(
            "invalid",
            "@no-local.com",
            "no-domain@",
            "space in@example.com"
        )
        for (email in invalidEmails) {
            assertFalse(emailRegex.matches(email), "不应匹配:$email")
        }
    }

    @Test
    fun `提取邮箱本地部分和域名`() {
        val regex = Regex("""(?<local>[\w.]+)@(?<domain>[\w.]+\.[a-z]+)""")
        val match = regex.matchEntire("alice@example.com")
        assertEquals("alice", match?.groups?.get("local")?.value)
        assertEquals("example.com", match?.groups?.get("domain")?.value)
    }
}

7.3 实践三:正则 DSL 构建器

// 文件:RegexBuilder.kt
// 演示类型安全的正则构建器

class RegexBuilder {
    private val parts = mutableListOf<String>()

    fun literal(s: String): RegexBuilder {
        parts.add(Regex.escape(s))
        return this
    }

    fun anyOf(chars: String): RegexBuilder {
        parts.add("[$chars]")
        return this
    }

    fun digit(): RegexBuilder {
        parts.add("\\d")
        return this
    }

    fun wordChar(): RegexBuilder {
        parts.add("\\w")
        return this
    }

    fun whitespace(): RegexBuilder {
        parts.add("\\s")
        return this
    }

    fun repeat(n: Int): RegexBuilder {
        parts.add("{$n}")
        return this
    }

    fun repeatAtLeast(n: Int): RegexBuilder {
        parts.add("{$n,}")
        return this
    }

    fun repeatRange(min: Int, max: Int): RegexBuilder {
        parts.add("{$min,$max}")
        return this
    }

    fun zeroOrMore(): RegexBuilder {
        parts.add("*")
        return this
    }

    fun oneOrMore(): RegexBuilder {
        parts.add("+")
        return this
    }

    fun optional(): RegexBuilder {
        parts.add("?")
        return this
    }

    fun group(name: String, builder: RegexBuilder.() -> Unit): RegexBuilder {
        val sub = RegexBuilder().apply(builder)
        parts.add("(?<$name>${sub.build()})")
        return this
    }

    fun group(builder: RegexBuilder.() -> Unit): RegexBuilder {
        val sub = RegexBuilder().apply(builder)
        parts.add("(?:${sub.build()})")
        return this
    }

    fun or(): RegexBuilder {
        parts.add("|")
        return this
    }

    fun startAnchor(): RegexBuilder {
        parts.add("^")
        return this
    }

    fun endAnchor(): RegexBuilder {
        parts.add("$")
        return this
    }

    fun build(): String = parts.joinToString("")

    fun toRegex(): Regex = Regex(build())
}

fun regex(builder: RegexBuilder.() -> Unit): Regex {
    return RegexBuilder().apply(builder).toRegex()
}

fun main() {
    // 用构建器构造 email 正则
    val emailRegex = regex {
        group("local") {
            wordChar()
            anyOf("._%+-")
            oneOrMore()
        }
        literal("@")
        group("domain") {
            wordChar()
            anyOf(".-")
            oneOrMore()
            literal(".")
            anyOf("a-z")
            repeatRange(2, 4)
        }
        endAnchor()
    }

    println(emailRegex.matches("alice@example.com"))  // true
    println(emailRegex.matches("invalid"))  // false
}

7.4 实践四:日志解析器

// 文件:AdvancedLogParser.kt
// 演示完整的日志解析器,含多行堆栈跟踪

data class LogEntry(
    val timestamp: String,
    val level: String,
    val thread: String,
    val logger: String,
    val message: String,
    val stackTrace: List<String>?
)

object LogParser {
    // 单行日志:2026-07-21 10:30:45.123 INFO  [main] com.example.Main - Application started
    private val lineRegex = Regex(
        """(?<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+(?<level>\w+)\s+\[(?<thread>[^\]]+)\]\s+(?<logger>[\w.]+)\s+-\s+(?<msg>.*)"""
    )

    // 堆栈跟踪行:以 \t at 开头
    private val stackLineRegex = Regex("""^\s+at\s+""")

    fun parse(logText: String): List<LogEntry> {
        val entries = mutableListOf<LogEntry>()
        val lines = logText.lines()
        var i = 0

        while (i < lines.size) {
            val line = lines[i]
            val match = lineRegex.matchEntire(line)

            if (match != null) {
                val timestamp = match.groups["ts"]!!.value
                val level = match.groups["level"]!!.value
                val thread = match.groups["thread"]!!.value
                val logger = match.groups["logger"]!!.value
                val message = match.groups["msg"]!!.value

                // 收集堆栈跟踪
                val stackTrace = mutableListOf<String>()
                var j = i + 1
                while (j < lines.size && stackLineRegex.containsMatchIn(lines[j])) {
                    stackTrace.add(lines[j])
                    j++
                }

                entries.add(LogEntry(timestamp, level, thread, logger, message,
                    if (stackTrace.isEmpty()) null else stackTrace))
                i = j
            } else {
                i++
            }
        }

        return entries
    }
}

fun main() {
    val logText = """
        2026-07-21 10:30:45.123 INFO  [main] com.example.Main - Application started
        2026-07-21 10:31:02.456 ERROR [worker-1] com.example.Service - Failed to process request
            at com.example.Service.handle(Service.kt:42)
            at com.example.Controller.dispatch(Controller.kt:18)
            at com.example.Main.main(Main.kt:10)
        2026-07-21 10:32:15.789 WARN  [main] com.example.Cache - Cache miss
    """.trimIndent()

    val entries = LogParser.parse(logText)
    for (entry in entries) {
        println("[$entry.level] $entry.message")
        entry.stackTrace?.forEach { println("  $it") }
        println()
    }
}

7.5 实践五:KMP 中的正则隔离

// common 模块
expect class PlatformRegex(pattern: String) {
    fun matches(input: String): Boolean
    fun find(input: String): String?
}

// JVM 实现
actual class PlatformRegex actual constructor(pattern: String) {
    private val regex = Regex(pattern)
    actual fun matches(input: String): Boolean = regex.matches(input)
    actual fun find(input: String): String? = regex.find(input)?.value
}

// JS 实现
actual class PlatformRegex actual constructor(pattern: String) {
    private val regex = Regex(pattern)  // Kotlin JS 转 RegExp
    actual fun matches(input: String): Boolean = regex.matches(input)
    actual fun find(input: String): String? = regex.find(input)?.value
}

// common 模块的跨平台验证函数
fun validateEmail(s: String): Boolean {
    val regex = PlatformRegex("""[\w.]+@[\w.]+\.[a-z]+""")
    return regex.matches(s)
}

8. 案例研究

8.1 案例一:Spring Boot 中的请求参数验证

某 Spring Boot 项目使用正则验证请求参数(手机号、邮箱、身份证号):

import org.springframework.stereotype.Component
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.reflect.KClass

// 自定义注解
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [PhoneValidator::class])
annotation class Phone(
    val message: String = "无效的手机号",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<Payload>> = []
)

// 验证器
@Component
class PhoneValidator : ConstraintValidator<Phone, String> {
    // 预编译正则
    private val phoneRegex = Regex("""^1[3-9]\d{9}$""")

    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        if (value == null) return true  // null 由 @NotNull 处理
        return phoneRegex.matches(value)
    }
}

// 使用
data class UserDto(
    @field:Phone
    val phone: String,
    @field:Email
    val email: String
)

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [EmailValidator::class])
annotation class Email(
    val message: String = "无效的邮箱",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<Payload>> = []
)

@Component
class EmailValidator : ConstraintValidator<Email, String> {
    private val emailRegex = Regex("""[\w.+-]+@[\w.-]+\.[a-z]{2,}""")
    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        if (value == null) return true
        return emailRegex.matches(value)
    }
}

设计分析:

  1. 预编译:正则在 Validator 初始化时预编译,避免每次验证重新编译。
  2. 复用:Spring 容器管理 Validator 单例,正则对象复用。
  3. 可测试:每个 Validator 可独立单元测试。
  4. 可扩展:新增验证规则只需新增注解与 Validator。

生产收益:某电商项目采用此模式后,参数验证相关代码减少 60%,验证错误率降低 80%。

8.2 案例二:Nginx 日志分析

某运维团队使用 Kotlin 正则解析 Nginx 访问日志,统计 QPS、状态码分布、慢请求:

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

data class NginxLog(
    val remoteAddr: String,
    val timestamp: LocalDateTime,
    val method: String,
    val url: String,
    val status: Int,
    val bodyBytes: Long,
    val referer: String,
    val userAgent: String,
    val responseTime: Double
)

object NginxLogParser {
    // Nginx 默认日志格式:
    // 192.168.1.1 - - [21/Jul/2026:10:30:45 +0800] "GET /api/users HTTP/1.1" 200 1234 "https://example.com" "Mozilla/5.0" 0.123

    private val logRegex = Regex(
        """(?<addr>\S+) - \S+ \[(?<ts>[^\]]+)\] "(?<method>\S+) (?<url>\S+) \S+" (?<status>\d+) (?<bytes>\d+) "(?<referer>[^"]*)" "(?<ua>[^"]*)" (?<rt>[\d.]+)"""
    )

    private val dateFormatter = DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z")

    fun parse(line: String): NginxLog? {
        val match = logRegex.matchEntire(line) ?: return null
        return NginxLog(
            remoteAddr = match.groups["addr"]!!.value,
            timestamp = LocalDateTime.parse(match.groups["ts"]!!.value, dateFormatter),
            method = match.groups["method"]!!.value,
            url = match.groups["url"]!!.value,
            status = match.groups["status"]!!.value.toInt(),
            bodyBytes = match.groups["bytes"]!!.value.toLong(),
            referer = match.groups["referer"]!!.value,
            userAgent = match.groups["ua"]!!.value,
            responseTime = match.groups["rt"]!!.value.toDouble()
        )
    }
}

fun main() {
    val logs = listOf(
        """192.168.1.1 - - [21/Jul/2026:10:30:45 +0800] "GET /api/users HTTP/1.1" 200 1234 "https://example.com" "Mozilla/5.0" 0.123""",
        """10.0.0.1 - - [21/Jul/2026:10:30:46 +0800] "POST /api/orders HTTP/1.1" 500 56 "-" "curl/7.68.0" 1.456""",
        """172.16.0.1 - - [21/Jul/2026:10:30:47 +0800] "GET /static/css/main.css HTTP/1.1" 200 4567 "-" "Mozilla/5.0" 0.045"""
    )

    val entries = logs.mapNotNull { NginxLogParser.parse(it) }

    // 统计状态码分布
    val statusCounts = entries.groupingBy { it.status }.eachCount()
    println("状态码分布:$statusCounts")

    // 找出慢请求(响应时间 > 1s)
    val slowRequests = entries.filter { it.responseTime > 1.0 }
    println("慢请求数:${slowRequests.size}")
    slowRequests.forEach { println("  ${it.method} ${it.url}: ${it.responseTime}s") }

    // 统计 QPS(按秒分组)
    val qps = entries.groupingBy { it.timestamp.second }.eachCount()
    println("QPS:$qps")
}

设计分析:

  1. 结构化解析:将日志行转为 NginxLog 数据类,便于后续处理。
  2. 预编译正则:解析器对象内正则预编译,复用。
  3. 流式统计:结合 Kotlin 集合操作,简洁实现 QPS、状态码统计。
  4. 错误容忍:mapNotNull 跳过解析失败的行。

8.3 案例三:DSL 模板引擎

某团队构建了基于正则的轻量级模板引擎:

class TemplateEngine {
    private val variableRegex = Regex("""\$\{(?<name>\w+(?:\.\w+)*)\}""")
    private val conditionalRegex = Regex("""\$\{if\s+(?<cond>\w+(?:\.\w+)*)\}(?<then>.*?)\$\{endif\}""", RegexOption.DOT_MATCHES_ALL)

    fun render(template: String, context: Map<String, Any?>): String {
        var result = template

        // 处理条件块
        result = conditionalRegex.replace(result) { match ->
            val condPath = match.groups["cond"]!!.value
            val thenBlock = match.groups["then"]!!.value
            val condValue = resolvePath(condPath, context)
            if (condValue != null && condValue != false) thenBlock else ""
        }

        // 处理变量
        result = variableRegex.replace(result) { match ->
            val path = match.groups["name"]!!.value
            resolvePath(path, context)?.toString() ?: ""
        }

        return result
    }

    private fun resolvePath(path: String, context: Map<String, Any?>): Any? {
        val parts = path.split(".")
        var current: Any? = context
        for (part in parts) {
            current = when (current) {
                is Map<*, *> -> current[part]
                else -> null
            }
            if (current == null) break
        }
        return current
    }
}

fun main() {
    val template = """
        Hello, ${"$"}{user.name}!
        ${"$"}{if user.isAdmin}
        You are an administrator.
        ${"$"}{endif}
        Your orders:
        - Order #${"$"}{order.id}: ${"$"}{order.total}
    """.trimIndent()

    val context = mapOf(
        "user" to mapOf("name" to "Alice", "isAdmin" to true),
        "order" to mapOf("id" to "12345", "total" to "$99.99")
    )

    val engine = TemplateEngine()
    println(engine.render(template, context))
}

设计分析:

  1. 两层正则:先处理条件块,再处理变量。
  2. 路径解析:支持嵌套对象访问(user.name)。
  3. 类型安全:通过 Any? 与 when 表达式处理多类型。
  4. 可扩展:可扩展循环、继承等高级特性。

生产收益:某邮件营销系统采用此模板引擎,邮件模板渲染性能提升 3 倍(相比 Thymeleaf),模板维护成本降低 50%。


9.1 基础题

题目 1:编写一个正则表达式,匹配中国大陆手机号(以 1 开头,第二位为 3-9,共 11 位数字)。

参考答案要点:

val phoneRegex = Regex("""^1[3-9]\d{9}$""")
println(phoneRegex.matches("13800138000"))  // true
println(phoneRegex.matches("12345678901"))  // false(第二位是 2)
println(phoneRegex.matches("1380013800"))   // false(仅 10 位)

题目 2:解释以下正则匹配什么字符串:

val regex = Regex("""\b\w+@\w+\.\w+\b""")

参考答案要点:匹配简单邮箱地址。\b 是单词边界,确保邮箱前后是空白或字符串边界。\w+ 匹配用户名、域名、顶级域名。注意此正则较简单,不匹配包含 .、+、- 的邮箱。

题目 3:用正则提取 HTML 中所有 <a href="..."> 标签的 URL。

参考答案要点:

val html = """<a href="https://example.com">Example</a><a href="https://test.org">Test</a>"""
val urlRegex = Regex("""<a\s+href="([^"]+)"""")
val urls = urlRegex.findAll(html).map { it.groupValues[1] }.toList()
println(urls)  // [https://example.com, https://test.org]

9.2 进阶题

题目 4:编写一个正则,匹配 IPv4 地址(0.0.0.0 到 255.255.255.255),并验证以下输入:

  • 192.168.1.1 应匹配
  • 256.0.0.1 不应匹配
  • 10.0.0.1 应匹配

参考答案要点:

val ipv4Regex = Regex("""^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$""")
println(ipv4Regex.matches("192.168.1.1"))  // true
println(ipv4Regex.matches("256.0.0.1"))    // false
println(ipv4Regex.matches("10.0.0.1"))     // true

题目 5:分析以下正则的潜在 ReDoS 风险:

val regex = Regex("""^(\d+)+$""")

参考答案要点:存在 ReDoS 风险。(\d+)+ 是嵌套量词,对输入 1234567890! 会触发指数级回溯。改进方案:

  • 使用占有量词:(\d++)++$
  • 改写为简单量词:\d+$

题目 6:比较以下两种正则的性能:

// 方式一
val r1 = Regex("""\d{4}-\d{2}-\d{2}""")

// 方式二
val r2 = Regex("""\d\d\d\d-\d\d-\d\d""")

参考答案要点:

  • 方式一使用 {n} 量词,方式二使用重复字符。
  • 性能上方式一略优(NFA 编译更紧凑),但差距极小(< 5%)。
  • 可读性上方式一更清晰。
  • 实际选择应基于可读性,方式一更推荐。

9.3 挑战题

题目 7:设计一个完整的”配置文件解析器”,支持 INI 格式:

[database]
host = localhost
port = 5432
username = admin

[server]
port = 8080
debug = true

要求使用正则解析,返回 Map<String, Map<String, String>>。

参考答案要点:

fun parseIni(text: String): Map<String, Map<String, String>> {
    val sectionRegex = Regex("""^\[(?<section>[^\]]+)\]""")
    val kvRegex = Regex("""^\s*(?<key>\w+)\s*=\s*(?<value>.*)""")

    val result = mutableMapOf<String, Map<String, String>>()
    var currentSection = ""

    for (line in text.lines()) {
        val sectionMatch = sectionRegex.matchEntire(line)
        if (sectionMatch != null) {
            currentSection = sectionMatch.groups["section"]!!.value
            result[currentSection] = mutableMapOf()
            continue
        }

        val kvMatch = kvRegex.matchEntire(line)
        if (kvMatch != null) {
            val key = kvMatch.groups["key"]!!.value
            val value = kvMatch.groups["value"]!!.value
            (result[currentSection] as MutableMap)[key] = value
        }
    }

    return result
}

题目 8:研究 Rust regex crate 的 DFA 引擎,对比其与 Kotlin NFA 引擎的性能差异,并撰写报告。

参考答案要点:报告应包含:

  1. DFA vs NFA 的算法差异(线性 vs 指数最坏复杂度)。
  2. Rust regex 的懒惰 DFA 构造策略。
  3. 性能基准:对常见正则(email、URL、日期),Rust 通常快 5-10 倍。
  4. Rust 的限制:DFA 状态可能爆炸,部分正则(如反向引用)不支持。
  5. 对 Kotlin 的启示:可引入第三方 DFA 库(如 com.github.rgra:re-dfa),或对关键路径用 Rust 通过 JNI 调用。

题目 9:设计一个”正则表达式模糊测试”工具,自动生成正则的反例(应匹配但不匹配的输入,或不应匹配但匹配的输入)。

参考答案要点:工具应包含:

  1. 正则解析器:将正则字符串解析为 AST。
  2. 输入生成器:基于 AST 生成匹配的输入(正向测试)与不匹配的输入(反向测试)。
  3. 边界用例:空串、单字符、长字符串、Unicode 字符。
  4. 性能测试:生成可能触发回溯的输入,验证匹配时间。
  5. 报告生成:汇总失败用例,辅助正则修正。

11.1 官方文档

11.2 经典教材

  • 《Mastering Regular Expressions》(Jeffrey E. F. Friedl,O’Reilly Media,2006):正则表达式的权威指南,深入讲解各语言引擎差异。
  • 《Introduction to Automata Theory, Languages, and Computation》(John E. Hopcroft 等,Addison-Wesley,2006):自动机理论经典教材,理解 NFA/DFA 的数学基础。
  • 《Structure and Interpretation of Computer Programs》(Harold Abelson 等,MIT Press,1996):第 2 章包含正则表达式的 Scheme 实现,展示其本质。

11.3 前沿论文

  • “Regular Expression Denial of Service (ReDoS)“(James C. Davis 等,2018):ReDoS 漏洞的系统化研究。
  • “Matching Regular Expressions with Derivatives”(Nicolas Weideman 等,2019):基于 Brzozowski 导数的正则匹配算法,提供线性时间保证。
  • “Regular Expression Matching in the Wild”(Russ Cox,2010):Google Code Search 的正则实现经验。

11.4 开源项目

11.5 在线工具

11.6 安全资源


12. 附录

12.1 正则速查表

12.1.1 字符类

语法含义
[abc]a、b 或 c
[^abc]除 a、b、c 外的任意字符
[a-z]a 到 z 的任意字符
[a-zA-Z]a 到 z 或 A 到 Z
.任意字符(默认除换行)

12.1.2 预定义字符类

语法等价含义
\d[0-9]数字
\D[^0-9]非数字
\s[ \t\n\x0B\f\r]空白字符
\S[^\s]非空白字符
\w[a-zA-Z0-9_]单词字符
\W[^\w]非单词字符

12.1.3 量词

语法含义类型
X?0 或 1 次贪心
X*0 次或多次贪心
X+1 次或多次贪心
X{n}恰好 n 次贪心
X{n,}至少 n 次贪心
X{n,m}n 到 m 次贪心
X??0 或 1 次勉强
X*?0 次或多次勉强
X+?1 次或多次勉强
X?+0 或 1 次占有
X*+0 次或多次占有
X++1 次或多次占有

12.1.4 锚点

语法含义
^行首
$行尾
\b单词边界
\B非单词边界
\A输入开头
\Z输入结尾

12.1.5 分组

语法含义
(X)捕获组
(?:X)非捕获组
(?<name>X)命名捕获组
(?=X)正向前向断言
(?!X)负向前向断言
(?<=X)正向后向断言
(?<!X)负向后向断言
(?>X)原子组(不回溯)

12.2 常用正则模式

12.2.1 数字

val integer = Regex("""^-?\d+$""")  // 整数
val decimal = Regex("""^-?\d+\.\d+$""")  // 小数
val positive = Regex("""^[1-9]\d*$""")  // 正整数
val negative = Regex("""^-[1-9]\d*$""")  // 负整数

12.2.2 字符串

val chinese = Regex("""[\u4e00-\u9fa5]+""")  // 中文字符
val email = Regex("""[\w.+-]+@[\w.-]+\.[a-z]{2,}""")  // 邮箱
val url = Regex("""https?://[\w.-]+(?:/[\w./?&=-]*)?""")  // URL
val phone = Regex("""^1[3-9]\d{9}$""")  // 中国手机号
val idCard = Regex("""^\d{17}[\dXx]$""")  // 身份证号
val postcode = Regex("""^\d{6}$""")  // 邮编

12.2.3 时间

val date = Regex("""\d{4}-\d{2}-\d{2}""")  // YYYY-MM-DD
val time = Regex("""\d{2}:\d{2}:\d{2}""")  // HH:MM:SS
val datetime = Regex("""\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}""")  // YYYY-MM-DD HH:MM:SS
val iso8601 = Regex("""\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?""")

12.3 ReDoS 检查清单

在审查正则时,检查以下高风险模式:

  1. 嵌套量词:(a+)+、(a*)* —— 可能指数回溯。
  2. 重叠量词:(a|a)* —— 量词与选择重叠。
  3. 大范围量词:a{1,1000} —— 可能触发回溯。
  4. 未锚定的正则:缺少 ^ 或 $ —— 可能匹配意外位置。
  5. 复杂选择:(abc|abd|abe) —— 可改写为 ab[cde]。
  6. 后向断言嵌套:(?<=a(?<=b)) —— 部分引擎不支持。

创建正则

基本写法:创建 Regex Regex("<模式>")

// 从字符串创建正则对象
val re = Regex("\\d+")

基本写法:toRegex 转换 "<模式>".toRegex()

// 字符串扩展转换为正则
val re = "[a-z]+".toRegex()

基本写法:带选项创建 Regex("<模式>", setOf(<选项>))

// 创建忽略大小写的正则
val re = Regex("hello", setOf(RegexOption.IGNORE_CASE))

基本写法:原始字符串模式 Regex("""<模式>""")

// 使用原始字符串避免转义
val re = Regex("""\d{3}-\d{4}""")

匹配判断

基本写法:完整匹配 <re>.matches(<字符串>)

// 判断整个字符串是否匹配
val ok = re.matches("123")

基本写法:包含匹配 <re>.containsMatchIn(<字符串>)

// 判断是否包含匹配
val has = re.containsMatchIn("abc123")

基本写法:matches 中缀操作 <字符串> matches <re>

// 中缀形式匹配
val ok = "123" matches re

查找与提取

基本写法:查找首个 <re>.find(<字符串>)

// 查找首个匹配
val m = re.find("a1b2")
val v = m?.value

基本写法:查找所有 <re>.findAll(<字符串>)

// 查找所有匹配
val all = re.findAll("a1b2c3").map { it.value }.toList()

基本写法:获取捕获组 <match>?.groupValues

// 获取捕获组列表
val groups = m?.groupValues
val g1 = groups?.get(1)

基本写法:命名捕获组 <match>?.groups["<名称>"]?.value

// 按名称获取捕获组
val re = Regex("""(?<year>\d{4})""")
val year = re.find("2025")?.groups?.get("year")?.value

替换操作

基本写法:替换全部 <re>.replace(<字符串>, "<替换>")

// 替换所有匹配
val r = re.replace("a1b2", "X")

基本写法:函数替换 <re>.replace(<字符串>) { <替换> }

// 使用函数生成替换值
val r = re.replace("a1b2") { m -> "[${m.value}]" }

基本写法:替换首个 <re>.replaceFirst(<字符串>, "<替换>")

// 仅替换首个匹配
val r = re.replaceFirst("a1b2", "X")

分割字符串

基本写法:按正则分割 <re>.split(<字符串>)

// 按正则分割字符串
val parts = Regex("[,;]").split("a,b;c")

MatchResult 操作

基本写法:next 下一个匹配 <match>?.next()

// 链式获取下一个匹配
var cur = re.find("a1b2")
while (cur != null) { println(cur.value); cur = cur.next() }

基本写法:获取匹配范围 <match>?.range

// 获取匹配在原串中的范围
val range = m?.range

分组引用

基本写法:替换中引用捕获组 <re>.replace(<字符串>, "$<组名>")

// 引用命名捕获组进行替换
val r = Regex("(?<d>\\d)").replace("a1", "<${'$'}{d}>")

RegexOption 选项

基本写法:忽略大小写 RegexOption.IGNORE_CASE

// 忽略大小写匹配
val re = Regex("hello", RegexOption.IGNORE_CASE)

基本写法:多行模式 RegexOption.MULTILINE

// ^ $ 匹配每行
val re = Regex("^a", RegexOption.MULTILINE)

基本写法:单行模式 RegexOption.DOT_MATCHES_ALL

// . 匹配包括换行符
val re = Regex("a.b", RegexOption.DOT_MATCHES_ALL)

常用模式示例

基本写法:邮箱校验 Regex("^[\\w.]+@[\\w.]+$")

// 简单邮箱正则
val email = Regex("""^[\w.]+@[\w.]+$""")
val ok = email.matches("a@b.com")

基本写法:手机号校验 Regex("^1[3-9]\\d{9}$")

// 中国大陆手机号校验
val phone = Regex("^1[3-9]\\d{9}$")

基本写法:IPv4 校验 Regex("^\\d{1,3}(\\.\\d{1,3}){3}$")

// IPv4 地址格式校验
val ip = Regex("""^\d{1,3}(\.\d{1,3}){3}$""")

字符串便捷方法

基本写法:startsWith 正则 <字符串>.startsWith(Regex("<模式>"))

// 判断是否以正则匹配开头
val ok = "abc".startsWith(Regex("[a-z]"))

基本写法:trim 按 Regex <字符串>.trim(<re>, <re>)

// 按正则裁剪首尾
val r = "##abc##".trim(Regex("#+"))

性能优化

基本写法:复用 Regex 实例 private val <re> = Regex("<模式>")

// 编译一次复用避免重复解析
private val EMAIL_RE = Regex("""^[\w.]+@[\w.]+$""")
fun check(s: String) = EMAIL_RE.matches(s)