MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin正则表达式高效使用技巧

2021-02-196.7k 阅读

Kotlin 正则表达式基础

在 Kotlin 中,正则表达式是用于匹配和操作字符串的强大工具。正则表达式由字符和操作符组成的模式,用于定义搜索模式。Kotlin 对正则表达式的支持主要通过 Regex 类实现。

创建 Regex 对象

要在 Kotlin 中使用正则表达式,首先需要创建一个 Regex 对象。可以通过以下几种方式创建:

  1. 直接字符串创建
val regex = Regex("pattern")

这里的 "pattern" 是正则表达式的字符串表示。例如,要匹配数字,可以使用 Regex("\\d")。注意,在 Kotlin 字符串中,反斜杠 \ 需要转义,所以是 \\d。 2. 使用 toRegex() 扩展函数

val pattern = "pattern".toRegex()

这是一种更简洁的创建 Regex 对象的方式,例如 "\d+".toRegex() 可以创建匹配一个或多个数字的正则表达式。

简单匹配

一旦有了 Regex 对象,就可以使用它进行匹配操作。Regex 类提供了多个方法用于匹配字符串。

  1. matches 方法: 这个方法用于判断整个字符串是否与正则表达式匹配。
val regex = Regex("abc")
val str1 = "abc"
val str2 = "abcd"
println(regex.matches(str1)) // true
println(regex.matches(str2)) // false
  1. containsMatchIn 方法: 判断字符串中是否包含与正则表达式匹配的子串。
val regex = Regex("abc")
val str1 = "123abc456"
val str2 = "123def456"
println(regex.containsMatchIn(str1)) // true
println(regex.containsMatchIn(str2)) // false

正则表达式语法

字符类

  1. 预定义字符类
    • \d:匹配任意一个数字字符,等价于 [0-9]
    val regex = Regex("\\d")
    val str = "a1b"
    println(regex.containsMatchIn(str)) // true
    
    • \D:匹配任意一个非数字字符,等价于 [^0-9]
    val regex = Regex("\\D")
    val str = "a1b"
    println(regex.containsMatchIn(str)) // true
    
    • \w:匹配任意一个单词字符,包括字母、数字和下划线,等价于 [a-zA-Z0-9_]
    val regex = Regex("\\w")
    val str = "a_1"
    println(regex.containsMatchIn(str)) // true
    
    • \W:匹配任意一个非单词字符,等价于 [^a-zA-Z0-9_]
    val regex = Regex("\\W")
    val str = "a_1!"
    println(regex.containsMatchIn(str)) // true
    
    • \s:匹配任意一个空白字符,包括空格、制表符、换行符等,等价于 [ \t\n\r\f]
    val regex = Regex("\\s")
    val str = "a b"
    println(regex.containsMatchIn(str)) // true
    
    • \S:匹配任意一个非空白字符,等价于 [^ \t\n\r\f]
    val regex = Regex("\\S")
    val str = "a b"
    println(regex.containsMatchIn(str)) // true
    
  2. 自定义字符类: 可以使用方括号 [] 定义自己的字符类。例如,[aeiou] 匹配任意一个元音字母。
val regex = Regex("[aeiou]")
val str = "apple"
println(regex.containsMatchIn(str)) // true

也可以使用连字符 - 表示范围,如 [a - z] 匹配任意小写字母,[0 - 9] 匹配任意数字。

val regex1 = Regex("[a - z]")
val regex2 = Regex("[0 - 9]")
val str1 = "a"
val str2 = "1"
println(regex1.containsMatchIn(str1)) // true
println(regex2.containsMatchIn(str2)) // true

使用 ^ 作为字符类的第一个字符表示取反,如 [^0 - 9] 匹配任意非数字字符。

val regex = Regex("[^0 - 9]")
val str = "a"
println(regex.containsMatchIn(str)) // true

量词

  1. 贪婪量词
    • *:匹配前面的字符零次或多次。例如,a* 可以匹配空字符串、aaaaaa 等。
    val regex = Regex("a*")
    val str1 = ""
    val str2 = "a"
    val str3 = "aaa"
    println(regex.matches(str1)) // true
    println(regex.matches(str2)) // true
    println(regex.matches(str3)) // true
    
    • +:匹配前面的字符一次或多次。例如,a+ 可以匹配 aaaaaa 等,但不匹配空字符串。
    val regex = Regex("a+")
    val str1 = ""
    val str2 = "a"
    val str3 = "aaa"
    println(regex.matches(str1)) // false
    println(regex.matches(str2)) // true
    println(regex.matches(str3)) // true
    
    • ?:匹配前面的字符零次或一次。例如,a? 可以匹配空字符串或 a
    val regex = Regex("a?")
    val str1 = ""
    val str2 = "a"
    println(regex.matches(str1)) // true
    println(regex.matches(str2)) // true
    
    • {n}:匹配前面的字符恰好 n 次。例如,a{3} 只匹配 aaa
    val regex = Regex("a{3}")
    val str1 = "aa"
    val str2 = "aaa"
    println(regex.matches(str1)) // false
    println(regex.matches(str2)) // true
    
    • {n,}:匹配前面的字符至少 n 次。例如,a{3,} 匹配 aaaaaaaaaaaa 等。
    val regex = Regex("a{3,}")
    val str1 = "aa"
    val str2 = "aaa"
    println(regex.matches(str1)) // false
    println(regex.matches(str2)) // true
    
    • {n,m}:匹配前面的字符至少 n 次,最多 m 次。例如,a{2,4} 匹配 aaaaaaaaa
    val regex = Regex("a{2,4}")
    val str1 = "a"
    val str2 = "aa"
    val str3 = "aaa"
    val str4 = "aaaa"
    val str5 = "aaaaa"
    println(regex.matches(str1)) // false
    println(regex.matches(str2)) // true
    println(regex.matches(str3)) // true
    println(regex.matches(str4)) // true
    println(regex.matches(str5)) // false
    
  2. 懒惰量词: 在贪婪量词后面加上 ? 就变成了懒惰量词。懒惰量词会尽可能少地匹配字符。
    • *?:匹配前面的字符零次或多次,但尽可能少匹配。例如,对于字符串 aaaa*? 只会匹配空字符串,因为它尽可能少地匹配。
    val regex = Regex("a*?")
    val str = "aaa"
    val matchResult = regex.find(str)
    matchResult?.let { println(it.value) } // 输出空字符串
    
    • +?:匹配前面的字符一次或多次,但尽可能少匹配。例如,对于字符串 aaaa+? 只会匹配一个 a
    val regex = Regex("a+?")
    val str = "aaa"
    val matchResult = regex.find(str)
    matchResult?.let { println(it.value) } // 输出 "a"
    
    • ??:匹配前面的字符零次或一次,但尽可能少匹配。例如,对于字符串 aa?? 会匹配空字符串。
    val regex = Regex("a??")
    val str = "a"
    val matchResult = regex.find(str)
    matchResult?.let { println(it.value) } // 输出空字符串
    
    • {n}?:匹配前面的字符恰好 n 次,这里 ? 没有实际作用,因为已经明确了匹配次数。
    • {n,}?:匹配前面的字符至少 n 次,但尽可能少匹配。例如,对于字符串 aaaaa{2,}? 会匹配 aa
    val regex = Regex("a{2,}?")
    val str = "aaaa"
    val matchResult = regex.find(str)
    matchResult?.let { println(it.value) } // 输出 "aa"
    
    • {n,m}?:匹配前面的字符至少 n 次,最多 m 次,但尽可能少匹配。例如,对于字符串 aaaaa{2,3}? 会匹配 aa
    val regex = Regex("a{2,3}?")
    val str = "aaaa"
    val matchResult = regex.find(str)
    matchResult?.let { println(it.value) } // 输出 "aa"
    

边界匹配

  1. ^:匹配字符串的开始位置。例如,^abc 表示以 abc 开头的字符串。
val regex = Regex("^abc")
val str1 = "abcdef"
val str2 = "defabc"
println(regex.matches(str1)) // true
println(regex.matches(str2)) // false
  1. $:匹配字符串的结束位置。例如,abc$ 表示以 abc 结尾的字符串。
val regex = Regex("abc$")
val str1 = "defabc"
val str2 = "abcdef"
println(regex.matches(str1)) // true
println(regex.matches(str2)) // false
  1. \b:匹配单词边界。单词边界是指单词字符(\w)和非单词字符(\W)之间的位置,或者字符串开始/结束位置与单词字符之间的位置。例如,\bcat\b 只匹配单词 cat,而不匹配 category 中的 cat
val regex = Regex("\\bcat\\b")
val str1 = "cat"
val str2 = "category"
println(regex.containsMatchIn(str1)) // true
println(regex.containsMatchIn(str2)) // false
  1. \B:匹配非单词边界。例如,\Bcat\B 匹配 category 中的 cat,但不匹配单独的 cat 单词。
val regex = Regex("\\Bcat\\B")
val str1 = "cat"
val str2 = "category"
println(regex.containsMatchIn(str1)) // false
println(regex.containsMatchIn(str2)) // true

分组

  1. 捕获组: 使用圆括号 () 定义捕获组。捕获组可以记住匹配的子串,以便后续使用。例如,(abc) 定义了一个捕获组,它会捕获匹配 abc 的子串。
val regex = Regex("(abc)")
val str = "abcdef"
val matchResult = regex.find(str)
matchResult?.let {
    println(it.groupValues[0]) // 完整匹配的字符串 "abc"
    println(it.groupValues[1]) // 第一个捕获组的内容 "abc"
}

可以有多个捕获组,例如 (abc)(def),它们按照从左到右的顺序编号,编号从 1 开始。

val regex = Regex("(abc)(def)")
val str = "abcdef"
val matchResult = regex.find(str)
matchResult?.let {
    println(it.groupValues[0]) // 完整匹配的字符串 "abcdef"
    println(it.groupValues[1]) // 第一个捕获组的内容 "abc"
    println(it.groupValues[2]) // 第二个捕获组的内容 "def"
}
  1. 命名捕获组: 在 Kotlin 中,可以给捕获组命名,使用 (?<name>pattern) 的形式,其中 name 是组的名称,pattern 是正则表达式。
val regex = Regex("(?<first>abc)(?<second>def)")
val str = "abcdef"
val matchResult = regex.find(str)
matchResult?.let {
    println(it.groupValues[0]) // 完整匹配的字符串 "abcdef"
    println(it.groups["first"]?.value) // 第一个命名捕获组的内容 "abc"
    println(it.groups["second"]?.value) // 第二个命名捕获组的内容 "def"
}
  1. 非捕获组: 如果只想分组但不想捕获,可以使用 (?:pattern) 的形式。例如,(?:abc)(def)(?:abc) 是一个非捕获组,只有 (def) 会被捕获。
val regex = Regex("(?:abc)(def)")
val str = "abcdef"
val matchResult = regex.find(str)
matchResult?.let {
    println(it.groupValues[0]) // 完整匹配的字符串 "abcdef"
    println(it.groupValues[1]) // 第一个捕获组(这里是 (def))的内容 "def"
}

正则表达式的高级应用

替换操作

  1. replace 方法Regex 类的 replace 方法用于将字符串中匹配正则表达式的部分替换为指定的字符串。
val regex = Regex("abc")
val str = "123abc456"
val newStr = regex.replace(str, "xyz")
println(newStr) // 输出 "123xyz456"
  1. 使用函数进行替换replace 方法还可以接受一个函数作为参数,该函数根据匹配结果生成替换内容。
val regex = Regex("\\d+")
val str = "a12b34c"
val newStr = regex.replace(str) { matchResult ->
    (matchResult.value.toInt() * 2).toString()
}
println(newStr) // 输出 "a24b68c"
  1. replaceFirst 方法replaceFirst 方法只替换字符串中第一个匹配的子串。
val regex = Regex("abc")
val str = "abcabcabc"
val newStr = regex.replaceFirst(str, "xyz")
println(newStr) // 输出 "xyzabcabc"

分割操作

  1. split 方法Regex 类的 split 方法用于根据正则表达式将字符串分割成多个子串。
val regex = Regex(",")
val str = "a,b,c"
val parts = regex.split(str)
println(parts.contentToString()) // 输出 "[a, b, c]"
  1. 限制分割次数split 方法可以接受一个参数指定最大分割次数。
val regex = Regex(",")
val str = "a,b,c,d"
val parts = regex.split(str, 2)
println(parts.contentToString()) // 输出 "[a, b, c,d]"

查找所有匹配

  1. findAll 方法findAll 方法返回一个 MatchResult 的序列,包含字符串中所有匹配的结果。
val regex = Regex("\\d+")
val str = "a12b34c56"
val matches = regex.findAll(str)
matches.forEach {
    println(it.value) // 输出 "12", "34", "56"
}
  1. 获取捕获组信息: 对于包含捕获组的正则表达式,findAll 返回的 MatchResult 可以获取每个捕获组的信息。
val regex = Regex("(\\d+)([a - z])")
val str = "12a34b"
val matches = regex.findAll(str)
matches.forEach {
    println(it.groupValues[0]) // 完整匹配的字符串,如 "12a"
    println(it.groupValues[1]) // 第一个捕获组的内容,如 "12"
    println(it.groupValues[2]) // 第二个捕获组的内容,如 "a"
}

性能优化

  1. 缓存 Regex 对象: 如果在程序中多次使用同一个正则表达式,应该缓存 Regex 对象,而不是每次都创建新的。
private val regex = Regex("\\d+")

fun processString(str: String) {
    val matches = regex.findAll(str)
    // 处理匹配结果
}
  1. 避免不必要的捕获组: 捕获组会增加匹配的开销,因为需要记住匹配的子串。如果不需要捕获某些分组的内容,使用非捕获组 (?:pattern)
  2. 使用合适的量词: 尽量使用精确的量词,避免使用过于贪婪或懒惰的量词,因为它们可能会导致不必要的回溯。例如,如果确定匹配次数,使用 {n}*+ 更高效。

常见应用场景

验证邮箱地址

val emailRegex = Regex("^[A - Za - z0 - 9._%+-]+@[A - Za - z0 - 9.-]+\\.[A - Za - z]{2,}$")

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

验证手机号码

val phoneRegex = Regex("^1[3 - 9]\\d{9}$")

fun isValidPhone(phone: String): Boolean {
    return phoneRegex.matches(phone)
}

提取 URL

val urlRegex = Regex("(https?|ftp)://([-A - Za - z0 - 9.]+)(/[-A - Za - z0 - 9_\\./]*)?")

fun extractUrls(str: String): List<String> {
    return urlRegex.findAll(str).map { it.value }.toList()
}

通过以上对 Kotlin 正则表达式的详细介绍,包括基础使用、语法、高级应用、性能优化和常见场景,相信开发者能够在实际项目中高效地使用正则表达式来处理字符串相关的任务。在实际应用中,需要根据具体需求灵活运用正则表达式的各种特性,以达到最佳的效果。同时,要注意正则表达式的可读性和可维护性,对于复杂的正则表达式,可以添加注释进行说明。