正则表达式匹配和替换 Kotlin KMP & OpenHarmony

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
目录
- 概述
- 算法功能
- 核心实现
- 实战案例
- 编译过程详解
- 算法扩展
- 最佳实践
- 常见问题
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个完整的正则表达式匹配和替换工具系统。这个案例展示了如何使用 Kotlin 的正则表达式功能来创建一个功能丰富的文本处理工具。通过 KMP,这个工具可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行,并支持用户输入进行实时处理。
工具的特点
- 多模式匹配:支持邮箱、电话、URL、数字、单词等多种模式
- 灵活替换:支持将匹配项替换为指定内容
- 详细统计:提供匹配项的详细统计信息
- 频率分析:分析单词和数字的出现频率
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
算法功能
1. 模式匹配
- 邮箱匹配:匹配标准的电子邮件地址
- 电话匹配:匹配格式化的电话号码
- URL匹配:匹配 HTTP/HTTPS 链接
- 数字匹配:匹配整数和浮点数
- 单词匹配:匹配英文单词
2. 匹配统计
- 匹配数量:统计每种模式的匹配数量
- 匹配列表:列出所有匹配项
- 唯一匹配:统计唯一的匹配项
3. 替换操作
- 邮箱替换:将邮箱替换为占位符
- 电话替换:将电话替换为占位符
- 数字替换:将数字替换为占位符
4. 字符统计
- 字母数:统计字母个数
- 数字数:统计数字个数
- 空格数:统计空格个数
- 特殊字符:统计特殊字符个数
- 大小写:统计大写和小写字母
5. 频率分析
- 单词频率:统计每个单词出现的次数
- 数字频率:统计每个数字出现的次数
- 排序:按频率从高到低排序
核心实现
1. 定义正则表达式模式
val emailPattern = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
val phonePattern = Regex("\\d{3}-\\d{3}-\\d{4}")
val urlPattern = Regex("https?://[^\\s]+")
val numberPattern = Regex("\\d+")
val wordPattern = Regex("\\b[a-zA-Z]+\\b")
这段代码定义了五个常用的正则表达式模式。emailPattern 用于匹配标准的电子邮件地址,[a-zA-Z0-9._%+-]+ 匹配用户名部分,@ 匹配 @ 符号,[a-zA-Z0-9.-]+ 匹配域名,\\.[a-zA-Z]{2,} 匹配顶级域名(至少 2 个字母)。phonePattern 用于匹配格式为 XXX-XXX-XXXX 的电话号码,其中 \\d 表示任意数字。urlPattern 用于匹配 HTTP 或 HTTPS 链接,https?:// 表示可选的 s,[^\\s]+ 表示一个或多个非空白字符。numberPattern 用于匹配一个或多个数字。wordPattern 用于匹配英文单词,\\b 表示单词边界。这些预编译的正则表达式可以被重复使用,提高性能。
2. 提取匹配项
val emails = emailPattern.findAll(inputText).map { it.value }.toList()
val phones = phonePattern.findAll(inputText).map { it.value }.toList()
val urls = urlPattern.findAll(inputText).map { it.value }.toList()
val numbers = numberPattern.findAll(inputText).map { it.value }.toList()
val words = wordPattern.findAll(inputText).map { it.value }.toList()
这段代码使用 findAll() 函数从输入文本中提取所有匹配项。findAll() 返回一个 MatchResult 序列,每个 MatchResult 对象包含匹配的详细信息。使用 map { it.value } 提取每个匹配项的值(即匹配的字符串),然后使用 toList() 将序列转换为列表。这种方法比使用 find() 循环更简洁高效。例如,emailPattern.findAll(inputText) 会找到输入文本中所有的邮箱地址,然后提取它们的值。这个过程对所有五种模式重复进行,分别提取邮箱、电话、URL、数字和单词。
3. 替换匹配项
val emailReplaced = inputText.replace(emailPattern, "[EMAIL]")
val phoneReplaced = inputText.replace(phonePattern, "[PHONE]")
val numberReplaced = inputText.replace(numberPattern, "[NUM]")
这段代码使用 replace() 函数将匹配项替换为指定的字符串。inputText.replace(emailPattern, "[EMAIL]") 会找到输入文本中所有匹配邮箱模式的字符串,并将它们替换为 “[EMAIL]”。类似地,电话号码被替换为 “[PHONE]”,数字被替换为 “[NUM]”。这种替换方法在数据脱敏、日志处理或文本规范化中很有用。例如,可以将敏感信息(如邮箱、电话)替换为占位符,保护用户隐私。replace() 函数会替换所有匹配项,如果没有匹配项则返回原始文本。
4. 频率统计
val uniqueWords = words.toSet()
val wordStats = uniqueWords.map { word ->
val count = words.count { it == word }
"$word: $count 次"
}.sorted()
这段代码统计了每个单词出现的频率。首先使用 words.toSet() 将单词列表转换为集合,去除重复的单词,得到所有唯一的单词。然后使用 map() 对每个唯一单词进行处理,计算该单词在原始列表中出现的次数(使用 words.count { it == word }),并将结果格式化为 “单词: 次数 次” 的字符串。最后使用 sorted() 按字母顺序排序统计结果。这个过程可以用来分析文本中最常见的单词,对于文本分析、关键词提取等应用很有用。
5. 字符统计
val letterCount = inputText.count { it.isLetter() }
val digitCount = inputText.count { it.isDigit() }
val spaceCount = inputText.count { it.isWhitespace() }
val specialCount = inputText.count { !it.isLetterOrDigit() && !it.isWhitespace() }
这段代码统计了输入文本中不同类型字符的个数。inputText.count { it.isLetter() } 统计所有字母的个数,包括大写和小写。inputText.count { it.isDigit() } 统计所有数字的个数。inputText.count { it.isWhitespace() } 统计所有空白字符的个数,包括空格、制表符、换行符等。inputText.count { !it.isLetterOrDigit() && !it.isWhitespace() } 统计特殊字符的个数,即既不是字母或数字,也不是空白字符的字符。这种字符统计在文本分析、密码强度检查、数据验证等场景中很有用。
实战案例
案例:完整的正则表达式匹配和替换工具
Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun regexMatcherReplacer(inputText: String = "Email: john@example.com, Phone: 123-456-7890"): String {
if (inputText.isEmpty()) {
return "❌ 错误: 输入不能为空\n请输入要处理的文本"
}
// 1. 常用正则表达式模式
val emailPattern = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
val phonePattern = Regex("\\d{3}-\\d{3}-\\d{4}")
val urlPattern = Regex("https?://[^\\s]+")
val numberPattern = Regex("\\d+")
val wordPattern = Regex("\\b[a-zA-Z]+\\b")
// 2. 提取匹配项
val emails = emailPattern.findAll(inputText).map { it.value }.toList()
val phones = phonePattern.findAll(inputText).map { it.value }.toList()
val urls = urlPattern.findAll(inputText).map { it.value }.toList()
val numbers = numberPattern.findAll(inputText).map { it.value }.toList()
val words = wordPattern.findAll(inputText).map { it.value }.toList()
// 3. 统计匹配数量
val emailCount = emails.size
val phoneCount = phones.size
val urlCount = urls.size
val numberCount = numbers.size
val wordCount = words.size
// 4. 替换操作
val emailReplaced = inputText.replace(emailPattern, "[EMAIL]")
val phoneReplaced = inputText.replace(phonePattern, "[PHONE]")
val numberReplaced = inputText.replace(numberPattern, "[NUM]")
// 5. 提取所有单词
val uniqueWords = words.toSet()
val wordStats = uniqueWords.map { word ->
val count = words.count { it == word }
"$word: $count 次"
}.sorted()
// 6. 提取所有数字
val uniqueNumbers = numbers.toSet()
val numberStats = uniqueNumbers.map { num ->
val count = numbers.count { it == num }
"$num: $count 次"
}.sorted()
// 7. 字符类型统计
val letterCount = inputText.count { it.isLetter() }
val digitCount = inputText.count { it.isDigit() }
val spaceCount = inputText.count { it.isWhitespace() }
val specialCount = inputText.count { !it.isLetterOrDigit() && !it.isWhitespace() }
// 8. 大小写统计
val uppercaseCount = inputText.count { it.isUpperCase() }
val lowercaseCount = inputText.count { it.isLowerCase() }
return "🔍 正则表达式匹配和替换\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"原始文本: $inputText\n\n" +
"1️⃣ 模式匹配统计:\n" +
" 邮箱: $emailCount 个\n" +
" 电话: $phoneCount 个\n" +
" URL: $urlCount 个\n" +
" 数字: $numberCount 个\n" +
" 单词: $wordCount 个\n\n" +
"2️⃣ 匹配项详情:\n" +
(if (emails.isEmpty()) " 邮箱: 无" else " 邮箱: ${emails.joinToString(", ")}") + "\n" +
(if (phones.isEmpty()) " 电话: 无" else " 电话: ${phones.joinToString(", ")}") + "\n" +
(if (urls.isEmpty()) " URL: 无" else " URL: ${urls.joinToString(", ")}") + "\n\n" +
"3️⃣ 字符统计:\n" +
" 字母: $letterCount 个\n" +
" 数字: $digitCount 个\n" +
" 空格: $spaceCount 个\n" +
" 特殊字符: $specialCount 个\n" +
" 大写: $uppercaseCount 个\n" +
" 小写: $lowercaseCount 个\n\n" +
"4️⃣ 单词频率 (Top 10):\n" +
wordStats.take(10).mapIndexed { index, stat -> " ${index + 1}. $stat" }.joinToString("\n") + "\n\n" +
"5️⃣ 数字频率:\n" +
numberStats.take(5).mapIndexed { index, stat -> " ${index + 1}. $stat" }.joinToString("\n") + "\n\n" +
"6️⃣ 替换示例:\n" +
" 邮箱替换: $emailReplaced\n" +
" 电话替换: $phoneReplaced\n" +
" 数字替换: $numberReplaced\n\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"✅ 处理完成!"
}
这是正则表达式匹配和替换工具的核心实现函数。函数使用 @OptIn(ExperimentalJsExport::class) 和 @JsExport 注解,使其可以被编译成 JavaScript 并在 ArkTS 中调用。函数首先进行输入验证,如果输入为空则返回错误信息。然后定义五个常用的正则表达式模式。接着使用 findAll() 提取各种类型的匹配项(邮箱、电话、URL、数字、单词)。然后统计每种类型的匹配数量。进行替换操作,将敏感信息替换为占位符。统计单词和数字的频率。统计各种类型字符的个数,包括字母、数字、空格、特殊字符、大写和小写。最后生成格式化的输出结果,包含所有的分析结果。这个函数展示了如何综合使用正则表达式进行复杂的文本处理。
ArkTS 调用代码(带输入框)
import { regexMatcherReplacer } from './hellokjs';
@Entry
@Component
struct Index {
@State message: string = '加载中...';
@State results: string[] = [];
@State caseTitle: string = '正则表达式匹配和替换';
@State inputText: string = 'Email: john@example.com, Phone: 123-456-7890';
aboutToAppear(): void {
this.loadResults();
}
loadResults(): void {
try {
const results: string[] = [];
const algorithmResult = regexMatcherReplacer(this.inputText);
results.push(algorithmResult);
this.results = results;
this.message = '✓ 匹配完成';
} catch (error) {
this.message = `✗ 错误: ${error}`;
}
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('KMP 鸿蒙跨端')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Spacer()
Text('Kotlin 案例')
.fontSize(14)
.fontColor(Color.White)
}
.width('100%')
.height(50)
.backgroundColor('#3b82f6')
.padding({ left: 20, right: 20 })
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
// 案例标题
Column() {
Text(this.caseTitle)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1f2937')
Text(this.message)
.fontSize(13)
.fontColor('#6b7280')
.margin({ top: 5 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 20, bottom: 15 })
.alignItems(HorizontalAlign.Start)
// 输入框区域
Column() {
Text('输入文本:')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#1f2937')
.margin({ bottom: 8 })
TextInput({ placeholder: '输入要匹配的文本...', text: this.inputText })
.width('100%')
.height(60)
.padding(12)
.border({ width: 1, color: '#d1d5db' })
.borderRadius(6)
.onChange((value: string) => {
this.inputText = value
})
Button('匹配')
.width('100%')
.height(40)
.margin({ top: 12 })
.backgroundColor('#3b82f6')
.fontColor(Color.White)
.onClick(() => {
this.loadResults()
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
// 结果显示区域
Scroll() {
Column() {
ForEach(this.results, (result: string) => {
Column() {
Text(result)
.fontSize(13)
.fontFamily('monospace')
.fontColor('#374151')
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.border({ width: 1, color: '#e5e7eb' })
.borderRadius(8)
.margin({ bottom: 12 })
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.width('100%')
// 底部按钮区域
Row() {
Button('示例文本')
.width('48%')
.height(44)
.backgroundColor('#10b981')
.fontColor(Color.White)
.fontSize(14)
.onClick(() => {
this.inputText = 'Email: john@example.com, Phone: 123-456-7890'
this.loadResults()
})
Button('清空')
.width('48%')
.height(44)
.backgroundColor('#6b7280')
.fontColor(Color.White)
.fontSize(14)
.onClick(() => {
this.inputText = ''
this.results = []
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#f9fafb')
}
}
这段 ArkTS 代码是鸿蒙应用的用户界面实现,通过 import 语句导入之前编译的 Kotlin 函数。使用 @State 装饰器定义四个响应式状态变量:message 用于显示状态信息,results 存储匹配结果,caseTitle 显示标题,inputText 存储用户输入的文本。aboutToAppear() 生命周期函数在组件加载时自动调用 loadResults() 进行初始匹配。loadResults() 方法调用 Kotlin 编译的 JavaScript 函数 regexMatcherReplacer(),并将结果存储在 results 数组中,使用 try-catch 捕获异常。build() 方法构建整个 UI 布局,分为五个部分:顶部蓝色标题栏、案例标题和状态信息、用户输入区域(包含文本输入框和匹配按钮)、可滚动的结果显示区域以及底部的示例文本和清空按钮。整个界面采用响应式设计,使用 Flexbox 布局和现代化的配色方案。
编译过程详解
Kotlin 到 JavaScript 的转换
| Kotlin 特性 | JavaScript 等价物 |
|---|---|
| Regex | RegExp |
| findAll() | match() 或 matchAll() |
| map() | 数组 map 方法 |
| count() | 条件计数循环 |
| toSet() | Set 对象 |
关键转换点
- 正则表达式:转换为 JavaScript RegExp
- 集合操作:转换为数组操作
- Lambda 表达式:转换为 JavaScript 函数
- 字符串处理:保持功能一致
算法扩展
扩展 1:添加自定义模式
fun matchCustomPattern(text: String, pattern: String): List<String> {
return try {
Regex(pattern).findAll(text).map { it.value }.toList()
} catch (e: Exception) {
emptyList()
}
}
这个函数允许用户提供自定义的正则表达式模式进行匹配。函数接收两个参数:要匹配的文本和正则表达式模式字符串。使用 try-catch 块来处理可能的异常,因为无效的正则表达式会抛出异常。如果正则表达式有效,使用 Regex(pattern).findAll(text) 找到所有匹配项,然后提取它们的值并转换为列表。如果发生异常(例如正则表达式语法错误),返回空列表。这个函数提供了灵活性,允许用户定义自己的匹配模式,而不仅限于预定义的模式。
扩展 2:添加分组提取
fun extractGroups(text: String, pattern: String): List<List<String>> {
return Regex(pattern).findAll(text).map { match ->
match.groupValues
}.toList()
}
这个函数用于从正则表达式的分组中提取内容。正则表达式中使用括号定义分组,例如 (\\w+)@(\\w+\\.\\w+) 定义了两个分组。函数使用 findAll() 找到所有匹配项,然后对每个匹配项使用 match.groupValues 提取所有分组的值。groupValues 是一个列表,其中第一个元素是整个匹配项,后续元素是各个分组的内容。最后将所有分组值转换为列表。这个函数在需要提取正则表达式中特定部分的场景中很有用,例如从邮箱地址中提取用户名和域名。
扩展 3:添加条件替换
fun conditionalReplace(text: String, pattern: String, condition: (String) -> Boolean, replacement: String): String {
return Regex(pattern).replace(text) { match ->
if (condition(match.value)) replacement else match.value
}
}
这个函数实现了条件替换功能,只有满足特定条件的匹配项才会被替换。函数接收四个参数:要处理的文本、正则表达式模式、条件函数和替换字符串。使用 Regex(pattern).replace(text) 的重载版本,该版本接收一个 lambda 函数作为替换逻辑。对于每个匹配项,lambda 函数检查条件函数是否返回 true,如果是则返回替换字符串,否则返回原始匹配项。这个函数提供了更灵活的替换控制,例如只替换满足特定条件的邮箱地址或电话号码。
扩展 4:添加多模式替换
fun multiPatternReplace(text: String, replacements: Map<String, String>): String {
var result = text
for ((pattern, replacement) in replacements) {
result = result.replace(Regex(pattern), replacement)
}
return result
}
这个函数实现了多模式替换功能,可以一次性应用多个正则表达式替换。函数接收两个参数:要处理的文本和一个 Map,其中键是正则表达式模式,值是替换字符串。函数遍历 Map 中的每个模式-替换对,依次对文本进行替换。每次替换的结果作为下一次替换的输入,这样可以实现链式替换。这个函数在需要应用多个替换规则的场景中很有用,例如同时替换邮箱、电话和数字。注意替换的顺序很重要,因为前面的替换可能会影响后面的替换。
最佳实践
1. 使用 findAll() 提取所有匹配项
// ✅ 好:使用 findAll()
val matches = pattern.findAll(text).map { it.value }.toList()
// ❌ 不好:使用 find() 循环
val matches = mutableListOf<String>()
var remaining = text
while (true) {
val match = pattern.find(remaining)
if (match != null) {
matches.add(match.value)
remaining = remaining.substring(match.range.last + 1)
} else break
}
这个最佳实践强调了使用合适函数的重要性。findAll() 直接返回所有匹配项的序列,代码简洁高效。而使用 find() 循环需要手动管理状态,代码复杂且容易出错。findAll() 方法内部已经优化了匹配过程,性能更好。此外,findAll() 返回的是惰性序列,只在需要时才进行计算,而 find() 循环每次都需要重新搜索。
2. 使用 toSet() 去重
// ✅ 好:使用 toSet()
val uniqueMatches = matches.toSet()
// ❌ 不好:手动去重
val uniqueMatches = mutableSetOf<String>()
for (match in matches) {
if (!uniqueMatches.contains(match)) {
uniqueMatches.add(match)
}
}
这个最佳实践强调了使用内置函数的优势。toSet() 一行代码就能完成去重,代码简洁清晰。而手动去重需要创建集合、循环遍历、检查是否存在等多个步骤,代码冗长且容易出错。toSet() 方法内部已经优化了去重过程,性能更好。此外,toSet() 的意图更清晰,任何阅读代码的人都能立即理解其目的。
3. 使用 count() 统计
// ✅ 好:使用 count()
val emailCount = emails.size
// ❌ 不好:使用 filter().size
val emailCount = emails.filter { it.isNotEmpty() }.size
这个最佳实践强调了选择合适方法的重要性。对于简单的计数,直接使用 size 属性是最高效的。而 filter().size 需要创建一个新的过滤列表,然后获取其大小,这会浪费内存和时间。如果需要条件计数,应该使用 count() 函数而不是 filter().size。size 和 count() 都是 O(1) 或 O(n) 的操作,但 filter().size 需要额外的内存分配。
4. 预编译正则表达式
// ✅ 好:预编译
val emailPattern = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
val emails = emailPattern.findAll(text)
// ❌ 不好:每次都编译
val emails = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}").findAll(text)
这个最佳实践强调了性能优化的重要性。预编译正则表达式意味着在使用前将其编译一次,然后可以多次重复使用。这样可以避免每次使用时都重新编译正则表达式,节省大量时间。如果在循环中使用正则表达式,预编译的效果尤其明显。每次创建新的 Regex 对象都需要解析和编译正则表达式字符串,这是一个相对昂贵的操作。预编译的正则表达式可以被重复使用,提高性能。
常见问题
Q1: 如何匹配中文字符?
A: 使用 Unicode 范围:
val chinesePattern = Regex("[\\u4e00-\\u9fa5]+")
val chineseWords = chinesePattern.findAll(text).map { it.value }.toList()
Q2: 如何实现不区分大小写的匹配?
A: 使用 RegexOption.IGNORE_CASE:
val pattern = Regex("hello", RegexOption.IGNORE_CASE)
val matches = pattern.findAll(text)
Q3: 如何提取分组内容?
A: 使用 groupValues:
val pattern = Regex("(\\w+)@(\\w+\\.\\w+)")
pattern.findAll(text).forEach { match ->
val username = match.groupValues[1]
val domain = match.groupValues[2]
}
Q4: 如何处理特殊字符?
A: 使用 Regex.escape():
val specialChars = "$100"
val pattern = Regex(Regex.escape(specialChars))
Q5: 如何实现多行匹配?
A: 使用 RegexOption.MULTILINE:
val pattern = Regex("^\\w+$", RegexOption.MULTILINE)
val lines = pattern.findAll(text)
总结
关键要点
- ✅ 使用 findAll() 提取所有匹配项
- ✅ 使用 toSet() 去重
- ✅ 使用 count() 统计
- ✅ 预编译正则表达式
- ✅ KMP 能无缝编译到 JavaScript
更多推荐




所有评论(0)