在这里插入图片描述
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

目录

  1. 概述
  2. 算法功能
  3. 核心实现
  4. 实战案例
  5. 编译过程详解
  6. 算法扩展
  7. 最佳实践
  8. 常见问题

概述

本文档介绍如何在 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 对象

关键转换点

  1. 正则表达式:转换为 JavaScript RegExp
  2. 集合操作:转换为数组操作
  3. Lambda 表达式:转换为 JavaScript 函数
  4. 字符串处理:保持功能一致

算法扩展

扩展 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().sizesizecount() 都是 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
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐