一个用于将非结构化数据转换为结构化数据的库,专注于组合、性能、通用性和可逆性
组合:将大型、复杂的解析问题分解为更小、更简单问题的能力。以及将小型、简单解析器轻松组合成更大、更复杂解析器的能力。
性能:由许多较小部分组成的解析器应具有与高度优化、手工编写的解析器一样出色的性能。
通用性:将任何类型的输入解析为任何类型的输出的能力。这允许您根据所需的性能或希望保证的正确性程度来选择要处理的抽象级别。例如,您可以在 UTF-8 代码单元集合上编写高度优化的解析器,并且它可以自动插入到字符串、数组、不安全缓冲区指针等解析器中。
可逆性:反转解析器使其成为打印器的能力。这允许您将结构良好的数据转换回非结构化数据,这对于序列化、通过网络发送数据、URL 路由等非常有用。
这个库是在 Point-Free 上的许多剧集中设计的,Point-Free 是一个探索函数式编程和 Swift 语言的视频系列,由 Brandon Williams 和 Stephen Celis 主持。 您可以在这里观看所有剧集。
解析是编程中一个非常普遍的问题。我们可以将解析定义为尝试将非结构化数据转换为结构化数据。Swift 标准库附带了许多我们每天都在使用的解析器。例如,Int
、Double
甚至 Bool
上都有初始化器,它们尝试从字符串中解析数字和布尔值
Int("42") // 42
Int("Hello") // nil
Double("123.45") // 123.45
Double("Goodbye") // nil
Bool("true") // true
Bool("0") // nil
还有像 JSONDecoder
和 PropertyListDecoder
这样的类型,它们尝试从数据中解析符合 Decodable
协议的类型
try JSONDecoder().decode(User.self, from: data)
try PropertyListDecoder().decode(Settings.self, from: data)
虽然解析器在 Swift 中无处不在,但 Swift 没有关于解析的整体方案。相反,我们通常以特别的方式使用许多不相关的初始化器、方法和其他方式来解析数据。这通常会导致代码的可维护性和可重用性降低。
这个库旨在为 Swift 中的解析编写这样一个方案。它引入了一个单一的解析单元,可以以有趣的方式组合,形成大型、复杂的解析器,以可维护的方式解决您需要解决的编程问题。
假设您有一个字符串,其中包含一些您想要解析为 User
数组的用户数据
var input = """
1,Blob,true
2,Blob Jr.,false
3,Blob Sr.,true
"""
struct User {
var id: Int
var name: String
var isAdmin: Bool
}
一种朴素的方法是嵌套使用 .split(separator:)
,然后再做一些额外的工作将字符串转换为整数和布尔值
let users = input
.split(separator: "\n")
.compactMap { row -> User? in
let fields = row.split(separator: ",")
guard
fields.count == 3,
let id = Int(fields[0]),
let isAdmin = Bool(String(fields[2]))
else { return nil }
return User(id: id, name: String(fields[1]), isAdmin: isAdmin)
}
这段代码不仅有点混乱,而且效率低下,因为我们为 .split
分配了数组,然后立即丢弃这些值。
更直接和高效的方法是描述如何从输入的开头消耗位,并将其转换为用户。这正是这个解析器库擅长的事情 😄。
我们可以从描述解析单行意味着什么开始,首先从字符串的前面解析一个整数,然后解析一个逗号。我们可以通过使用 Parse
类型来做到这一点,Parse
类型充当描述您想要依次运行以从输入中消耗的解析器列表的入口点
let user = Parse(input: Substring.self) {
Int.parser()
","
}
请注意,这个解析库非常通用,允许将任何类型的输入解析为任何类型的输出。因此,我们有时需要指定解析器可以处理的确切输入类型,在本例中为子字符串。
这已经可以消耗输入的开头
try user.parse("1,") // 1
接下来,我们想要获取用户姓名之前的所有内容,直到下一个逗号,然后消耗逗号
let user = Parse(input: Substring.self) {
Int.parser()
","
Prefix { $0 != "," }
","
}
然后我们想要获取行尾的布尔值,表示用户的管理员状态
let user = Parse(input: Substring.self) {
Int.parser()
","
Prefix { $0 != "," }
","
Bool.parser()
}
目前,这将从输入中解析一个元组 (Int, Substring, Bool)
,我们可以对其进行 .map
以将其转换为 User
let user = Parse(input: Substring.self) {
Int.parser()
","
Prefix { $0 != "," }
","
Bool.parser()
}
.map { User(id: $0, name: String($1), isAdmin: $2) }
为了使我们正在解析的数据更加突出,我们可以将转换闭包作为 Parse
的第一个参数传递
let user = Parse(input: Substring.self) {
User(id: $0, name: String($1), isAdmin: $2)
} with: {
Int.parser()
","
Prefix { $0 != "," }
","
Bool.parser()
}
或者,我们可以通过首先将 Prefix
解析器的输出从 Substring
转换为 String
,以 point-free 风格将 User
初始化器传递给 Parse
let user = Parse(input: Substring.self, User.init(id:name:isAdmin:)) {
Int.parser()
","
Prefix { $0 != "," }.map(String.init)
","
Bool.parser()
}
这足以从输入字符串中解析单个用户
try user.parse("1,Blob,true")
// User(id: 1, name: "Blob", isAdmin: true)
要从输入中解析多个用户,我们可以使用 Many
解析器多次运行用户解析器
let users = Many {
user
} separator: {
"\n"
}
try users.parse(input)
// [User(id: 1, name: "Blob", isAdmin: true), ...]
现在,此解析器可以处理整个用户文档,并且代码比使用 .split
和 .compactMap
的版本更简单、更直接。
更棒的是,它性能更高。我们为这两种解析风格编写了基准测试,.split
风格的解析速度慢两倍以上
name time std iterations
------------------------------------------------------------------
README Example.Parser: Substring 3426.000 ns ± 63.40 % 385395
README Example.Ad hoc 7631.000 ns ± 47.01 % 169332
Program ended with exit code: 0
此外,如果您愿意针对 UTF8View
而不是 Substring
编写解析器,则可以获得更高的性能,速度提高一倍以上
name time std iterations
------------------------------------------------------------------
README Example.Parser: Substring 3693.000 ns ± 81.76 % 349763
README Example.Parser: UTF8 1272.000 ns ± 128.16 % 999150
README Example.Ad hoc 8504.000 ns ± 59.59 % 151417
我们还可以将这些时间与 Apple Foundation 提供给我们的工具进行比较:Scanner
。它是一种允许您从字符串开头消耗以生成值的类型,并提供比使用 .split
更友好的 API
var users: [User] = []
while scanner.currentIndex != input.endIndex {
guard
let id = scanner.scanInt(),
let _ = scanner.scanString(","),
let name = scanner.scanUpToString(","),
let _ = scanner.scanString(","),
let isAdmin = scanner.scanBool()
else { break }
users.append(User(id: id, name: name, isAdmin: isAdmin))
_ = scanner.scanString("\n")
}
但是,Scanner
风格的解析速度比上面编写的子字符串解析器慢 5 倍以上,比 UTF-8 解析器慢 15 倍以上
name time std iterations
-------------------------------------------------------------------
README Example.Parser: Substring 3481.000 ns ± 65.04 % 376525
README Example.Parser: UTF8 1207.000 ns ± 110.96 % 1000000
README Example.Ad hoc 8029.000 ns ± 44.44 % 163719
README Example.Scanner 19786.000 ns ± 35.26 % 62125
我们可以更进一步。只需做一个小小的改动,我们就可以将解析器变成打印器。
-let user = Parse(User.init(id:name:isAdmin:)) {
+let user = ParsePrint(.memberwise(User.init(id:name:isAdmin:))) {
Int.parser()
","
Prefix { $0 != "," }.map(String.init)
","
Bool.parser()
}
let users = Many {
user
} separator: {
"\n"
}
通过这一处改动,我们现在可以将用户数组打印回字符串
users.print([
User(id: 1, name: "Blob", isAdmin: true),
User(id: 2, name: "Blob Jr.", isAdmin: false),
User(id: 3, name: "Blob Sr.", isAdmin: true),
])
// 1,Blob,true
// 2,Blob Jr.,false
// 3,Blob Sr.,true
这就是解析和打印简单字符串格式的基础知识,但是还有更多的运算符和技巧需要学习,以便高效地解析更大的输入。阅读文档以更深入地了解解析器-打印器的概念,并查看基准测试以获取更多真实解析场景的示例。
该库附带一个基准测试可执行文件,它不仅演示了库的性能,还提供了各种解析示例
这些是我们目前运行基准测试时获得的时间
MacBook Pro (16-inch, 2021)
Apple M1 Pro (10 cores, 8 performance and 2 efficiency)
32 GB (LPDDR5)
name time std iterations
----------------------------------------------------------------------------------
Arithmetic.Parser 6166.000 ns ± 10.73 % 228888
BinaryData.Parser 208.000 ns ± 39.64 % 1000000
Bool.Bool.init 41.000 ns ± 84.71 % 1000000
Bool.Bool.parser 42.000 ns ± 87.86 % 1000000
Bool.Scanner.scanBool 916.000 ns ± 30.55 % 1000000
Color.Parser 208.000 ns ± 28.34 % 1000000
CSV.Parser 3675250.000 ns ± 1.16 % 380
CSV.Ad hoc mutating methods 651333.000 ns ± 1.00 % 2143
Date.Parser 3500.000 ns ± 5.65 % 238924
Date.DateFormatter 23542.000 ns ± 5.50 % 58766
Date.ISO8601DateFormatter 29041.000 ns ± 3.31 % 48028
HTTP.HTTP 10250.000 ns ± 6.24 % 135657
JSON.Parser 38167.000 ns ± 3.26 % 36423
JSON.JSONSerialization 1792.000 ns ± 54.14 % 753770
Numerics.Int.init 0.000 ns ± inf % 1000000
Numerics.Int.parser 83.000 ns ± 67.28 % 1000000
Numerics.Scanner.scanInt 125.000 ns ± 38.65 % 1000000
Numerics.Digits 83.000 ns ± 65.03 % 1000000
Numerics.Comma separated: Int.parser 15364583.000 ns ± 0.63 % 91
Numerics.Comma separated: Scanner.scanInt 50654458.500 ns ± 0.30 % 28
Numerics.Comma separated: String.split 15452542.000 ns ± 1.30 % 90
Numerics.Double.init 42.000 ns ± 152.57 % 1000000
Numerics.Double.parser 166.000 ns ± 45.23 % 1000000
Numerics.Scanner.scanDouble 167.000 ns ± 42.36 % 1000000
Numerics.Comma separated: Double.parser 18539833.000 ns ± 0.57 % 75
Numerics.Comma separated: Scanner.scanDouble 55239167.000 ns ± 0.46 % 25
Numerics.Comma separated: String.split 17636000.000 ns ± 1.34 % 78
PrefixUpTo.Parser: Substring 182041.000 ns ± 1.78 % 7643
PrefixUpTo.Parser: UTF8 40417.000 ns ± 2.71 % 34379
PrefixUpTo.String.range(of:) 49792.000 ns ± 2.70 % 27891
PrefixUpTo.Scanner.scanUpToString 53959.000 ns ± 3.87 % 25745
Race.Parser 59583.000 ns ± 2.78 % 23333
README Example.Parser: Substring 2834.000 ns ± 12.87 % 488264
README Example.Parser: UTF8 1291.000 ns ± 22.65 % 1000000
README Example.Ad hoc 2459.000 ns ± 20.61 % 561930
README Example.Scanner 12084.000 ns ± 5.53 % 115388
String Abstractions.Substring 472083.500 ns ± 1.38 % 2962
String Abstractions.UTF8 196041.000 ns ± 3.38 % 7059
UUID.UUID.init 208.000 ns ± 43.60 % 1000000
UUID.UUID.parser 167.000 ns ± 42.00 % 1000000
Xcode Logs.Parser 4511625.500 ns ± 0.58 % 226
发行版和 main 分支的文档可在此处获得
如果您想讨论这个库,或者对如何使用它来解决特定问题有疑问,您可以在许多地方与 Point-Free 爱好者们进行讨论
Swift 社区中还有一些其他解析库,您可能也会感兴趣
此库中的打印功能灵感来自 Tillmann Rendel 和 Klaus Ostermann 的论文 “可逆语法描述:统一解析和美观打印”。
本库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。