大多数浏览器和
Developer App 均支持流媒体播放。
-
Swift 正则表达式简介
了解如何在利用 Swift 正则表达式时更高效地处理字符串。让字面值更简洁,同时保留正则表达式生成器 (一种全新的声明式字符串处理方法)。我们还将探索"字符串"中的 Unicode 模型,分享 Swift 正则表达式可以如何简化 Unicode 正确性处理。
资源
相关视频
WWDC22
-
下载
大家好 我是 Michael Ilseman 是 Swift 标准库团队的一名工程师 加入我 让我们认识 并了解 Swift 中的 Regex Swift Regex 有很多功能 我们将体验它支持的所有功能 假设我们是开发人员 正在与金融调查人员合作开发一种 分析交易违规行为的工具 现在 您会认为 对于一项如此重要的任务 我们应该处理良构的数据 相反 我们面对的是一堆字符串 第一个字段表示交易类型 第二个字段表示交易日期 第三个字段表示个人或机构 第四个也是最后一个字段 是以美元为单位的金额 字段由两个或多个空格 或一个制表符来分隔 源于一个非常重要的技术原因 没有人记得了 是的 日期字段是完全模糊的 我们不妨假设它是月 日 年 看看会发生什么 处理这些交易涉及到处理字符串 而字符串是一个集合 这意味着我们可以访问泛型集合算法 这些算法基本上有两种 一种是针对元素的 另一种是针对索引的 我们可以尝试通过拆分交易字段 来使用基于元素的算法 但是字段分隔符是制表符 或两个或多个空格 使得这样做很困难 仅靠空格分割并不能解决问题 另一种方法是下拉到 低级索引操作代码
但是很难正确地进行 即使您知道自己在做什么 也仍然需要编写大量代码 我们回到拆分 这种方法不起作用的原因是 它是基于元素的 而字段分隔符是更复杂的模式 在多种语言中找到的解决方案是 编写正则表达式 regex 源于形式语言理论 它们定义了一种正则语言 它们运用于编辑器和 命令行工具的实际应用 以及编译器的词法分析 这些应用对正则表达式的接受程度 超越了它们的理论根源 因为它们需要提取部分输入 控制和直接执行 并增强表达能力 Swift 正进一步推动它们 我们称之为 Regex Regex 是 Output 之上的结构泛型 Output 是应用它的结果 包括捕获 您可以在斜杠分隔符之间 使用包含正则表达式语法的字面量 来创建它 Swift 的正则表达式语法 与 Perl Python Ruby Java NSRegularExpression 以及很多很多其他语言兼容 此 regex 匹配一个或多个数字 编译器知道正则表达式语法 所以会得到语法高亮显示 编译时错误 甚至是强类型捕获 这些我们稍后会讲到 可以在运行时从包含相同 正则表达式语法的字面量创建 regex 这对于编辑器或命令行工具中的 搜索字段很有用 如果输入包含无效语法 那么就会在运行时引发错误 输出类型是存在的 AnyRegexOutput 因为直到运行时 才会知道捕获的类型和数量
同样的 regex 也可以使用 声明性的 结构良好的 regex 生成器来编写 尽管更加冗长
我们从之前使用的 regex 字面量拆分 第一部分匹配两次或多次出现的 任意空格字符 第二部分匹配单个水平制表符 管道符表示选项之间的选择 为我们提供两个或多个空格 或单个制表符的字段分隔符 现在我们的字段被拆分了 让我们为文明本身做出贡献 并将字段分隔符规范化为 单个制表符并完成它 我们可以对拆分后的 结果调用 join 但有一个更好的算法 “replacing” 让我们用单个制表符 替换所有字段分隔符
因此 我们走出去 向任何愿意倾听的人 传播我们显然很优越的方法 采纳缓慢但有希望 如果您熟悉 regex 您可能也知道它好坏参半的名声 俗话说 “我遇到了问题 所以我编写了一个正则表达式 现在我有两个问题” 但 Swift regex 是不同的
Swift 在四个关键领域推进这门艺术 Regex 语法简洁且富有表现力 但它可能变得简洁且难以阅读 更新的功能必须使用 越来越神秘的语法
我们可以使用 通过 Regex 生成器来构建 和组织源代码的方式 来构建和组织 Swift Regex 字面量简洁 生成器结构化 而在生成器中可以使用字面量 来找到完美的平衡 数据的文本表示变得复杂很多 正确地处理它们需要符合标准的解析器 Swift regex 允许将 工业强度的解析器 作为 regex 的单个组件交替使用 这是以库可扩展的方式完成的 意味着任何解析器都可以参与
应用正则表达式的大部分历史 发生在整个计算机系统只支持一种 语言和编码的世界 最著名的 ASCII 但现代世界是 Unicode Swift regex 在不影响 表现力的情况下使用 Unicode 最后正则表达式的强大功能 可以打开一个必须彻底探索的 广阔搜索空间 这使得它们的执行情况难以推敲 有些语言支持控件 但由于它们的语法晦涩难懂 所以它们往往是模糊的 Swift regex 提供可预测的执行 并突出显示控件 让我们回到已经处理过的财务报表 并使用 Regex 生成器 完全解析每笔交易 这是 Swift 中处理字符串的一种 声明性方法 我们将导入 RegexBuilder 模块来开始 我们可以重用刚刚定义的 字段分隔符 regex 第一个字段很简单 可以是 CREDIT 或 DEBIT 我们可以使用已经看到的 regex 字面量语法来编写它 之后是字段分隔符 然后是日期 手动解析日期是个坏主意 Foundation 对于日期 数字和 URL 等类型有非常好的解析器 我们可以直接在 Regex Builder 中使用它们
我们提供了一个明确的区域设置 这是我们对作者意图的最佳猜测 我们这样做 而不是隐式地 使用系统的当前区域设置 以后我们可以经常更改 而且很容易做到 因为我们将猜测显式展示在代码中
第三个字段可以是 “any” 所以很容易写成 “OneOrMore { any }” 虽然这会给我们正确的答案 但它首先会做很多不必要的工作 因为它首先会匹配后面的任何东西 regex 将每次备份一个字符 并尝试模式的其余部分 我们想要告诉 regex 在看到终止字段分隔符时停止 有很多方法可以实现这一点 有一个很好的方法是使用 NegativeLookahead 来查看输入的下一部分 而不实际使用它 这里我们查看输入 以确保在匹配任何字符之前 不会出现字段分隔符 NegativeLookahead 是一系列工具之一 可以让您精确控制 Regex 如何匹配其组件
最后 我们再次使用 Foundation 的一个解析器来匹配金额 这次匹配的是货币 我们一直假设逗号 是千位分隔符 而句点是小数分隔符 我们明确了该假设 我们已经构建了一个 regex 它可以让我们解析交易分类账的一行 我们不只是想识别几行 还想从中提取一些数据 为此 我们使用 capture 来提取输入的部分内容 以便稍后进行处理 按照惯例 “第 0 个”捕获 是输入中 匹配整个 regex 的部分 每个显式捕获跟随其后 我们的交易类型被捕获为 一个子字符串 它是我们输入的一部分 对于日期 我们实际上 捕获了解析出来的强类型值 而不需要对文本进行后处理 个人或机构再次 被捕获为输入的一部分 十进制捕获是另一个强类型值 为了使用它 我们从匹配结果中 提取日期和小数值 然后调查人员从这里获取数据 在这一点上 我们建议 他们将数据转存到真实数据库中 以获得明显的好处 例如结构化查询 他们有不同的意见 他们希望所有东西都是字符串 这对于此次演讲来说是个好消息 因为我们可以看到更多 Swift Regex 一切都很顺利 突然就遇到阻碍了 我们刚刚了解到 交易文本中的 日期顺序实际上是不明确的 我们告诉所有人它是完全不明确的 这并不总是相同的 主要的理论是 这取决于交易中使用的货币 还能是什么 这意味着美元是月 日 年 英镑是日 月 年 因此 我们编写一个 类似 sed 的脚本来消除歧义 对于 regex 我们将使用扩展分隔符 这允许我们在内部使用斜杠 而无需避开它们 这也使我们能够访问一种 扩展的语法模式 其中空格被忽略 这意味着我们可以使用空格来提高 可读性 就像在普通代码中一样 我们使用了命名的捕获 它在 Regex 的输出中显示为元组标签 我们使用 Unicode Property 来识别货币符号 这使得 regex 的适应性更强 我们将处理应用逻辑中的特定符号
我们将再次使用 Foundation 的日期解析器 而不是尝试手动剪切和拼接文本 我们将再次使用 Foundation 的日期解析器 pickStrategy 接收货币符号 并将根据它来确定解析策略 我们所有的假设 在代码中都是明确的 这使我们更容易适应和发展 这差不多就是我们最终需要的
我们将 regex 和 helper 函数 与查找及替换算法一起使用 方法是提供一个闭包 该闭包使用匹配结果 包括捕获 来构造替换字符串 我们根据捕获的货币 选择策略并解析捕获日期 我们可以通过名称访问捕获 而不仅仅是位置 对于输出 我们将使用 明确的行业标准 ISO-8601 来格式化新的日期 我们的工具将这个账本 转换成一个无歧义的版本 因为我们使用的是真正的 日期解析器和格式化器 所以我们更能适应不断变化的需求 使用 Unicode Property 来识别货币符号 有助于我们更快地进化 regex 声明了某个字符串模型上的算法 Swift 的字符串提供了 多种使用 Unicode 的模型 这个字符串代表着永恒的爱情故事 它包含 3 个字符 这些字符是正式称为 Unicode 扩展字位簇的复杂实体 单个字符由一个或多个 Unicode 标量值组成 字符串提供了一个 UnicodeScalarView 来访问其内容的低级表示 这可以实现高级使用 以及与其他系统的兼容性
我们的第一个字符是 我们故事的主角 由 4 个 Unicode 标量组成 ZOMBIE 零宽度连接件 FEMALE SIGN 还有 VARIATION SELECTOR-16 在这种情况下 表示偏好呈现为表情符号 当然了 这些标量生成了 我们视觉上看到的单个表情符号 当字符串存储在内存中时 它们被编码为 UTF-8 字节 我们可以使用 UTF-8 视图 来查看这些字节 UTF-8 是一种可变宽度编码 这意味着单个标量可能需要 多个字节 正如我们所见 单个字符可能需要多个标量 我们故事的主角用 4 个 Unicode 标量表示 用 13 个 UTF-8 字节编码 除了由多个标量组成外 相同的字符有时还可以 由不同的标量集表示 这在处理英语以外的语言时经常出现 在这个例子中 带有重音的 e 在这个例子中 e 加了尖音符号 或作为 ASCII 码的 e 后面跟了一个组合尖音符号 这些都是相同的字符 所以字符串比较将返回 true 这是因为字符串遵循形式上所谓的 Unicode Canonical Equivalence
从 UnicodeScalarView 或 UTF-8 视图的角度来看 内容是不同的 当我们在这些较低级别视图中 进行比较时就会看到这种差异 就像字符串一样 Swift regex 是默认强制 Unicode 正确的 但它做到这一点并没有影响表现力 让我们切换一对字符串 对于第一个字符串 我们将匹配 由点 (.) 表示的任何字符包围的 已命名的 Unicode 标量 SPARKLING HEART
任何字符类将匹配任何 Swift 字符 也就是任何 Unicode 扩展字位簇
对于第二个字符串 相等的字符比较为相等 我们可以忽略大小写 现在我们简单的爱情故事 变得复杂了很多 有时候 生命 或者是本例中的非生命 有我们需要处理的复杂性
就像字符串一样 如果您需要 自己处理 Unicode 标量值 无论是为了兼容性 还是为了子字位簇的精度 您可以通过匹配 unicodeScalar 语义来处理 当我们在 Unicode 标量级别匹配时 点匹配单个 Unicode 标量值 而不是完整的 Swift Character 这意味着我们可以再次见到我们的朋友 VARIATION-SELECTOR 16 这个友好的选择器通过点匹配 您看不到它 因为当它单独存在时 会呈现为空白 很有帮助
既然我们已经学习了精确性和正确性 让我们做一些不同的事情 回到金融上面来 调查人员回来了 这次他们提出了一个有趣的要求 他们修改了我们的交易匹配工具 使其能够 实时嗅探交易 而不是事后处理账本 看看他们的代码 他们实际上做得相当不错 但他们面临着扩展的问题 需要我们的帮助 他们正在处理的交易非常相似 但略有不同 没有日期 但有一个精确的时间戳 这是以一种清晰 明确和令人震惊的 专有格式表示的 他们有在上个世纪编写的正则表达式 可以很好地匹配这个 很好 接下来 他们有一个详细信息字段 其中包括个人和识别代码 他们通过使用运行时从输入派生的 编译的 regex 来从该字段过滤交易 因为这是实时的 而且后面还有更多的字段 所以对任何无关的交易 他们都希望可以提前退出 然后是数量和校验等字段 他们自己处理就可以了 当然 字段仍然由两个 或多个空格或一个制表符来分隔
他们的交易匹配器看起来 很像我们的 他们有自己的时间戳 regex 详细信息 regex 是从输入编译的 他们处理其余的字段 他们做得相当不错 全是技术活 它只是不能很好地扩展 他们注意到他们的时间戳 和详细信息 regex 通常比他们的字段匹配更多的输入 理想情况下 这些 regex 将被限制为仅在单个字段上运行 在我们的项目中 通过使用 NegativeLookahead 来处理类似问题 所以我们把 regex 拉进来
“field” 将有效地匹配任何字符 直到遇到字段分隔符 我们希望使用它 来包含它们的 regex 我们可以将其作为后处理步骤来执行 但因为这是实时运行的 所以如果这些 regex 与其字段不匹配 我们希望尽早放弃 我们可以使用 TryCapture 来做到这一点 TryCapture 将匹配的字段 传递给我们的闭包 在这里我们根据调查者的时间戳 和详细信息 regex 进行测试 如果它们匹配 我们返回字段的值 这意味着匹配成功 字段被捕获 否则我们就返回 nil 表示匹配失败 TryCapture 的闭包积极参与匹配 这正是我们所需要的 有了这个 我们解决了一个 主要的规模问题 但还有一个问题 当交易匹配器中稍后出现故障时 可能需要很长时间才能退出
我们在一开始定义的 fieldSeparator regex 匹配两个或多个空格或一个制表符 这正是我们想要的 如果有 8 个空白字符 它将在尝试其余的 regex 之前 匹配所有的字符 但如果 regex 稍后失败 那么它将备份 并仅匹配 7 个空白字符 然后再试一次 如果失败 它将只匹配 6 个空白字符 以此类推
只有在尝试了所有选项之后 匹配才会失败 这种为了尝试替代方案 而进行的备份被称为全局回溯 或者在正式的逻辑中称为 Kleene closure 正是它赋予了 regex 特有的强大功能 但它开辟了一个广阔的 搜索空间来探索 这里我们想要一个更线性的搜索空间 我们想要匹配所有的空格 并且不放弃任何空格 我们可以使用一些工具 更通用的工具是将 fieldSeparator 放在本地回溯范围 而不是全局范围
Local 生成器创建一个范围 如果包含的 regex 成功匹配 那么就丢弃任何未尝试的替代项
即使我们的交易匹配器稍后失败 也不会返回去尝试消耗更少的空间 全局回溯 regex 的默认值 非常适合搜索和模糊匹配 Local 对于精确匹配 指定的标记非常有用 字段分隔符虽然令人烦恼 但却是精确的
Local 在其他地方被称为 原子非捕获组 这可能是一个可怕的名字 看起来您的 regex 可能会爆炸 但实际上它的作用正好相反 它包含了搜索空间
有了这个 我们帮助他们解决了扩展问题 今天 我们了解了 Swift Regex 但是还有很多我们未能讲到的内容 一定要看看我的同事 Richard 的 “深入了解 Swift 正则表达式” 在我们离开之前 我想强调几点 regex 生成器给出结构 regex 字面量简洁 在何时使用其中一种 而非另一种的选择 最终将是主观的 只要可能 请确保使用真正的解析器 这将为您节省大量的时间 避免头痛 只要使用 Swift 的默认设置 您就会得到比其他任何地方 更多的 Unicode 支持和好处 寻找有效使用 字符属性等内容的方法 例如匹配货币符号 最后 通过使用前瞻 和本地回溯范围等控件 来简化搜索和处理算法 感谢收看
-
-
1:35 - Processing collections
let transaction = "DEBIT 03/05/2022 Doug's Dugout Dogs $33.27" let fragments = transaction.split(whereSeparator: \.isWhitespace) // ["DEBIT", "03/05/2022", "Doug\'s", "Dugout", "Dogs", "$33.27"]
-
1:49 - Low-level index manipulation
var slice = transaction[...] // Extract a field, advancing `slice` to the start of the next field func extractField() -> Substring { let endIdx = { var start = slice.startIndex while true { // Position of next whitespace (including tabs) guard let spaceIdx = slice[start...].firstIndex(where: \.isWhitespace) else { return slice.endIndex } // Tab suffices if slice[spaceIdx] == "\t" { return spaceIdx } // Otherwise check for a second whitespace character let afterSpaceIdx = slice.index(after: spaceIdx) if afterSpaceIdx == slice.endIndex || slice[afterSpaceIdx].isWhitespace { return spaceIdx } // Skip over the single space and try again 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"))
-
2:47 - Regex literals
// Regex literals let digits = /\d+/ // digits: Regex<Substring>
-
3:20 - Regex created at run-time
// Run-time construction let runtimeString = #"\d+"# let digits = try Regex(runtimeString) // digits: Regex<AnyRegexOutput>
-
3:44 - Regex builder
// Regex builders let digits = OneOrMore(.digit) // digits: Regex<Substring>
-
3:56 - Split approach with a regex literal
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"]
-
4:36 - Normalize field separators
let transaction = "DEBIT 03/05/2022 Doug's Dugout Dogs $33.27" let normalized = transaction.replacing(/\s{2,}|\t/, with: "\t") // DEBIT»03/05/2022»Doug's Dugout Dogs»$33.27
-
6:55 - Create a Regex builder
// CREDIT 03/02/2022 Payroll from employer $200.23 // CREDIT 03/03/2022 Suspect A $2,000,000.00 // DEBIT 03/03/2022 Ted's Pet Rock Sanctuary $2,000,000.00 // DEBIT 03/05/2022 Doug's Dugout Dogs $33.27 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"))) }
-
9:04 - Use Captures to extract portions of input
let fieldSeparator = /\s{2,}|\t/ 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)>
-
10:31 - Plot twist!
private let ledger = """ KIND DATE INSTITUTION AMOUNT ---------------------------------------------------------------- CREDIT 03/01/2022 Payroll from employer $200.23 CREDIT 03/03/2022 Suspect A $2,000,000.00 DEBIT 03/03/2022 Ted's Pet Rock Sanctuary $2,000,000.00 DEBIT 03/05/2022 Doug's Dugout Dogs $33.27 DEBIT 06/03/2022 Oxford Comma Supply Ltd. £57.33 """ // 😱
-
10:53 - Use named captures
let regex = #/ (?<date> \d{2} / \d{2} / \d{4}) (?<middle> \P{currencySymbol}+) (?<currency> \p{currencySymbol}) /# // Regex<(Substring, date: Substring, middle: Substring, currency: Substring)>
-
11:33 - Use Foundation's date parser
let regex = #/ (?<date> \d{2} / \d{2} / \d{4}) (?<middle> \P{currencySymbol}+) (?<currency> \p{currencySymbol}) /# // Regex<(Substring, date: Substring, middle: Substring, currency: Substring)> func pickStrategy(_ currency: Substring) -> Date.ParseStrategy { switch currency { case "$": return .date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt) case "£": return .date(.numeric, locale: Locale(identifier: "en_GB"), timeZone: .gmt) default: fatalError("We found another one!") } }
-
11:48 - Find and replace
let regex = #/ (?<date> \d{2} / \d{2} / \d{4}) (?<middle> \P{currencySymbol}+) (?<currency> \p{currencySymbol}) /# // Regex<(Substring, date: Substring, middle: Substring, currency: Substring)> func pickStrategy(_ currency: Substring) -> Date.ParseStrategy { … } ledger.replace(regex) { match -> String in let date = try! Date(String(match.date), strategy: pickStrategy(match.currency)) // ISO 8601, it's the only way to be sure let newDate = date.formatted(.iso8601.year().month().day()) return newDate + match.middle + match.currency }
-
12:45 - A zombie love story
let aZombieLoveStory = "🧟♀️💖🧠" // Characters: 🧟♀️, 💖, 🧠
-
13:01 - A zombie love story in unicode scalars
aZombieLoveStory.unicodeScalars // Unicode scalar values: U+1F9DF, U+200D, U+2640, U+FE0F, U+1F496, U+1F9E0
-
13:44 - A zombie love story in UTF8
aZombieLoveStory.utf8 // UTF-8 code units: F0 9F A7 9F E2 80 8D E2 99 80 EF B8 8F F0 9F 92 96 F0 9F A7 A0
-
14:12 - Unicode canonical equivalence
"café".elementsEqual("cafe\u{301}") // true
-
14:49 - String's views are compared at binary level
"café".elementsEqual("cafe\u{301}") // true "café".unicodeScalars.elementsEqual("cafe\u{301}".unicodeScalars) // false "café".utf8.elementsEqual("cafe\u{301}".utf8) // false
-
15:14 - Unicode processing
switch ("🧟♀️💖🧠", "The Brain Cafe\u{301}") { case (/.\N{SPARKLING HEART}./, /.*café/.ignoresCase()): print("Oh no! 🧟♀️💖🧠, but 🧠💖☕️!") default: print("No conflicts found") }
-
15:54 - Complex scalar processing
let input = "Oh no! 🧟♀️💖🧠, but 🧠💖☕️!" input.firstMatch(of: /.\N{SPARKLING HEART}./) // 🧟♀️💖🧠 input.firstMatch(of: /.\N{SPARKLING HEART}./.matchingSemantics(.unicodeScalar)) // ️💖🧠
-
17:56 - Live transaction matcher
let timestamp = Regex { ... } // proprietary let details = try Regex(inputString) let amountMatcher = /[\d.]+/ // CREDIT <proprietary> <redacted> 200.23 A1B34EFF ... let fieldSeparator = /\s{2,}|\t/ let transactionMatcher = Regex { Capture { /CREDIT|DEBIT/ } fieldSeparator Capture { timestamp } fieldSeparator Capture { details } fieldSeparator // ... }
-
18:26 - Replace field separator
let field = OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any }
-
18:55 - Use TryCapture
// CREDIT <proprietary> <redacted> 200.23 A1B34EFF ... let fieldSeparator = /\s{2,}|\t/ let field = OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any } let transactionMatcher = Regex { Capture { /CREDIT|DEBIT/ } fieldSeparator TryCapture(field) { timestamp ~= $0 ? $0 : nil } fieldSeparator TryCapture(field) { details ~= $0 ? $0 : nil } fieldSeparator // ... }
-
21:45 - Fixing the scaling issues
// CREDIT <proprietary> <redacted> 200.23 A1B34EFF ... let fieldSeparator = Local { /\s{2,}|\t/ } let field = OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any } let transactionMatcher = Regex { Capture { /CREDIT|DEBIT/ } fieldSeparator TryCapture(field) { timestamp ~= $0 ? $0 : nil } fieldSeparator TryCapture(field) { details ~= $0 ? $0 : nil } fieldSeparator // ... }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。