WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift Regex

Meet Swift Regex

观看原视频

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:55NegativeLookahead 是关键组件。它”偷看”后面的输入但不消费,确保字段内容在遇到分隔符时停止匹配:

OneOrMore {
    NegativeLookahead { fieldSeparator }  // 偷看:下一个不是分隔符才继续
    CharacterClass.any                    // 匹配任意字符
}

没有 NegativeLookaheadCharacterClass.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:55TryCapture 让你用闭包参与匹配过程,实现运行时验证:

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.indexSubstring 范围操作、.components(separatedBy:) 解析字符串的地方。特别是日志解析、CSV/TSV 处理、配置文件读取、用户输入验证。Swift Regex 的强类型捕获让解析结果直接可用,减少中间转换。

入口:split(separator:)prefixMatch(of:)firstMatch(of:)replacing(_:with:)

2. 用 Regex Builder 替代 NSRegularExpression

项目中还在用 NSRegularExpression 的地方,迁移到 Regex Builder。声明式语法可读性更好,强类型捕获避免 NSTextCheckingResult 的索引操作,Unicode 默认正确处理。可以从简单模式开始逐步迁移。

入口:import RegexBuilderRegex { ... }

3. 封装业务专用的 RegexComponent

把项目中重复使用的正则模式封装成 RegexComponent。比如邮箱验证、手机号格式、订单号规则、日志时间戳等。复用组件保证一致性,修改时只需改一处。

入口:struct EmailPattern: RegexComponent { ... }

4. 用 Local 控制性能敏感的正则

对于处理大量文本的场景(日志分析、数据清洗、实时过滤),检查正则是否存在不必要的全局回溯。字段分隔符、固定格式的 token 等精确模式用 Local { ... } 包裹,限制搜索空间。

入口:Local { /pattern/ }

关联 Session

评论

GitHub Issues · utterances