1. 概述

在数据处理和文本解析中,从字符串中提取数值是一项基础但关键的操作。无论是解析 ID、提取电话号码、识别邮编,还是清洗日志中的金额信息,都离不开这项能力。

本文将介绍几种在 Kotlin 中从字符串提取数字的常用方法,并结合实际代码示例帮助你掌握不同方案的优劣和适用场景,避免踩坑。


2. 前提假设

为了聚焦核心逻辑,我们对“提取数字”这一任务做如下限定:

  • ✅ 只提取 十进制正整数
  • ✅ 支持超大数值(使用 BigInteger 类型)
  • ❌ 不处理小数(如 3.14
  • ❌ 不处理负数(如 -123
  • ❌ 不处理十六进制、二进制等非十进制数

这样可以简化实现,同时保证对大数的安全支持。


3. 使用循环遍历

最直观的方式是手动遍历每个字符,逐个判断是否为数字,并拼接连续的数字串。

fun extractNumbersUsingLoop(str: String): List<BigInteger> {
    val numbers = mutableListOf<BigInteger>()
    val currentNumber = StringBuilder()
    for (char in str) {
        if (char.isDigit()) {
            currentNumber.append(char)
        } else if (currentNumber.isNotEmpty()) {
            numbers.add(currentNumber.toString().toBigInteger())
            currentNumber.clear()
        }
    }
    if (currentNumber.isNotEmpty()) {
        numbers.add(currentNumber.toString().toBigInteger())
    }
    return numbers
}

关键点说明:

  • ✅ 使用 StringBuilder 高效拼接字符
  • ✅ 遇到非数字字符时,触发当前数字串的提交
  • ✅ 循环结束后别忘了处理末尾残留的数字(容易踩坑!)
  • ⚠️ 手动状态管理,代码稍显冗长,但逻辑清晰可控

适合对性能要求高、且需要自定义行为(比如跳过某些特殊分隔符)的场景。


4. 使用正则表达式

正则表达式是处理模式匹配的利器。我们可以用 \d+ 匹配一个或多个连续数字。

fun extractMultipleUsingRegex(str: String): List<BigInteger> {
    return Regex("\\d+").findAll(str).map { it.value.toBigInteger() }.toList()
}

特点分析:

  • ✅ 一行搞定,简洁优雅
  • findAll() 返回所有匹配结果的序列,延迟计算效率高
  • it.value 是匹配到的原始字符串,转成 BigInteger 安全无溢出
  • ❌ 对正则不熟的同学可能觉得 \d+ 神秘,但这是标准写法,建议记牢

🔍 小贴士:\d 表示 digit,+ 表示一个或多个,合起来就是“连续数字”。


5. 使用 split + 正则

另一种思路是:把非数字当作分隔符,直接切开字符串

fun extractNumbersUsingSplitAndRegex(str: String): List<BigInteger> {
    return str.split(Regex("\\D+"))
        .filter { it.isNotBlank() }
        .map { it.toBigInteger() }
}

解析:

  • \\D+:匹配一个或多个非数字字符(注意大小写:\D 是 non-digit)
  • split() 切割后会得到空字符串或纯数字字符串
  • filter { it.isNotBlank() } 去除空项,防止 ""BigInteger 报错
  • ✅ 链式调用,函数式风格清爽

⚠️ 注意:如果原字符串以数字开头或结尾,split 仍能正确处理,但中间若全是非数字,会产生多个空串,必须过滤。


6. 测试验证

再好的逻辑也得靠测试兜底。我们使用 JUnit 5 的参数化测试来覆盖多种场景。

添加依赖

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
</dependency>

参数化测试示例

@ParameterizedTest
@CsvSource(
    "string with 123 and 333 in the text, 123:333",
    "another string with 456 and 789, 456:789",
    "string 123-234, 123:234",
    "no numbers,",
    "3 4 50 numbers6, 3",
    "91234567891011121314151617181920number, 91234567891011121314151617181920",
    "123456789large numbers0, 123456789"
)
fun `extract all occurrences of numbers from string using regex`(str: String, expected: String?) {
    val numbers = extractMultipleUsingRegex(str)
    val expectedList = expected?.split(":")?.map { it.toBigInteger() } ?: emptyList()
    Assertions.assertEquals(expectedList, numbers)
}

测试设计要点:

  • ✅ 使用 @CsvSource 定义多组输入输出
  • ✅ 自定义分隔符 : 来区分预期多个数字
  • expected 为 null 时表示无数字,返回空列表
  • ✅ 覆盖了大数、混合字符、边界情况等典型场景

边界测试补充

对于空字符串这种特殊情况,建议单独写一个简单测试,避免参数化格式混乱:

@Test
fun `check empty string scenario for split`() {
    val numbers = extractNumbersUsingSplitAndRegex("")
    Assertions.assertIterableEquals(numbers, listOf<BigInteger>())
}

✅ 清晰明了,不易出错。


7. 总结

方法 优点 缺点 推荐场景
✅ 循环遍历 控制精细,性能好 代码多,易漏边界 高频调用、需定制逻辑
✅ 正则 findAll 简洁高效,语义清晰 需懂正则 大多数通用场景
✅ split + 正则 函数式风格,易读 多一次过滤步骤 字符串切割思维自然的场景

💡 推荐优先使用 Regex("\\d+").findAll() 方案——简洁、安全、可读性强,是 Kotlin 社区中的主流做法。

文中所有示例代码已托管至 GitHub:
👉 https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-strings-5

建议集合,下次遇到类似需求直接抄作业,少走弯路。


原始标题:Extract Numbers From a String in Kotlin