Highlight
Swift 5.7 为字符串处理引入了
Regex类型、正则字面量语法和RegexBuilderDSL,开发者可以用类型安全的方式匹配、提取和转换字符串内容,还能将 Foundation 的日期/数字解析器直接嵌入正则表达式中。
核心内容
处理字符串是编程中最常见的任务之一。从日志解析到数据校验,开发者每天都在写各种字符串匹配代码。
在 Swift 5.7 之前,处理正则表达式主要依赖 NSRegularExpression。这个 API 来自 Objective-C 时代,用起来繁琐:匹配结果是 NSTextCheckingResult,捕获组要通过数字索引访问,类型全是 String 或 Range,很容易出错。
Swift 5.7 带来了全新的正则解决方案。核心是三样东西:
Regex类型:标准库新增的正则类型,强类型、可组合- 正则字面量:编译器检查语法,Xcode 支持语法高亮
RegexBuilderDSL:用 Swift 代码描述正则模式,可读性接近自然语言
这三者的组合让字符串处理从”黑魔法”变成了普通的 Swift 代码。
从字符串到类型安全的匹配
假设你要从一段文本中提取用户 ID:
name: John Appleseed, user_id: 100
以前用 NSRegularExpression 需要写一堆样板代码。现在用 Swift Regex,有三种写法可选。
从字符串创建(运行时解析):
let regex = try Regex(#"user_id:\s*(\d+)"#)
正则字面量(编译期检查):
let regex = /user_id:\s*(\d+)/
RegexBuilder DSL(最佳可读性):
import RegexBuilder
let regex = Regex {
"user_id:"
OneOrMore(.whitespace)
Capture(.localizedInteger)
}
三种写法创建的 regex 都是 Regex 类型,可以传给统一的匹配算法。
匹配算法一览
Swift 标准库为 String 和 Substring 扩展了一批正则驱动的算法(03:49):
let input = "name: John Appleseed, user_id: 100"
let regex = /user_id:\s*(\d+)/
input.firstMatch(of: regex) // 找第一个匹配
input.wholeMatch(of: regex) // 整个字符串必须匹配
input.prefixMatch(of: regex) // 前缀匹配
input.starts(with: regex) // 是否以模式开头
input.replacing(regex, with: "456") // 替换匹配内容
input.trimmingPrefix(regex) // 去掉前缀匹配部分
input.split(separator: /\s*,\s*/) // 按正则分割
正则还能用在 switch 语句中:
switch "abc" {
case /\w+/:
print("It's a word!")
}
解析银行对账单
Foundation 的解析器可以直接嵌入 RegexBuilder(05:14):
let statement = """
DSLIP 04/06/20 Paypal $3,020.85
CREDIT 04/03/20 Payroll $69.73
"""
let regex = Regex {
Capture(.date(format: "\(month: .twoDigits)/\(day: .twoDigits)/\(year: .twoDigits)"))
OneOrMore(.whitespace)
OneOrMore(.word)
OneOrMore(.whitespace)
Capture(.currency(code: "USD").sign(strategy: .accounting))
}
这里 .date() 和 .currency() 是 Foundation 提供的解析器,支持本地化,处理了各种边界情况。你不需要自己写日期解析逻辑。
详细内容
用 Capture 提取数据
匹配只是第一步,提取数据才是关键。Capture 可以把匹配到的子串保存下来,追加到输出元组中(10:28):
let regex = Regex {
"a"
Capture("b")
"c"
/d(e)f/
}
if let match = "abcdef".wholeMatch(of: regex) {
let (wholeMatch, b, e) = match.output
// wholeMatch = "abcdef"
// b = "b"
// e = "e"
}
关键点:
match.output是一个元组,第一个元素是整个匹配,后面依次是各个Capture- 正则字面量中的括号
()也是捕获组 - 输出元组的类型在编译期就确定了
实战:解析 XCTest 日志
假设你要解析测试套件日志:
Test Suite 'RegexDSLTests' started at 2022-06-06 09:41:00.001
Test Suite 'RegexDSLTests' failed at 2022-06-06 09:41:00.001.
Test Suite 'RegexDSLTests' passed at 2022-06-06 09:41:00.001.
第一步,写出基础正则(06:24):
import RegexBuilder
let regex = Regex {
"Test Suite '"
/[a-zA-Z][a-zA-Z0-9]*/
"' "
ChoiceOf {
"started"
"passed"
"failed"
}
" at "
OneOrMore(.any)
Optionally(".")
}
关键点:
ChoiceOf匹配多个选项中的一个Optionally匹配零次或一次- 正则字面量
/[a-zA-Z][a-zA-Z0-9]*/可以嵌在 DSL 中
加上 Capture 提取数据(11:10):
let regex = Regex {
"Test Suite '"
Capture(/[a-zA-Z][a-zA-Z0-9]*/)
"' "
Capture {
ChoiceOf {
"started"
"passed"
"failed"
}
}
" at "
Capture(OneOrMore(.any))
Optionally(".")
}
for line in testInputs {
if let (whole, name, status, dateTime) = line.wholeMatch(of: regex)?.output {
print("\(name), \(status), \(dateTime)")
}
}
处理贪婪匹配问题
运行上面的代码,你会发现 dateTime 包含了末尾的句点。原因是 OneOrMore(.any) 默认是贪婪(eager)的,会尽可能多地匹配字符(11:51)。
修复方法是改成 reluctant(惰性)模式:
Capture(OneOrMore(.any, .reluctant))
关键点:
.reluctant让重复匹配尽可能少的字符- 引擎会先尝试匹配后面的模式,失败才回溯消费更多字符
- 也可以用
.repetitionBehavior(.reluctant)修改整个正则的默认行为
Transforming Capture:从子串到具体类型
捕获到的内容默认是 Substring,通常需要转换成更有意义的类型。Capture 支持 transform 闭包(15:20):
Regex {
Capture {
OneOrMore(.digit)
} transform: {
Int($0) // 输出类型变成 Int?
}
}
如果转换可能失败,用 TryCapture,它会把可选类型”拍平”(15:55):
Regex {
TryCapture {
OneOrMore(.digit)
} transform: {
Int($0) // 输出类型是 Int,不是 Int?
}
}
关键点:
TryCapture的 transform 返回Optional,但输出类型是非可选的- 转换失败时引擎会自动回溯,尝试其他匹配路径
- 适合配合可失败初始化器使用
用 Foundation 解析器处理日期
把状态转成枚举,把时间戳转成 Date(16:21):
enum TestStatus: String {
case started, passed, failed
}
let regex = Regex {
"Test Suite '"
Capture(/[a-zA-Z][a-zA-Z0-9]*/)
"' "
TryCapture {
ChoiceOf { "started"; "passed"; "failed" }
} transform: {
TestStatus(rawValue: String($0))
}
" at "
Capture(.iso8601(
timeZone: .current,
includingFractionalSeconds: true,
dateTimeSeparator: .space))
Optionally(".")
}
// 输出类型: Regex<(Substring, Substring, TestStatus, Date)>
关键点:
.iso8601()是 Foundation 提供的日期解析器- 输出元组的最后一个元素直接是
Date类型 - 不需要手动做二次解析
自定义解析器:接入 C 库
如果你想接入已有的 C 解析器,可以实现 CustomConsumingRegexComponent 协议(19:16):
import Darwin
struct CDoubleParser: CustomConsumingRegexComponent {
typealias RegexOutput = Double
func consuming(
_ input: String,
startingAt index: String.Index,
in bounds: Range<String.Index>
) throws -> (upperBound: String.Index, output: Double)? {
input[index...].withCString { startAddress in
var endAddress: UnsafeMutablePointer<CChar>!
let output = strtod(startAddress, &endAddress)
guard endAddress > startAddress else { return nil }
let parsedLength = startAddress.distance(to: endAddress)
let upperBound = input.utf8.index(index, offsetBy: parsedLength)
return (upperBound, output)
}
}
}
使用自定义解析器(20:13):
let regex = Regex {
"Test Case "
OneOrMore(.any, .reluctant)
"("
Capture {
CDoubleParser()
}
" seconds)."
}
if let match = input.wholeMatch(of: regex) {
print("Time: \(match.1)") // Double 类型
}
关键点:
consuming方法返回匹配的上界索引和解析结果- 返回
nil表示匹配失败,引擎会回溯 - 这是接入现有解析库(如 JSON、YAML 解析器)的桥梁
核心启发
1. 日志解析器
做什么:为你的项目写一个 Xcode 构建日志解析器,提取编译错误和警告。
为什么值得做:用 RegexBuilder 描述日志格式,用 TryCapture 把错误信息转成结构化数据,比正则字符串更易维护。
怎么开始:定义一个 BuildLogEntry 结构体,用 RegexBuilder 匹配 error: / warning: 前缀,用 Capture 提取文件名、行号和错误描述。
2. 智能剪贴板工具
做什么:做一个 macOS 工具,自动识别剪贴板内容并提取关键信息(如快递单号、手机号、URL)。
为什么值得做:Swift Regex 的 firstMatch 和 wholeMatch 可以直接用在剪贴板字符串上,配合 TryCapture 把匹配结果转成具体类型。
怎么开始:用 NSPasteboard 监听剪贴板变化,对文本内容尝试多种正则模式,匹配成功后显示提取结果。
3. 配置文件验证器
做什么:为团队内部的配置文件格式写一个验证工具,检查语法错误并给出友好提示。
为什么值得做:RegexBuilder 的可读性让配置规则一目了然,新成员能快速理解验证逻辑。
怎么开始:把配置文件的每一行定义成一个 Regex,用 wholeMatch 验证,失败时返回具体的错误位置和期望格式。
4. 数据迁移脚本
做什么:写脚本把旧系统的文本日志迁移到新数据库,用正则提取字段并转换类型。
为什么值得做:TryCapture 配合 Date / Int / Double 初始化器,一行正则就能完成提取+转换,不需要二次解析。
怎么开始:用 Foundation 的 .date() 和 .currency() 解析器处理日期和金额字段,自定义 CustomConsumingRegexComponent 处理特殊格式。
关联 Session
- Meet Swift Regex — Swift Regex 入门,介绍基础语法和核心概念
- What’s new in Foundation — Foundation 的格式化器和解析器如何与 RegexBuilder 配合
- Swift Concurrency — 了解 Swift 并发模型,配合字符串处理编写高性能解析工具
评论
GitHub Issues · utterances