CodableCSV 提供以下功能:
String
、Data
数据块、URL
和 Stream
(通常用于 stdin
)。\r\n
) 作为行分隔符。要使用此库,您需要
CodableCSV
添加到您的项目中。
您可以选择通过 SPM 或 Cocoapods 添加库
SPM (Swift Package Manager)。
// swift-tools-version:5.1
import PackageDescription
let package = Package(
/* Your package name, supported platforms, and generated products go here */
dependencies: [
.package(url: "https://github.com/dehesa/CodableCSV.git", from: "0.6.7")
],
targets: [
.target(name: /* Your target name here */, dependencies: ["CodableCSV"])
]
)
pod 'CodableCSV', '~> 0.6.7'
CodableCSV
。
import CodableCSV
使用此库有两种方式
Codable
接口。以下类型提供了对如何读取/写入 CSV 数据的命令式控制。
CSVReader
CSVReader
从给定的输入(String
、Data
、URL
或 InputStream
)解析 CSV 数据,并返回 CSV 行作为 String
数组。CSVReader
可以在高级别使用,在这种情况下,它会完全解析输入;也可以在低级别使用,在这种情况下,每行在请求时解码。
完整输入解析。
let data: Data = ...
let result = try CSVReader.decode(input: data)
一旦输入被完全解析,您可以选择如何访问解码后的数据
let headers: [String] = result.headers
// Access the CSV rows (i.e. raw [String] values)
let rows = result.rows
let row = result[0]
// Access the CSV record (i.e. convenience structure over a single row)
let records = result.records
let record = result[record: 0]
// Access the CSV columns through indices or header values.
let columns = result.columns
let column = result[column: 0]
let column = result[column: "Name"]
// Access fields through indices or header values.
let fieldB: String = result[row: 3, column: 2]
let fieldA: String? = result[row: 2, column: "Age"]
逐行解析。
let reader = try CSVReader(input: string) { $0.headerStrategy = .firstLine }
let rowA = try reader.readRow()
一次解析一行,直到返回 nil
;或者退出作用域,读取器将清理所有使用的内存。
// Let's assume the input is:
let string = "numA,numB,numC\n1,2,3\n4,5,6\n7,8,9"
// The headers property can be accessed at any point after initialization.
let headers: [String] = reader.headers // ["numA", "numB", "numC"]
// Keep querying rows till `nil` is received.
guard let rowB = try reader.readRow(), // ["4", "5", "6"]
let rowC = try reader.readRow() /* ["7", "8", "9"] */ else { ... }
或者,您可以使用 readRecord()
函数,该函数也返回下一行 CSV,但它将结果包装在一个方便的结构中。 只要 headerStrategy
标记为 .firstLine
,此结构允许您使用标题名称访问每个字段。
let reader = try CSVReader(input: string) { $0.headerStrategy = .firstLine }
let headers = reader.headers // ["numA", "numB", "numC"]
let recordA = try reader.readRecord()
let rowA = recordA.row // ["1", "2", "3"]
let fieldA = recordA[0] // "1"
let fieldB = recordA["numB"] // "2"
let recordB = try reader.readRecord()
Sequence
语法解析。
let reader = try CSVReader(input: URL(...), configuration: ...)
for row in reader {
// Do something with the row: [String]
}
请注意,Sequence
语法(即 IteratorProtocol
)不会抛出错误;因此,如果 CSV 数据无效,则之前的代码将崩溃。 如果您不控制 CSV 数据的来源,请改用 readRow()
。
CSVReader
接受以下配置属性
encoding
(默认 nil
) 指定 CSV 文件的编码。
此 String.Encoding
值指定如何表示每个底层字节(例如 .utf8
、.utf32littleEndian
等)。 如果为 nil
,则库将尝试通过文件的字节顺序标记来确定文件编码。 如果文件不包含 BOM,则假定为 .utf8
。
delimiters
(默认 (field: ",", row: "\n")
) 指定字段和行分隔符。
CSV 字段在行内用字段分隔符(通常是“逗号”)分隔。 CSV 行通过行分隔符(通常是“换行符”)分隔。 您可以为未知分隔符指定任何 Unicode 标量、String
值或 nil
。
escapingStrategy
(默认 "
) 指定用于转义字段的 Unicode 标量。
CSV 字段可以被转义,以防它们包含特权字符,例如字段/行分隔符。 通常,转义字符是双引号(即 "
),通过设置此配置值,您可以更改它(例如单引号)或禁用转义功能。
headerStrategy
(默认 .none
) 指示 CSV 数据是否具有标题行。
CSV 文件可能在最开始包含一个可选的标题行。 此配置值允许您指定该文件是否具有标题行,或者您是否希望库来确定它。
trimStrategy
(默认空集) 删除每个解析字段开头和结尾的给定字符。
修剪字符应用于转义和未转义字段。 该集合不能包含任何分隔符字符或转义标量。 如果是这样,则在初始化期间将抛出错误。
presample
(默认 false
) 指示 CSV 数据是否应在开始解析之前完全加载到内存中。
对于中小型文件,将所有数据加载到内存中可能会提供更快的迭代速度,因为您可以消除管理 InputStream
的开销。
配置值在初始化期间设置,并且可以通过结构或便利闭包语法传递给 CSVReader
实例
let reader = CSVReader(input: ...) {
$0.encoding = .utf8
$0.delimiters.row = "\r\n"
$0.headerStrategy = .firstLine
$0.trimStrategy = .whitespaces
}
CSVWriter
CSVWriter
将 CSV 信息编码到指定的目标(即 String
、Data
或文件)。 它可以以高级别使用,通过完全编码准备好的信息集;或者以低级别使用,在这种情况下,可以单独写入行或字段。
完整 CSV 行编码。
let input = [
["numA", "numB", "name" ],
["1" , "2" , "Marcos" ],
["4" , "5" , "Marine-Anaïs"]
]
let data = try CSVWriter.encode(rows: input)
let string = try CSVWriter.encode(rows: input, into: String.self)
try CSVWriter.encode(rows: input, into: URL("~/Desktop/Test.csv")!, append: false)
逐行编码。
let writer = try CSVWriter(fileURL: URL("~/Desktop/Test.csv")!, append: false)
for row in input {
try writer.write(row: row)
}
try writer.endEncoding()
或者,您可以直接写入内存中的缓冲区并访问其 Data
表示形式。
let writer = try CSVWriter { $0.headers = input[0] }
for row in input.dropFirst() {
try writer.write(row: row)
}
try writer.endEncoding()
let result = try writer.data()
逐字段编码。
let writer = try CSVWriter(fileURL: URL("~/Desktop/Test.csv")!, append: false)
try writer.write(row: input[0])
input[1].forEach {
try writer.write(field: field)
}
try writer.endRow()
try writer.write(fields: input[2])
try writer.endRow()
try writer.endEncoding()
CSVWriter
拥有丰富的低级别命令式 API,可让您写入一个字段、一次写入多个字段、结束一行、写入空行等。
请注意,CSV 要求所有行具有相同数量的字段。
当您尝试写入超过预期数量的字段时,或者当您调用 endRow()
但并非所有字段都已写入时,CSVWriter
会通过抛出错误或用空字段填充一行来强制执行此操作。
CSVWriter
接受以下配置属性
delimiters
(默认 (field: ",", row: "\n")
) 指定字段和行分隔符。
CSV 字段在行内用字段分隔符(通常是“逗号”)分隔。 CSV 行通过行分隔符(通常是“换行符”)分隔。 您可以为未知分隔符指定任何 Unicode 标量、String
值或 nil
。
escapingStrategy
(默认 .doubleQuote
) 指定用于转义字段的 Unicode 标量。
CSV 字段可以被转义,以防它们包含特权字符,例如字段/行分隔符。 通常,转义字符是双引号(即 "
),通过设置此配置值,您可以更改它(例如单引号)或禁用转义功能。
headers
(默认 []
) 指示 CSV 数据是否具有标题行。
CSV 文件可能在最开始包含一个可选的标题行。 如果此配置值为空,则不写入标题行。
encoding
(默认 nil
) 指定 CSV 文件的编码。
此 String.Encoding
值指定如何表示每个底层字节(例如 .utf8
、.utf32littleEndian
等)。 如果为 nil
,则库将尝试通过文件的字节顺序标记来确定文件编码。 如果文件不包含 BOM,则假定为 .utf8
。
bomStrategy
(默认 .convention
) 指示是否将在 CSV 表示形式的开头包含字节顺序标记。
操作系统惯例是永远不写入 BOM,除非指定了 .utf16
、.utf32
或 .unicode
字符串编码。 但是,您可以指示您始终希望写入 BOM (.always
) 或者永远不写入 (.never
)。
配置值在初始化期间设置,并且可以通过结构或便利闭包语法传递给 CSVWriter
实例
let writer = CSVWriter(fileURL: ...) {
$0.delimiters.row = "\r\n"
$0.headers = ["Name", "Age", "Pet"]
$0.encoding = .utf8
$0.bomStrategy = .never
}
CSVError
由于无效的配置值、无效的 CSV 输入、文件流失败等,CodableCSV
的许多命令式函数可能会抛出错误。 所有这些抛出操作都专门抛出 CSVError
,可以使用 do
-catch
子句轻松捕获。
do {
let writer = try CSVWriter()
for row in customData {
try writer.write(row: row)
}
} catch let error {
print(error)
}
CSVError
采用了 Swift Evolution 的 SE-112 协议 和 CustomDebugStringConvertible
。 错误的属性提供了丰富的注释,解释了出了什么问题并指示如何解决该问题。
type
:错误组类别。failureReason
:对出错原因的解释。helpAnchor
:关于如何解决问题的建议。errorUserInfo
:与抛出错误的操作相关的参数。underlyingError
:可选的底层错误,导致操作失败(大多数情况下为 nil
)。localizedDescription
:返回一个人类可读的字符串,其中包含错误中包含的所有信息。
您可以通过简单地打印错误或在正确转换的 CSVError<CSVReader>
或 CSVError<CSVWriter>
上调用 localizedDescription
属性来获取所有信息。
此库提供的编码器/解码器使您可以使用 Swift 的 Codable
声明式方法来编码/解码 CSV 数据。
CSVDecoder
CSVDecoder
将 CSV 数据转换为符合 Decodable
的 Swift 类型。 解码过程非常简单,只需要创建一个解码实例并调用其 decode
函数,传递 Decodable
类型和输入数据。
let decoder = CSVDecoder()
let result = try decoder.decode(CustomType.self, from: data)
CSVDecoder
可以解码表示为 Data
数据块、String
、文件系统中实际文件或 InputStream
(例如 stdin
)的 CSV。
let decoder = CSVDecoder { $0.bufferingStrategy = .sequential }
let content = try decoder.decode([Student].self, from: URL("~/Desktop/Student.csv"))
如果您正在处理大型 CSV 文件,最好使用直接文件解码、.sequential
或 .unrequested
缓冲策略,并将预采样设置为 false;因为这样可以大大减少内存使用量。
可以通过在初始化时指定配置值来调整解码过程。 CSVDecoder
接受与 CSVReader
相同的配置值,以及以下配置值
nilStrategy
(默认: .empty
) 指示 nil
概念(缺少值)在 CSV 中的表示方式。
boolStrategy
(默认: .insensitive
) 定义如何将字符串解码为 Bool
值。
nonConformingFloatStrategy
(默认 .throw
) 指定如何处理非数字(例如 NaN
和无穷大)。
decimalStrategy
(默认 .locale
) 指示如何将字符串解码为 Decimal
值。
dateStrategy
(默认 .deferredToDate
) 指定如何将字符串解码为 Date
值。
dataStrategy
(默认 .base64
) 指示如何将字符串解码为 Data
值。
bufferingStrategy
(默认 .keepAll
) 控制 KeyedDecodingContainer
的行为。
选择缓冲策略会影响解码性能以及解码过程中使用的内存量。有关更多信息,请查看 README 的 使用 Codable
的技巧 部分和 Strategy.DecodingBuffer
定义。
配置值可以在 CSVDecoder
初始化期间设置,也可以在调用 decode
函数之前的任何时候设置。
let decoder = CSVDecoder {
$0.encoding = .utf8
$0.delimiters.field = "\t"
$0.headerStrategy = .firstLine
$0.bufferingStrategy = .keepAll
$0.decimalStrategy = .custom({ (decoder) in
let value = try Float(from: decoder)
return Decimal(value)
})
}
CSVDecoder.Lazy
可以使用解码器的 lazy(from:)
函数按需(即逐行)解码 CSV 输入。
let decoder = CSVDecoder(configuration: config).lazy(from: fileURL)
let student1 = try decoder.decodeRow(Student.self)
let student2 = try decoder.decodeRow(Student.self)
CSVDecoder.Lazy
遵循 Swift 的 Sequence
协议,允许您使用诸如 map()
、allSatisfy()
等功能。请注意,CSVDecoder.Lazy
不能用于重复访问;它会消耗输入的 CSV 数据。
let decoder = CSVDecoder().lazy(from: fileData)
let students = try decoder.map { try $0.decode(Student.self) }
使用惰性操作的一个好处是,它允许您在任何时候切换行的解码方式。 例如
let decoder = CSVDecoder().lazy(from: fileString)
// The first 100 rows are students.
let students = ( 0..<100).map { _ in try decoder.decode(Student.self) }
// The second 100 rows are teachers.
let teachers = (100..<110).map { _ in try decoder.decode(Teacher.self) }
由于 CSVDecoder.Lazy
专门提供顺序访问,因此将缓冲策略设置为 .sequential
将减少解码器的内存使用量。
let decoder = CSVDecoder {
$0.headerStrategy = .firstLine
$0.bufferingStrategy = .sequential
}.lazy(from: fileURL)
CSVEncoder
CSVEncoder
将符合 Encodable
协议的 Swift 类型转换为 CSV 数据。 编码过程非常简单,只需要创建一个编码实例并调用其 encode
函数,传入 Encodable
值即可。
let encoder = CSVEncoder()
let data = try encoder.encode(value, into: Data.self)
Encoder
的 encode()
函数创建一个 CSV 文件,作为 Data
blob、String
或文件系统中的实际文件。
let encoder = CSVEncoder { $0.headers = ["name", "age", "hasPet"] }
try encoder.encode(value, into: URL("~/Desktop/Students.csv"))
如果您正在处理大型 CSV 内容,则最好使用直接文件编码和 .sequential
或 .assembled
缓冲策略,因为这样可以大大减少内存使用量。
可以通过指定配置值来调整编码过程。CSVEncoder
接受与 CSVWriter
相同的配置值,以及以下值:
nilStrategy
(默认: .empty
) 指示 nil
概念(缺少值)在 CSV 中的表示方式。
boolStrategy
(默认值:.deferredToString
)定义如何将布尔值编码为 String
值。
nonConformingFloatStrategy
(默认值 .throw
)指定如何处理非数字(即 NaN
和无穷大)。
decimalStrategy
(默认值 .locale
)指示如何将十进制数编码为 String
值。
dateStrategy
(默认值 .deferredToDate
)指定如何将日期编码为 String
值。
dataStrategy
(默认值 .base64
)指示如何将数据 blobs 编码为 String
值。
bufferingStrategy
(默认值 .keepAll
)控制 KeyedEncodingContainer
的行为。
选择缓冲策略会直接影响编码性能以及该过程中使用的内存量。有关更多信息,请查看 README 的 使用 Codable
的技巧 部分和 Strategy.EncodingBuffer
定义。
配置值可以在 CSVEncoder
初始化期间设置,也可以在调用 encode
函数之前的任何时候设置。
let encoder = CSVEncoder {
$0.headers = ["name", "age", "hasPet"]
$0.delimiters = (field: ";", row: "\r\n")
$0.dateStrategy = .iso8601
$0.bufferingStrategy = .sequential
$0.floatStrategy = .convert(positiveInfinity: "∞", negativeInfinity: "-∞", nan: "≁")
$0.dataStrategy = .custom({ (data, encoder) in
let string = customTransformation(data)
var container = try encoder.singleValueContainer()
try container.encode(string)
})
}
如果您正在使用键控编码容器,则需要
.headers
配置。
CSVEncoder.Lazy
可以使用编码器的 lazy(into:)
函数按需编码一系列可编码类型(表示 CSV 行)。
let encoder = CSVEncoder().lazy(into: Data.self)
for student in students {
try encoder.encodeRow(student)
}
let data = try encoder.endEncoding()
一旦没有更多值需要编码,请调用 endEncoding()
。 该函数将返回编码后的 CSV。
let encoder = CSVEncoder().lazy(into: String.self)
students.forEach {
try encoder.encode($0)
}
let string = try encoder.endEncoding()
使用惰性操作的一个好处是,它允许您在任何时候切换行的编码方式。 例如
let encoder = CSVEncoder(configuration: config).lazy(into: fileURL)
students.forEach { try encoder.encode($0) }
teachers.forEach { try encoder.encode($0) }
try encoder.endEncoding()
由于 CSVEncoder.Lazy
专门提供顺序编码,因此将缓冲策略设置为 .sequential
将减少编码器的内存使用量。
let encoder = CSVEncoder {
$0.bufferingStrategy = .sequential
}.lazy(into: String.self)
Codable
非常易于使用,并且大多数 Swift 标准库类型已经符合它。 但是,有时很难让自定义类型符合 Codable
以实现特定功能。
当自定义类型符合 Codable
时,该类型表明它能够从外部表示解码自身并编码自身到外部表示。 哪种表示形式取决于选择的解码器或编码器。 Foundation 提供了对 JSON 和属性列表 的支持,社区提供了许多其他格式,例如:YAML、XML、BSON 和 CSV(通过此库)。
通常,CSV 表示一个长的实体列表。 以下是一个表示学生列表的简单示例。
let string = """
name,age,hasPet
John,22,true
Marine,23,false
Alta,24,true
"""
一个学生可以表示为一个结构体
struct Student: Codable {
var name: String
var age: Int
var hasPet: Bool
}
要解码学生列表,请创建一个解码器并在其上调用 decode
,传入 CSV 示例。
let decoder = CSVDecoder { $0.headerStrategy = .firstLine }
let students = try decoder.decode([Student].self, from: string)
反向过程(从 Swift 到 CSV)非常相似(且简单)。
let encoder = CSVEncoder { $0.headers = ["name", "age", "hasPet"] }
let newData = try encoder.encode(students)
在编码/解码 CSV 数据时,务必牢记以下几点:
Codable
的自动合成需要带有标题行的 CSV 文件。
当您的自定义类型的所有成员/属性都符合 Codable
时,Codable
能够为您的自定义类型合成 init(from:)
和 encode(to:)
。 此自动合成创建了一个隐藏的 CodingKeys
枚举,其中包含您的所有属性名称。
在解码期间,CSVDecoder
尝试将枚举字符串值与行中的字段位置进行匹配。 为此,CSV 数据必须包含带有属性名称的标题行。 如果您的 CSV 不包含标题行,您可以指定带有表示字段索引的整数值的编码键。
struct Student: Codable {
var name: String
var age: Int
var hasPet: Bool
private enum CodingKeys: Int, CodingKey {
case name = 0
case age = 1
case hasPet = 2
}
}
使用整数编码键的另一个好处是提高编码器/解码器的性能。 通过显式指示字段索引,您可以让解码器跳过将编码键字符串值与标题匹配的功能。
CSV 格式的数据通常用于扁平层次结构(例如,学生列表、汽车型号列表等)。 CSV 实现默认不支持嵌套结构,例如 JSON 文件中找到的那些(例如,用户列表,其中每个用户都有一个她使用的服务列表,并且每个服务都有一个用户配置值列表)。
您可以在 CSV 中支持复杂的结构,但您必须将层次结构展平为单个模型或构建自定义编码/解码过程。 此过程将确保始终最多有两个键控/未键控容器。
例如,我们可以为拥有宠物的学生创建一所学校的嵌套结构。
struct School: Codable {
let students: [Student]
}
struct Student: Codable {
var name: String
var age: Int
var pet: Pet
}
struct Pet: Codable {
var nickname: String
var gender: Gender
enum Gender: Codable {
case male, female
}
}
默认情况下,前面的示例将不起作用。 如果要保留嵌套结构,则需要覆盖自定义的 init(from:)
实现(以支持 Decodable
)。
extension School {
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
self.student.append(try container.decode(Student.self))
}
}
}
extension Student {
init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CustomKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.pet = try decoder.singleValueContainer.decode(Pet.self)
}
}
extension Pet {
init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CustomKeys.self)
self.nickname = try container.decode(String.self, forKey: .nickname)
self.gender = try container.decode(Gender.self, forKey: .gender)
}
}
extension Pet.Gender {
init(from decoder: Decoder) throws {
var container = try decoder.singleValueContainer()
self = try container.decode(Int.self) == 1 ? .male : .female
}
}
private CustomKeys: Int, CodingKey {
case name = 0
case age = 1
case nickname = 2
case gender = 3
}
您可以通过定义如下所示的扁平结构来避免构建初始值设定项的开销
struct Student: Codable {
var name: String
var age: Int
var nickname: String
var gender: Gender
enum Gender: Int, Codable {
case male = 1
case female = 2
}
}
SE167 提案引入了 Foundation JSON 和 PLIST 编码器/解码器。 该提案还引入了编码/解码策略,作为配置编码/解码过程的新方法。 CodableCSV
继续了这一传统并镜像了此类策略,包括一些特定于 CSV 文件格式的新策略。
要配置编码/解码过程,您需要在调用 encode()
/decode()
函数之前设置 CSVEncoder
/CSVDecoder
的配置值。 有两种方法可以设置配置值
在初始化时,将 Configuration
结构体传递给初始化器。
var config = CSVDecoder.Configuration()
config.nilStrategy = .empty
config.decimalStrategy = .locale(.current)
config.dataStrategy = .base64
config.bufferingStrategy = .sequential
config.trimStrategy = .whitespaces
config.encoding = .utf16
config.delimiters.row = "\r\n"
let decoder = CSVDecoder(configuration: config)
或者,有便捷的初始化器接受带有 inout Configuration
值的闭包。
let decoder = CSVDecoder {
$0.nilStrategy = .empty
$0.decimalStrategy = .locale(.current)
// and so on and so forth
}
CSVEncoder
和 CSVDecoder
专门为它们的配置值实现 @dynamicMemberLookup
。 因此,您可以在初始化后或在执行编码/解码过程后设置配置值。
let decoder = CSVDecoder()
decoder.bufferingStrategy = .sequential
decoder.decode([Student].self, from: url1)
decoder.bufferingStrategy = .keepAll
decoder.decode([Pets].self, from: url2)
标有 .custom
的策略允许您将行为插入到编码/解码过程中,而无需强制您手动符合 init(from:)
和 encode(to:)
。 设置后,它们将在整个过程中引用目标类型。 例如,如果您想编码一个 CSV 文件,其中空字段用单词 null
标记(由于某种原因)。 您可以这样做
let decoder = CSVDecoder()
decoder.nilStrategy = .custom({ (encoder) in
var container = encoder.singleValueContainer()
try container.encode("null")
})
您可以使用 Swift 自省工具(即 Mirror
)或显式定义具有符合 CaseIterable
的 String
原始值的 CodingKey
枚举来生成类型安全的名称标题。
struct Student {
var name: String
var age: Int
var hasPet: Bool
enum CodingKeys: String, CodingKey, CaseIterable {
case name, age, hasPet
}
}
然后使用显式标题配置您的编码器。
let encoder = CSVEncoder {
$0.headers = Student.CodingKeys.allCases.map { $0.rawValue }
}
#warning("TODO:")
该库已被大量记录,欢迎任何贡献。 查看简短的 如何贡献 文档,或者查看 Github 项目 以获取更深入的路线图。
如果 CodableCSV
不符合您的喜好,Swift 社区提供了其他 CSV 解决方案
在 Swift 社区之外有很多不错的工具。 由于编写所有这些工具将是一项艰巨的任务,因此我只会将您指向出色的 AwesomeCSV github 仓库。 在那里可以找到很多宝藏。