Highlight
Swift 5.7 在语言层面引入 Regex,支持
/.../字面量、运行时构造和 Regex Builder 三种创建方式,强类型捕获、Foundation 解析器嵌入、Unicode 正确处理,以及 Local 回溯控制等高级特性。
核心内容
字符串解析的痛点
你在和财务调查员合作开发一个交易分析工具。他们给你的数据是一堆纯文本字符串:
DEBIT 03/05/2022 Doug's Dugout Dogs $33.27
字段之间用”两个以上空格”或”制表符”分隔——没人记得为什么。日期格式 ambiguous,你只能祈祷是月/日/年。
(01:16)用 String 的集合算法试试?split(whereSeparator: \.isWhitespace) 会把 “Doug’s Dugout Dogs” 拆成三个字段。降到索引级别手动解析?代码冗长且容易出错:
var slice = transaction[...]
func extractField() -> Substring {
let endIdx = {
var start = slice.startIndex
while true {
guard let spaceIdx = slice[start...].firstIndex(where: \.isWhitespace) else {
return slice.endIndex
}
if slice[spaceIdx] == "\t" {
return spaceIdx
}
let afterSpaceIdx = slice.index(after: spaceIdx)
if afterSpaceIdx == slice.endIndex || slice[afterSpaceIdx].isWhitespace {
return spaceIdx
}
start = afterSpaceIdx
}
}()
defer { slice = slice[endIdx...].drop(while: \.isWhitespace) }
return slice[..<endIdx]
}
let kind = extractField()
let date = try Date(String(extractField()), strategy: Date.FormatStyle(date: .numeric))
let account = extractField()
let amount = try Decimal(String(extractField()), format: .currency(code: "USD"))
这段代码超过 20 行,而且只处理了字段拆分。你需要更好的工具。
Regex 字面量:简洁的语法
(02:47)Swift 5.7 引入了 /.../ Regex 字面量语法,兼容 Perl、Python、Ruby、Java 等主流正则方言:
let digits = /\d+/
// digits: Regex<Substring>
关键点:
- 编译器认识正则语法,提供语法高亮和编译时错误检查
- 输出类型是泛型
Regex<Output>,包含捕获类型信息
先用正则解决字段拆分问题:
let transaction = "DEBIT 03/05/2022 Doug's Dugout Dogs $33.27"
let fragments = transaction.split(separator: /\s{2,}|\t/)
// ["DEBIT", "03/05/2022", "Doug's Dugout Dogs", "$33.27"]
(03:56)\s{2,} 匹配两个以上空白字符,| 表示或,\t 匹配制表符。一行代码替代了之前 20 多行的索引操作。
还可以用 replacing 把分隔符统一:
let normalized = transaction.replacing(/\s{2,}|\t/, with: "\t")
// DEBIT\t03/05/2022\tDoug's Dugout Dogs\t$33.27
Regex Builder:声明式字符串处理
(03:44)字面量简洁,但复杂正则难以阅读。Regex Builder 提供了声明式语法:
import RegexBuilder
let fieldSeparator = /\s{2,}|\t/
let transactionMatcher = Regex {
/CREDIT|DEBIT/
fieldSeparator
One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt))
fieldSeparator
OneOrMore {
NegativeLookahead { fieldSeparator }
CharacterClass.any
}
fieldSeparator
One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")))
}
关键点:
Regex { ... }是声明式构建器,类似 SwiftUI 的 ViewBuilder- 可以混用字面量(
/CREDIT|DEBIT/)和构建器组件 One(.date(...))直接嵌入 Foundation 的日期解析器One(.localizedCurrency(...))嵌入货币解析器
(06:55)NegativeLookahead 是关键组件。它”偷看”后面的输入但不消费,确保字段内容在遇到分隔符时停止匹配:
OneOrMore {
NegativeLookahead { fieldSeparator } // 偷看:下一个不是分隔符才继续
CharacterClass.any // 匹配任意字符
}
没有 NegativeLookahead,CharacterClass.any 会贪婪匹配到行尾,然后一路回溯,效率低下。
强类型捕获
(09:04)用 Capture 提取感兴趣的字段,类型直接体现在 Regex 的泛型参数中:
let transactionMatcher = Regex {
Capture { /CREDIT|DEBIT/ }
fieldSeparator
Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) }
fieldSeparator
Capture {
OneOrMore {
NegativeLookahead { fieldSeparator }
CharacterClass.any
}
}
fieldSeparator
Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) }
}
// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>
关键点:
- 第 0 个捕获是整个匹配文本(
Substring) - 第 1 个捕获是交易类型(
Substring) - 第 2 个捕获是日期(
Date,Foundation 解析器直接产出) - 第 3 个捕获是机构名称(
Substring) - 第 4 个捕获是金额(
Decimal,Foundation 解析器直接产出)
使用时直接访问强类型属性:
func parseLine(_ line: Substring) throws -> MailmapEntry {
let regex = /\h*([^<#]+?)??\h*<([^>#]+)>\h*(?:#|\Z)/
guard let match = line.prefixMatch(of: regex) else {
throw MailmapError.badLine
}
return MailmapEntry(name: match.1, email: match.2)
}
运行时构造与扩展分隔符
(03:20)有些正则需要在运行时从用户输入构造:
let runtimeString = #"\d+"#
let digits = try Regex(runtimeString)
// digits: Regex<AnyRegexOutput>
关键点:
- 运行时构造可能抛出语法错误
- 输出类型是
AnyRegexOutput,因为捕获信息在编译时未知
(10:53)对于包含斜杠的正则,用扩展分隔符 #/.../#:
let regex = #/
(?<date> \d{2} / \d{2} / \d{4})
(?<middle> \P{currencySymbol}+)
(?<currency> \p{currencySymbol})
/#
// Regex<(Substring, date: Substring, middle: Substring, currency: Substring)>
关键点:
#/.../#允许正则内部包含未转义的斜杠- 扩展语法模式下空白被忽略,可以用缩进提高可读性
(?<name>...)是命名捕获,输出类型带标签
Unicode 正确处理
(12:45)Swift 的 String 默认处理 Unicode extended grapheme clusters(字符级别)。Regex 也遵循这个模型:
let aZombieLoveStory = "🧟♀️💖🧠"
// Characters: 🧟♀️, 💖, 🧠
aZombieLoveStory.unicodeScalars
// U+1F9DF, U+200D, U+2640, U+FE0F, U+1F496, U+1F9E0
(14:12)Swift String 遵循 Unicode Canonical Equivalence。"café" 和 "cafe\u{301}" 比较结果相等,尽管底层 Unicode scalars 不同。
Regex 默认也在字符级别匹配:
input.firstMatch(of: /.\N{SPARKLING HEART}./)
// 🧟♀️💖🧠 — 匹配三个字符
input.firstMatch(of: /.\N{SPARKLING HEART}./.matchingSemantics(.unicodeScalar))
// ️💖🧠 — 匹配三个 Unicode scalar,注意前面的 variation selector
关键点:
- 默认
.matchingSemantics(.graphemeCluster)匹配 Swift Character .matchingSemantics(.unicodeScalar)匹配 Unicode scalar 级别- 大部分场景用默认行为即可,需要精确控制时才切换
Local 回溯控制
(21:45)正则的回溯机制是把双刃剑。全局回溯适合搜索和模糊匹配,但精确匹配时会造成性能问题:
let fieldSeparator = /\s{2,}|\t/
// 8 个空格时,匹配全部 8 个;后续失败时回溯到 7 个、6 个...
Local 构建器创建局部回溯作用域,成功匹配后丢弃未尝试的替代方案:
let fieldSeparator = Local { /\s{2,}|\t/ }
let transactionMatcher = Regex {
Capture { /CREDIT|DEBIT/ }
fieldSeparator // 匹配全部空白,后续失败也不回溯
TryCapture(field) { timestamp ~= $0 ? $0 : nil }
fieldSeparator
TryCapture(field) { details ~= $0 ? $0 : nil }
// ...
}
关键点:
Local { ... }相当于原子非捕获组(atomic non-capturing group)- 适合精确指定的 token,如字段分隔符
- 限制了搜索空间,提高匹配效率
详细内容
TryCapture:在匹配中执行自定义逻辑
(18:55)TryCapture 让你用闭包参与匹配过程,实现运行时验证:
let transactionMatcher = Regex {
Capture { /CREDIT|DEBIT/ }
fieldSeparator
TryCapture(field) { timestamp ~= $0 ? $0 : nil } // 运行时验证 timestamp 正则
fieldSeparator
TryCapture(field) { details ~= $0 ? $0 : nil } // 运行时验证 details 正则
fieldSeparator
// ...
}
关键点:
TryCapture的闭包返回nil表示匹配失败- 闭包主动参与匹配,可以在匹配过程中做任意验证
- 适合整合已有的运行时正则或自定义解析逻辑
可复用的 Regex 组件
(27:05)把常用模式封装成 RegexComponent,在不同地方复用:
struct MailmapLine: RegexComponent {
@RegexComponentBuilder
var regex: Regex<(Substring, Substring?, Substring)> {
ZeroOrMore(.horizontalWhitespace)
Optionally {
Capture(OneOrMore(.noneOf("<#")))
}
.repetitionBehavior(.reluctant)
ZeroOrMore(.horizontalWhitespace)
"<"
Capture(OneOrMore(.noneOf(">#")))
">"
ZeroOrMore(.horizontalWhitespace)
/#|\Z/
}
}
// 使用
let match = line.prefixMatch(of: MailmapLine())
关键点:
RegexComponent协议让自定义类型可以作为正则组件使用@RegexComponentBuilder属性包装器启用构建器语法- 可以在组件内部混用字面量和构建器
嵌入 Foundation 解析器
struct DatedMailmapLine: RegexComponent {
@RegexComponentBuilder
var regex: Regex<(Substring, Substring?, Substring, Date)> {
// ... 前面的邮件映射模式 ...
Capture(.iso8601.year().month().day()) // 直接产出 Date
ZeroOrMore(.horizontalWhitespace)
/#|\Z/
}
}
关键点:
.iso8601.year().month().day()是 Foundation 的日期解析策略- 捕获类型直接是
Date,不需要后续手动转换 - 任何符合解析策略的类型都可以嵌入 Regex Builder
核心启发
1. 用 Regex 替换所有字符串索引操作
检查项目里所有用 String.index、Substring 范围操作、.components(separatedBy:) 解析字符串的地方。特别是日志解析、CSV/TSV 处理、配置文件读取、用户输入验证。Swift Regex 的强类型捕获让解析结果直接可用,减少中间转换。
入口:split(separator:)、prefixMatch(of:)、firstMatch(of:)、replacing(_:with:)
2. 用 Regex Builder 替代 NSRegularExpression
项目中还在用 NSRegularExpression 的地方,迁移到 Regex Builder。声明式语法可读性更好,强类型捕获避免 NSTextCheckingResult 的索引操作,Unicode 默认正确处理。可以从简单模式开始逐步迁移。
入口:import RegexBuilder,Regex { ... }
3. 封装业务专用的 RegexComponent
把项目中重复使用的正则模式封装成 RegexComponent。比如邮箱验证、手机号格式、订单号规则、日志时间戳等。复用组件保证一致性,修改时只需改一处。
入口:struct EmailPattern: RegexComponent { ... }
4. 用 Local 控制性能敏感的正则
对于处理大量文本的场景(日志分析、数据清洗、实时过滤),检查正则是否存在不必要的全局回溯。字段分隔符、固定格式的 token 等精确模式用 Local { ... } 包裹,限制搜索空间。
入口:Local { /pattern/ }
关联 Session
- What’s new in Swift — Swift 5.7 新特性总览,包含 Regex 和 RegexBuilder 的引入背景
- Design protocol interfaces in Swift — Swift 5.7 泛型改进,Regex 的强类型捕获依赖于这些语言特性
- Meet Swift Async Algorithms — 异步算法库,可与 Regex 配合处理流式文本数据
评论
GitHub Issues · utterances