大多数浏览器和
Developer App 均支持流媒体播放。
-
深入了解 Swift 正则表达式
在字符串处理知识的基础上,进一步深入探索 Swift 正则表达式。我们将简要介绍正则表达式及其运作方式,探索 Foundation 的富数据解析以及如何集成您自己的内容,并针对捕获开展深入讨论。我们还将提供一些最佳实践,帮助您轻松匹配字符串并运用由正则表达式提供支持的算法。
资源
- SE-0350: Regex type and overview
- SE-0351: Regex builder DSL
- SE-0354: Regex literals
- SE-0355: Regex syntax
- SE-0357: Regex-powered algorithms
相关视频
WWDC22
WWDC21
-
下载
♪ ♪
Richard: 大家好 我是 Richard 是 Swift 标准库团队的一名工程师 今天 让我们开始一段 深入了解 Swift 正则表达式的旅程 Swift 5.7 在字符串处理方面 获得了强大的新功能 它们从 Regex 类型开始 这是 Swift 标准库中的 一个新类型 语言内置的 Regex 文字量语法 使得这个强大而熟悉的概念更加原生 最后是 一个名为 RegexBuilder 的 结果生成器 API 它是一种特定于领域的语言或 DSL 它利用了结果生成器的语法简单性 以及可组合性 并将 Regex 的可读性 提升到一个全新的水平 有关 Swift Regex 为何更易于处理字符串的背景信息 请查看我的同事 Michael 的 讲座 “Swift 正则表达式简介” 我们来看一个非常简单的 Swift Regex 示例 假设我有一个数据字符串 我想从这个字符串中 匹配并提取用户 ID 我可以从文本创建一个正则表达式 就像我通常使用 NSRegularExpression 一样 它匹配 user_id 冒号 后面跟着零或多个空格 后跟一个或多个数字 这次不同的是 我们创建的是 Regex 类型的值 这是 Swift 标准库中的一个新类型 然后我可以使用字符串的 firstMatch 算法 来查找首次出现的此 Regex 定义模式 并打印整个匹配项 就像那样 因为我的 Regex 字符串 在编译时是已知的 所以我可以切换到使用 Regex 字面量 以便编译器 检查语法错误 Xcode 可以展现语法突出显示 但是为了最终的可读性和自定义 我可以使用 Regex 生成器 DSL 使用 Regex 生成器 阅读 Regex 的内容 就像读取原生 Swift API 一样容易 本期讲座中 我将向您展示 Regex 的工作原理 以及如何在工作流程中应用 Regex Regex 是由其底层 Regex 引擎 执行的程序 执行 Regex 时 Regex 引擎获取输入字符串 并从字符串的开始到末尾进行匹配 我们来看一个非常简单的 Regex 这个 Regex 匹配以一个 或多个字母 a 开头 后面跟着一个或多个数字的字符串 我使用一种匹配算法 wholeMatch 来匹配输入 aaa12 Regex 引擎将从输入的 第一个字符开始 首先 它匹配一个或多个字符 a 此时 它到达字符 1 并尝试将该字符与字符 a 进行匹配 但并不匹配 因此 Regex 引擎移动到 Regex 中的下一个模式 以匹配一个或多个数字 当我们到达字符串的末尾时 匹配成功 在本讲座余下的部分 我将进一步解释该执行模型 Regex 构建在其底层 Regex 引擎 Regex 生成器 DSL 上 而且 Regex 驱动的算法 扩展了其能力和表现力
Regex 驱动的算法是基于集合的 API 提供了一些最常见的操作 比如 firstMatch 查找字符串中第一个出现的 Regex wholeMatch 将整个字符串 与 Regex 匹配 prefixMatch 将字符串的前缀与 Regex 匹配 除了匹配之外 Swift 标准库还添加了 基于 Regex 的预测 替换 裁剪和拆分 API 此外 Regex 现在可以在 控制流语句中的 Swift 的模式匹配语法中使用 从而比以往更容易通过字符串来 switch 最后 比 Regex 生成器 和 Regex 驱动的算法更重要的是 今年 Foundation 引入了 自己的 Regex 支持 可以与 Regex 生成器无缝协作 Foundation 中的 Regex 支持 正是您可能 已经在使用的格式化器和解析器 例如 Date 和 Number 如果您想了解更多 关于这些 API 的信息 请观看 WWDC21 的讲座 What's new in Foundation 今年 Foundation 还增加了 对 URL 格式化和解析的支持 有了 Foundation 中的 Regex 支持 您可以直接在 Regex 生成器中 嵌入 Foundation 解析器 例如 要解析这样的银行对帐单 我可以使用 Foundation 提供的 带有自定义格式的日期解析器 和具有特定区域解析策略的货币解析器 这是一件非常重要的事情 因为您可以从已有的 久经考验的解析器创建 Regex 来处理临界状况 和支持本地化 并使用 Regex 生成器 DSL 的 表达能力将它们组合在一起 为了向您展示如何将 Swift Regex 应用到工作流程中 我们一起来完成一个例子 我一直在编写一个脚本 以解析运行 基于 XCTest 的单元测试日志 测试日志以测试套件的状态开始和结束 然后 XCTest 运行每个测试用例 并报告测试用例的状态 今天我们来解析日志的 第一行和最后一行 它是关于测试套件的信息 首先 我导入 RegexBuilder RegexBuilder 是 Swift 标准库中的一个新模块 它提供了 RegexBuilder DSL Regex 可以用一个表示 Regex 主体的 结尾闭包来初始化 我们来看一个日志消息示例 在这个日志中 我们要关注三个可变子字符串 测试套件的名称 状态 是启动 通过 还是失败 以及时间戳 我可以逐字解析这一行的其他部分 同时提出一个模式 来解析三个可变子字符串 日志消息以单词 test suite 开头 后跟一个空格和一个单引号 然后我们解析测试套件的名称 名称是一个标识符 可以包含小写或大写字母或数字 但第一个字符永远不能是数字
因此 我们创建了一个自定义字符类 来匹配一个字母作为第一个字符 然后匹配 0 或多个字符 这些字符要么是字母 要么是从 0 到 9 的数字 这个非常清晰易读 但是有点繁琐 许多开发者可能熟悉文本的 Regex 语法 在 RegexBuilder 中 我实际上可以直接在主体中嵌入 简明的 Regex 字面量 Regex 字面量以斜线开头和结尾 Swift 为其推断出正确的强类型 例如 此 Regex 匹配子字符串 Hello, WWDC! 它的输出类型是子字符串 但是一级 Regex 文字 真正酷的地方在于 强类型捕获组 例如 我可以编写一个捕获组 来捕获两个数字作为年份 并给这个捕获组命名为 “year” 当我这样做时 输出类型中将出现另一个子字符串 在本次演讲的后面 我将向您展示如何 使用捕获从字符串中提取信息 除了标准的 Regex 字面量 Swift 还支持扩展的 Regex 字面量 以井号斜线开始 以井号斜线结束 扩展的文字允许有非语义空格 在这种模式下 您可以将模式分成多行 在我的 RegexBuilder 中 嵌入了一个 Regex 字面量 它既清晰又熟悉 解析了测试名称之后 我解析单引号和空格 现在我达到了测试状态 测试状态有多种类型 开始 失败和通过 为了匹配其中一个选项 我们使用 ChoiceOf ChoiceOf 匹配 多个子模式中的一个 这正是我们需要的 接下来 我们解析 紧跟在状态之后的内容 一个空格 接着是 at 紧接着一个空格 字符串的其余部分是时间戳 我们可以将其匹配为 任意一个或多个字符 但是当我查看更多示例时 日志消息有时会以句号结尾 我们仍然希望使用 Optionally 来匹配可能存在的句号
要想通过 Regex 匹配输入 请使用提供的匹配算法之一 让我们使用 wholeMatch 它将整个字符串与 Regex 匹配 使用 wholeMatch 我匹配了每一条日志消息 并打印匹配的内容 匹配了 但我们不只是想知道 它是否匹配字符串 我们还希望提取所关心的信息 例如测试名称 状态 和时间戳 因此 让我们继续使用 Regex 最酷的功能之一 捕获 捕获在匹配期间保存一部分输入 它在 RegexBuilder 中 以 “Capture” 的形式提供 在 Regex 语法中 以一对括号的形式提供 捕获将匹配的子字符串 附加到输出元组类型 输出元组类型 以匹配整个 Regex 的 整个子字符串开头 然后是第一次捕获 第二次捕获 依此类推 匹配算法返回 Regex.Match 您可以从中获得输出元组 整个匹配 第一次捕获 第二次捕获
我在我的测试套件日志 Regex 中 使用捕获 我捕获测试套件的名称 状态 和时间戳 让我们再次在一些输入上 运行 Regex 并打印捕获的三个部分 看起来匹配很成功 它打印了名称 状态和时间戳
但当我仔细观察时 发现日期有点不对劲 它将句号作为捕获的一部分 包含在输入中 所以我回去检查 Regex 是否有错误 我想关注时间戳 Regex 看看它有什么问题 然后我意识到 “任意字符的一个或多个”模式 会消耗从时间戳的第一个数字 到行尾的所有内容 所以下面的 Optionally(".") 模式 不会匹配
我可以通过让这个 OneOrMore reluctant 来解决这个问题 “reluctant” 是重复行为的状况之一 一个或多个 0 或多个 可选和重复 是 Swift Regex 所谓的重复 默认情况下 重复是 eager 的 它匹配尽可能多的事件 我使用前面的例子 当 Regex 引擎急切 (eager) 地尝试 匹配任何字符的 OneOrMore 时 它会从第一个字符开始 并一直接受任何字符 直到输入的末尾 然后 Regex 引擎 继续匹配 Optionally(".") 没有更多的句号来匹配 但它是可选的 所以成功了 因为我们运行的是 wholeMatch 算法 输入和 Regex 模式都到达了末尾 匹配成功 虽然匹配成功了 但句号已经被意外地捕获为 OneOrMore 的一部分
当我们将重复行为更改为 reluctant 时 Regex 引擎匹配重复的方式 略有不同 它匹配尽可能少的字符 因此 当 Regex 引擎这次 匹配输入字符串时 它会小心地向前移动 总是首先尝试匹配 Regex 的其余部分 然后再消耗重复事件 当 Regex 的其余部分不匹配时 引擎返回到重复 并消耗额外的事件 我们快进到最后一个字符 句号 与 eager 的行为不同 Regex 引擎最初并未将句号作为 OneOrMore 的一部分使用 而是尝试匹配 Optionally(".") 模式 这是匹配的 并且 Regex 引擎 到达模式的末尾 因此匹配成功 并生成正确的捕获 它的结尾没有句号
因为 eager 是默认行为 所以在使用重复来创建 Regex 时 应该考虑它对预期匹配的影响 您可以通过传递一个额外的参数 来指定每次重复级别的行为 或者 您可以使用 repetitionBehavior 修改器 来覆盖所有未指定行为的重复 由于我们已将时间戳的重复行为 修改为 reluctant 所以匹配现在可以 提取正确的时间戳 但不包括句号
让我们回到 Regex 当我使用捕获 从输入中提取测试状态时 它的类型是子字符串 但是 如果我可以将子字符串 转换为对编程更友好 比如自定义数据结构 那就更好了 为此 我可以使用转换捕获 转换捕获是带有转换闭包的捕获 一旦匹配 Regex 引擎 就会调用匹配的子字符串上的 转换闭包 从而生成所需类型的结果 相应的 Regex 输出类型 成为闭包的返回类型 在这里 通过用 Int 的 初始化器从 String 转换捕获 我在输出元组类型中 获得了一个可选的 Int 要获得非可选输出 TryCapture 可以提供帮助 TryCapture 是 Capture 的 一个变体 它接受一个 返回可选值的转换 并删除输出类型中的可选性 在匹配期间返回 nil 将导致 Regex 引擎原路返回 并尝试可供代替的路径 当您使用可失败的 初始化器来转换捕获时 TryCapture 是最有用的 枚举类型天然适合于存储捕获的测试状态 我们来定义一个 我用三种情况定义了 TestStatus 枚举 started passed 和 failed 原始字符串值使这枚举 可从字符串初始化
在 Regex 中 我切换到带 transform 的 TryCapture 在 transform 闭包中 我调用 TestStatus 初始化器 来将匹配的子字符串 转换为 TestStatus 值 现在相应的输出类型是 TestStatus 使用这样的自定义数据结构 使 Regex 匹配输出类型是安全的 回到 Regex 我还想做一项额外的改进 目前 我使用通配符模式匹配时间戳 它会产生一个子字符串 这意味着如果我的 App 想要了解时间戳 那么它就必须再次将子字符串 解析为另一个数据结构 在讲座的早些时候 我提到 Foundation 现在支持 Swift Regex 提供行业实力解析器作为 Regex 因此 无需将日期解析为子字符串 我可以切换到 Foundation 的 ISO 8601 日期解析器 从而将时间戳解析为日期 现在推断的类型显示 这个 Regex 输出一个 Date
当我在输入上运行 wholeMatch 时 我可以看到日期字符串 被解析为一个 Foundation Date 值 可以以 Regex 形式使用 经过实战测试的解析器 如 Foundation 日期解析器 在日常的字符串处理任务中非常方便 接下来 我将向您展示一个高级功能 即重用在 Swift Regex 中 其他地方定义的预先存在的解析器 我们来看一个我们想要解析 测试用例持续时间的示例 持续时间为浮点数 例如 0.001 当然 最好的方法是 使用 Foundation 提供的 完全支持本地化的 浮点解析器 但是今天 我想向大家展示它的内部原理 以及如何自己连接到 Regex 引擎 以利用现有的解析器 来解析持续时间浮点数 strtod 是 C 标准库中的一个函数 它接受一个字符串指针 解析底层字符串 并将匹配的结束位置 分配给结束指针 让我们用 C 方法来解析持续时间 为此 我可以自己定义一个解析器类型 并使其符合 CustomConsumingRegexComponent 协议
我定义了一个名叫 CDoubleParser 的结构 它的 RegexOutput 是 Double 因为我们解析的是 Double 数字 在 “consuming” 方法中 我们从 C 标准库 调用 Double 解析器 将字符串指针传递给它 并返回一个数字 在方法主体中 我使用 withCString 方法 来获取起始地址 然后调用 strtod C 函数 传递起始地址 和一个指针来接收结果结束地址 然后检查错误 解析成功时 结束地址大于起始地址 否则 就是解析失败 所以返回 nil 我根据 C API 生成的指针 计算匹配的上限 最后 我返回匹配的上限和数字输出 我可以回到 Regex 并直接在 Regex 中使用我的 CDoubleParser 输出类型被推断为 Double 当我调用 wholeMatch 并打印解析后的数字时 它会像我预期的那样输出 0.001 总而言之 今天我们讨论了 Swift Regex 的一些 常见和高级用法 这是 Swift 5.7 中的一项新功能 可让您在 App 中 集成字符串处理的能力 使用 Swift Regex 时的 一个好做法是尝试在简洁性和 可读性之间取得良好的平衡 尤其是当您混合使用 RegexBuilder DSL 和 Regex 字面量时 当遇到日期和 URL 等常见模式时 请始终使用 Foundation 提供的行业实力解析器 因为用自定义代码解析这些模式 很容易出错
关于 Swift Regex 的更多信息 请查看 Swift Evolution 上的声明式字符串 处理建议系列 我希望您喜欢用 Swift 处理字符串 谢谢 祝您的 WWDC 之旅一切顺利
-
-
0:39 - Regex matching "Hi, WWDC22!"
Regex { "Hi, WWDC" Repeat(.digit, count: 2) "!" }
-
1:06 - Simple Regex from a string
let input = "name: John Appleseed, user_id: 100" let regex = try Regex(#"user_id:\s*(\d+)"#) if let match = input.firstMatch(of: regex) { print("Matched: \(match[0])") print("User ID: \(match[1])") }
-
1:56 - Simple Regex from a literal
let input = "name: John Appleseed, user_id: 100" let regex = /user_id:\s*(\d+)/ if let match = input.firstMatch(of: regex) { print("Matched: \(match.0)") print("User ID: \(match.1)") }
-
2:08 - Simple regex builder
import RegexBuilder let input = "name: John Appleseed, user_id: 100" let regex = Regex { "user_id:" OneOrMore(.whitespace) Capture(.localizedInteger) } if let match = input.firstMatch(of: regex) { print("Matched: \(match.0)") print("User ID: \(match.1)") }
-
2:38 - A trivial Regex interpreted by the Regex engine
let regex = Regex { OneOrMore("a") OneOrMore(.digit) } let match = "aaa12".wholeMatch(of: regex)
-
3:49 - Regex-powered algorithms
let input = "name: John Appleseed, user_id: 100" let regex = /user_id:\s*(\d+)/ input.firstMatch(of: regex) // Regex.Match<(Substring, Substring)> input.wholeMatch(of: regex) // nil input.prefixMatch(of: regex) // nil input.starts(with: regex) // false input.replacing(regex, with: "456") // "name: John Appleseed, 456" input.trimmingPrefix(regex) // "name: John Appleseed, user_id: 100" input.split(separator: /\s*,\s*/) // ["name: John Appleseed", "user_id: 100"] switch "abc" { case /\w+/: print("It's a word!") }
-
5:14 - Use Foundation parsers in regex builder
let statement = """ DSLIP 04/06/20 Paypal $3,020.85 CREDIT 04/03/20 Payroll $69.73 DEBIT 04/02/20 Rent ($38.25) DEBIT 03/31/20 Grocery ($27.44) DEBIT 03/24/20 IRS ($52,249.98) """ 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)) }
-
6:24 - XCTest log regex (version 1)
import RegexBuilder let regex = Regex { "Test Suite '" /[a-zA-Z][a-zA-Z0-9]*/ "' " ChoiceOf { "started" "passed" "failed" } " at " OneOrMore(.any) Optionally(".") }
-
6:25 - Test our Regex against some inputs
let testSuiteTestInputs = [ "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." ] for line in testSuiteTestInputs { if let match = line.wholeMatch(of: regex) { print("Matched: \(match.output)") } }
-
10:28 - Example of capture
let regex = Regex { "a" Capture("b") "c" /d(e)f/ } if let match = "abcdef".wholeMatch(of: regex) { let (wholeMatch, b, e) = match.output }
-
11:10 - XCTest log regex (version 2, with captures)
import RegexBuilder let regex = Regex { "Test Suite '" Capture(/[a-zA-Z][a-zA-Z0-9]*/) "' " Capture { ChoiceOf { "started" "passed" "failed" } } " at " Capture(OneOrMore(.any)) Optionally(".") }
-
11:21 - Test our Regex (version 2) against some inputs
let testSuiteTestInputs = [ "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." ] for line in testSuiteTestInputs { if let (whole, name, status, dateTime) = line.wholeMatch(of: regex)?.output { print("Matched: \"\(name)\", \"\(status)\", \"\(dateTime)\"") } }
-
11:51 - XCTest log regex (version 3, with reluctant repetition)
import RegexBuilder let regex = Regex { "Test Suite '" Capture(/[a-zA-Z][a-zA-Z0-9]*/) "' " Capture { ChoiceOf { "started" "passed" "failed" } } " at " Capture(OneOrMore(.any, .reluctant)) Optionally(".") }
-
15:20 - Example of transforming capture
Regex { Capture { OneOrMore(.digit) } transform: { Int($0) // Int.init?(_: some StringProtocol) } } // Regex<(Substring, Int?)>
-
15:55 - Example of transforming capture and removing optionality
Regex { TryCapture { OneOrMore(.digit) } transform: { Int($0) // Int.init?(_: some StringProtocol) } } // Regex<(Substring, Int)>
-
16:21 - XCTest log regex (version 4, with transforming capture)
enum TestStatus: String { case started = "started" case passed = "passed" case failed = "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(OneOrMore(.any, .reluctant)) Optionally(".") } // Regex<(Substring, Substring, TestStatus, Substring)>
-
17:23 - XCTest log regex (version 5, with Foundation ISO 8601 date parser)
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)>
-
18:19 - XCTest log duration parser
let input = "Test Case '-[RegexDSLTests testCharacterClass]' passed (0.001 seconds)." let regex = Regex { "Test Case " OneOrMore(.any, .reluctant) "(" Capture { .localizedDouble } " seconds)." } if let match = input.wholeMatch(of: regex) { print("Time: \(match.output)") }
-
19:16 - CDoubleParser definition
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 - Use CDoubleParser in regex builder
let input = "Test Case '-[RegexDSLTests testCharacterClass]' passed (0.001 seconds)." let regex = Regex { "Test Case " OneOrMore(.any, .reluctant) "(" Capture { CDoubleParser() } " seconds)." } // Regex<(Substring, Double)> if let match = input.wholeMatch(of: regex) { print("Time: \(match.1)") }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。