持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
上一节回顾
通过session 110354短短的5min对于Swift Regex的简介,我们对于Swift Regex有了一个初步认识,本节通过对session 110357的剖析,让我们来邂逅Swift Regex。
在Swift处理字符串的过程中,遇到的问题
视频的一开始,引入了一个鲜活的例子,分析一段交易数据。
我们以为是一段格式统一的数据,然而得到的却是一段字符串:
基于字符串是遵守Collection协议的一种类型,我们想到了以下几种方式进行加工处理:
-
利用Collection协议中函数式编程的方法,进行处理,比如
map,filter,splite
-
利用字符串的索引进行切片处理
但是,不管是通过函数式编程的方法还是通过字符串的索引方法,它们都不尽人意,甚至写到后面,会找不到北。
根本原因是:这些方法,它们是针对字符串的每个字符元素做操作,而我们希望匹配的有效信息,它是一段又一段有一定格式规律的字符串。
用面向字符元素的方法去需找其中有用的字符串片段,虽然可以做到,但是显然不够高效。
这个时候,我们应该怎么办呢?
他山之石
其他编程语言都有通过正则表达式去匹配有效字符串的。
Apple的开发工程师当然可以通过尝试编写正则表达式来处理字符串,并且NSRegularExpression
就是Foundation框架中正则表达式应用的类。
Swift亦是通过其他编程语言的正则表达式匹配为灵感,进而开发出了Swift Regex框架。
struct Regex<
Output>
的介绍与使用
我们可以看到Regex
它是一个结构体类型,并且有一个泛型参数Output
。
Regex的3种方式创建
- 我们可以通过
/
与/
这种字面量的方式创建Regex
,这种方式对应的Output
类型为Substring
(子字符串),这种方式对于熟悉正则表达式编写的开发者非常友好。
可能大家对这个字面量创建感到熟悉又陌生,给一个例子就明白了:
/// 字面量创建数组
let array = [1, 2, 3]
/// 构造器方法创建数组,本质调用的是init<S>(_ s: S) where Element == S.Element, S : Sequence这个方法
let array = Array(1...3)
所以字面量创建Regex
,和字面量创建Array是同一个概念。
- 我们可以通过构造器方式创建
Regex
,这种方式对应的Output
类型为AnyRegexOutput
,我认为AnyRegexOutput
应该是一种擦除类型。
注意这种构造器方法的前面有try
来修饰,因为只有在运行时我们才能知道这个正则对象是否正确,这个构造器方法与NSRegularExpression
的构造器方法有异曲同工之妙,它们的构造器函数都带有throws
,即提示开发人员必须注意构造出的对象是否成功,并可能返回nil对象。
open class NSRegularExpression : NSObject, NSCopying, NSSecureCoding {
public init(pattern: String, options: NSRegularExpression.Options = []) throws
}
- 最后我们可以通过声明式的DSL语法,通过RegexBuild去构建一个创建
Regex
,这种方式对应的Output
类型也为Substring
,这种方式虽然看起来有点费代码,不过语义表达相当的好。
这个方式有点用空间换时间的味道,代码变多变长了,但是理解需要花的时间却变少了。
/// 匹配一个或者多个数字
let digits = OneOrMore(.digit)
小试牛刀
接下来,我们用字面量创建Regex
,来尝试解析这段字符串吧:
let transaction = "DEBIT 03/05/2022 Doug's Dugout Dogs"
通过/\s{2,}|\t/
这个正则进行separator
操作。
\s{2,}
表示匹配2个以及其以上任何不可见字符,\t
匹配一个制表符,而|
就是或的意思。
让我们试试新的API:
let fragments = transaction.split(separator: /\s{2,}|\t/)
可以获取这些信息:
["DEBIT", "03/05/2022", "Doug's Dugout Dogs", "$33.27"]
接着我们在separator
操作之后,然后再进行join
操作:
let normalized = transaction.split(separator: /\s{2,}|\/).joined (separator: "It")
这些信息变成了如下所示:
DEBIT»03/05/2022»Doug's Dugout Dogs»$33.27
通过replacing
方法,我们可以更简洁的获取到同样的结果:
let normalized = transaction.replacing(/\s{2,}|\t/, with: "\t")
// DEBIT»03/05/2022»Doug's Dugout Dogs»$33.27
现在我们可以看到,通过创建Regex
,我们可以更有效的去匹配想要的数据了,但是问题也随之而来。
如何new一个正则表达式,是个问题
有些人在碰到问题时,就想:“我知道,我可以使用正则表达式。”现在,他们就有了两个问题。
-Jamie "ajwz" Zawinski, 1997年8月
引用自《Python核心编程》第3版,第1章:正则表达式,章节首页语
本session同样引用了这段话。
可见,编写一个符合业务场景的正则表达式有多么的困难,/\s{2,}|\t/
只是一个比较简单的正则,但是对于一个不懂正则表达式的开发者而言,简直就是天书。
为了解决问题而生产了新的问题,这是莫比斯环,会深深的陷进去。
而Swift Regex的出现,恰恰就是要解决这些的。
Swift Regex的优势
- 通过
RegexBuild
构建的正则,言简意赅,表现力强 - 正则解析器融入到正则表达式中,可独立使用并复用
- 支持Unicode编码级别的正则匹配(其实我对Swift String理解不不够深刻,但就我对于其他编写语言的了解,对于Unicode编码的支持,Swift很有优势,并不是所有的编程语言对Unicode编码都有很好的支持)
- 可预测执行,并可控
Talk is cheap, show me the code
牛逼吹了这么多,不如实战一把。
Swift Regex再战
import RegexBuilder
let fieldSeparator = /\s{2,}|\t/
let transactionMatcher = Regex {
/CREDIT| DEBIT/
fieldSeparator
One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .qmt))
fieldSeparator
OneOrMore {
NegativeLookahead { fieldSeparator }
CharacterClass.any
}
fieldSeparator
One (.localizedCurrency(code: "USD" ).locale (Locale(identifier: "en US")))
}
我们先引入了RegexBuilder
框架,再将可以复用的/\s{2,}|\t/
的抽离出来,最后开始编写Regex
。
如果熟悉SwiftUI的同学,马上就感受到了熟悉的味道,这种DSL与SwiftUI师出同门。
就算不熟悉SwiftUI,写Flutter在构建UI的时候也会有类似的代码,这种声明式的语法结构随处可见,这里不扩展了,有兴趣的同学可以自行查阅资料了解。
Regex
的编写用过横向来编写每个匹配单元,通过纵向来区分不同的匹配单元。
在这段代码中: /CREDIT| DEBIT/
去匹配CREDIT或者DEBIT;
fieldSeparator
去匹配空格或者制表符;
One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .qmt))
去匹配日期;
OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any }
去匹配一段任何字符,即内容;
One (.localizedCurrency(code: "USD" ).locale (Locale(identifier: "en US")))
去匹配金额;
因为目前工作原因,我无法升级mac到beta版本,Xcode也是不敢升级,所以Swift Regex的API用法,无法展开讨论,见谅。
这里的NegativeLookahead { fieldSeparator }
可以进行一下说明:
最初一开始视频中去匹配内容写的是OneOrMore { CharacterClass.any }
,但是这样有一个问题,这个正则去匹配任意字符时,如果不进行限制的话,会一直匹配到Doug's Dugout Dogs
以及后面的空格,这当然和我们业务需求的不一致,所以添加了NegativeLookahead { fieldSeparator }
这段代码。
这里涉及到正则匹配一个模式:贪婪与非贪婪,也就是尽可能多的去匹配字符串或者是尽可能少的去匹配,而NegativeLookahead { fieldSeparator }
的含义,就是当发现fieldSeparator
的时候,就应该终止后续的匹配了,这样就能匹配到业务需求的内容。
有关Swift Regex贪婪与非贪婪,会在session 110358中进行更详细的说明,这里记住一个匹配规则,那就是默认的正则匹配都是贪婪的。
就这样,通过Swift Regex
解析字符串的工作总算告一段落。
使用Capture
去提取正则信息
当然,我们不仅需要去匹配到相应的字符串,同时还需要将匹配的细节内容提取出来,这时在匹配单元外面包裹Capture
就可以了,还是熟悉的语法。
这个时候,struct Regex<
Output>
中的Output
返回是一个包含多个值的元组。
一一对应这个Output
元组表达的内容就显得非常的重要,同时也是我们需要优化的一个地方,下文会开展说。
Output.0是正则匹配的整体结果字符串,而之后元组的每一个值就依次对应每一个Capture
。
对于Capture
匹配的值,Swift做了一步到位的处理,比如Output.2匹配的是时间,输出的类型就是Date,Output.4匹配的是货币,输出的类型就是Decimal。
Output元组结果的优化
如果对Python的正则匹配熟悉的话,你会发现Python
与NSRegularExpression
在返回结果的一些相似之处:
import re
line = "Cats are smarter than dogs";
searchObj = re.search( r'(.*) are (.*?) .*', line, re.M|re.I)
if searchObj:
print "searchObj.group() : ", searchObj.group()
print "searchObj.group(1) : ", searchObj.group(1)
print "searchObj.group(2) : ", searchObj.group(2)
else:
print "Nothing found!!"
输出的结果如下:
# 整体匹配,相当于Swift Regex中的Output.0
searchObj.group() : Cats are smarter than dogs
# 第一个匹配,相当于Swift Regex中的Output.1
searchObj.group(1) : Cats
# 第二个匹配,相当于Swift Regex中的Output.2
searchObj.group(2) : smarter
Python中对于结果searchObj
通过.group()
去获取整个匹配的字符串,通过group(1)
与group(2)
区分别提取相应的内容。
Python的正则匹配的返回结果可以认为是一种类数组结构。
而NSRegularExpression
中的匹配方法,亦返回是数组类型:
open func matches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> [NSTextCheckingResult]
我们会发现这样的一个应用问题,返回的数据类型是数组,那么就只能通过下标去获取对应的值,这样既不好理解,又需要小心数组越界。
而Swift Regex则是通过元组去整合Output,又有什么玄机呢?我们先来看一个简单的例子:
func getInfo() -> (Int, Double) {
return (28, 170.0)
}
let a = getInfo()
print(a.0)
print(a.1)
上面这个例子,我们完全不知道(Int, Double)这个元组返回值的意义是什么。
我们再来看一下优化后的:
func getInfo() -> (age: Int, height: Double) {
return (28, 170.0)
}
let a = getInfo()
print(a.age)
print(a.height)
通过对元组添加修饰的属性名称,我们可以更好的理解返回数据的意义!
Swift Regex可以通过代码修饰,让Regex<
Output>
返回值时,增加对Output元组属性名称的支持!!!
我们可以看看视频中对于Output
的元组优化:
在编写Regex
的过程中,添加?<date>
,?<date>
,?<currency>
这些自然语言修饰,加上在#
与#
修饰之间字符串的特性,Output的元组也拥有了属性名称——Regex<(Substring, date: Substring, middle: Substring, currency: Substring)>
。
这样一来,pickStrategy(_ currency: Substring)
方法因为有明确的入参(Regex<(Substring, date: Substring, middle: Substring, currency: Substring)>
中的currency
),调用起来也舒心多了。
我们通过currency
区分美元与英镑符号,进而返回对应的日期格式。
最后,针对匹配的日期格式,进行标准化的iso8601格式输出。
这样一来,视频中提出的:付款是美元还是英镑而导致日期书写方式的不同,如何进行正确匹配的问题迎刃而解。
最终,我们将匹配的日期信息做了统一处理,这些格式化后的数据写入数据库,对于维护庞大数据的系统自然更加友好。
优化前 | 优化后 |
---|---|
因为中文视频播到14min左右的时候就没有了,所以从下面的内容开始,我都是自己盲听与理解的。虽然,自己大概理解主讲人在说明什么,但是通过自己的理解写成文字,或多或少会有问题甚至错误,欢迎大家指正。
匹配的功能扩展
随着业务需求的变化,交易数据又又又有了新的变化!
如图所示,数据中有出现了两个字段TIMESTAMP和DETAILS,而且这两个字段可能会包含有效的字符串,抑或什么都没有,又抑或仅仅包含占位字符串,我们该怎么办呢?
-
有还是没有,无效字符串占位,这些都是问题。
-
怎么写正则去满足业务要求也是问题。
我们根据之前积累经验,先做正则的模块化:
/// 独立编写时间戳正则
let timestamp = Regex{} // proprietary
/// 独立编写detail的正则
let details = try Regex(inputString)
/// 独立编写出amount的正则
let amountMatcher = /[ld.]+/
/// 之前编写的fieldSeparator正则
let fieldSeparator = /\s{2,}|\t/
然后我们开始进行整个Regex
的编写:
// CREDIT «proprietary> <redacted> 200.23 A1B34EFF
let transactionMatcher = Regex {
Capture { /CREDIT|DEBIT/ }
fieldSeparator
Capture { timestamp }
fieldSeparator
Capture { details }
fieldSeparator
//..
}
上面这种写法对于TIMESTAMP和DETAILS有可以匹配的值时候是可行的,但是显然对于例子中这种占位符,就会出现异常。
TryCapture
我们对«proprietary>
其所在占位区域进行处理,还是记得上面我们使用过的OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any }
吗,我们可以先尝试匹配到任意的内容,然后再通过timestamp
去匹配时间戳。
于是,我们跟着这个思路继续写:
/// 独立编写出非贪婪捕获任意内容的正则
let field = OneOrMore {
NegativeLookahead { fieldSeparator }
CharacterClass.any
}
关键的一步到了,我们匹配到任意内容后,如何继续去匹配时间戳正则呢?
该TryCapture
出场了:
let transactionMatcher = Regex {
Capture { /CREDIT/ DEBIT/ }
fieldSeparator
TryCapture (field) { timestamp ~= $0 ? $0 : nil }
fieldSeparator
TryCapture (field) { details ~=$0? S0 : nil }
fieldSeparator
//..
}
我们来详细说说这一段代码TryCapture (field) { timestamp ~= $0 ? $0 : nil }
:
TryCapture (field)
表示尝试去匹配field
正则,这和我们在Swift开发中使用try?
语法类似,相当于尝试去匹配,也就是TryCapture
匹配的结果是一个可选类型。
接着我们看看{ timestamp ~= $0 ? $0 : nil }
,针对TryCapture (field)
尝试匹配的任意内容,我们进行再次匹配,timestamp ~= $0
就是表达的这个意思,注意这里的~=
是一个运算符。
~=
运算符出自Compareable协议,表示的是判断某个值在某个范围类,我们无法通过例子中的代码看出~=
是否被重载了。
但是这段代码的逻辑应该是,TryCapture (field)
尝试匹配的任意内容是否和timestamp
匹配得上,如果匹配上了,就返回这段内容,如果匹配不上,就返回nil。
同理,TryCapture (field) { details ~=$0? S0 : nil }
的含义我们可以一举反三,就不重复拆解分析了。
到此,这个新需求,在TryCapture
的努力下解决了。
但是,同时也遗留下匹配效率的问题。
我们接着往下说。
全局匹配与局部匹配
本例子中,对于let fieldSeparator = /\s{2,}|\t/
这个正则匹配,它会进行全局的匹配搜索,这种global
模式是默认的,于是乎,我们可以看见视频中,光标不停移动与返回的操作。
在例子中,这么做没有什么大问题,因为通过全局匹配,索引的转变,我们最终可以得到正确的匹配结果,但是想想:
let transactionMatcher = Regex {
Capture { /CREDIT/ DEBIT/ }
fieldSeparator
TryCapture (field) { timestamp ~= $0 ? $0 : nil }
fieldSeparator
TryCapture (field) { details ~=$0? S0 : nil }
fieldSeparator
//..
}
这段代码有3个fieldSeparator
,使用全局匹配,会影响性能与效率,我们有更优解——局部匹配!
通过局部匹配,我们在匹配过程中,会缩小匹配的范围,更快的进入下一个匹配规则。
最终的代码,我们在/\s{2,}|\t/
外面包裹了Local
字段,以表示该段Regex
为局部匹配。
let fieldSeparator = Local { /\s{2,}|\t/ }
至于,如何理解全局匹配和局部匹配,以及使用场景,我目前还在通过其他编程语言进行实践。
Swift String与Unicode
说来很惭愧,我对于本session中12分40秒到16分30秒的视频内容理解不太透彻,这期间视频穿插讲解了Swift是以以Unicode编码为基础的匹配。
正是因为Swift的字符串构建是基于Character值的集合,并且是Unicode编码,所以Swift Regex的匹配才能如此准确。
如果大家有条件的话,可以阅读一下《Swift进阶》字符串这个章节的内容,下面这些段落摘自原作者对于Swift String的一些理解与评价:
Swift在字符串实现上做出了英勇的努力,它力求尽可能做到Unicode正确。Swift中的String是Character值的集合,而Character是人类在阅读文字时所理解的单个字符,这与该字符由多少个Unicode码点组成无关。
Swift的字符串和其他所有主流编程语言中的字符串都很不同。如果你已经习惯了将字符串作为编码单元数组进行处理的话,你可能会需要一点时间来切换你的思维方式,相比于简洁,Swift中的字符串以Unicode正确性为第一优先。
我们认为Swift做出了正确的选择。其他编程语言假装Unicode文本没有那么复杂,其实这不是真相。在⻓远看来,严格的Swift字符串可以让你避免写出一些本来会出现的bug,这可以节省下很多时间。与之相比,努力去忘却整数索引所花费的时间完全不足为意。
session 110357小结
-
我们可以通过3种方式创建
Regex
,它们分别是:字面量、构造器与语法糖。每个人对正则表达式的理解程度不同,选择合适自己的方式进行创建 -
通过RegexBuild与语法糖我们快速编写
Regex
,通过横
与纵
,编写匹配单元与分组 -
使用
Capture
API,我们可以将匹配的内容输出到Regex
的Output
中,此时Output
是一个元组结果集,通过优化,我们可以让Output
元组结果集的元素拥有属性名称,让元组的每一个元素都具有含义 -
使用
TryCapture
API,通过Try,我们可以更加灵活的面对复杂的需求场景,获得有效信息 -
用全局匹配的模式去思考,同时匹配的时候谨慎的使用局部匹配,性能与效率是我们需要关心的
-
Unicode模式是Swift的String默认模式
下节预告
计划是将session 110357和110358合并在一块进行写作的,如大家所见,又又又被拆开了。
我反反复复观看了session 110357的视频,希望融会贯通,可惜自己太菜,还是无法解读所有的内容,也欢迎各位大佬指点一二。
对Python爬虫有点理解,算是对于Swift Regex的理解与写作帮上了忙。
下一节,会对session 110358进行解读——Swift Regex进阶。
谢谢大家。
参考资料
-
What's new in Foundation (WWDC2021)
-
WWDC22中文视频资源地址,只能看看字幕,感觉像机翻
- 菜鸟教程-Python正则表达式
- 菜鸟教程-正则表达式
- 书籍《Python核心编程》第3版
- 书籍《Swift进阶》