- 命令式的CSV读取/写入器。
- 声明式的CSV编码/解码器。
- 支持多种输入/输出:
String
,Data
blobs,URL
和Stream
(通常用于stdin
)。 - 支持多种字符串编码和字节顺序标记(BOM)。
- 广泛配置:分隔符,转义量,修剪策略,可编码策略,预采样等。
- 默认配置和CRLF(
\r\n
)行分隔符符合RFC4180。 - 支持多个平台,无依赖项(Swift标准库和Foundation是隐式依赖)。
用法
要使用这个库,您需要
-
SPM(Swift包管理器)。
// 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
添加到您的项目中。
您可以通过SPM或Cocoapods添加库
在需要它的文件中导入CodableCSV
。
import CodableCSV
使用此库有两种方式
- 命令式地,作为按行和按字段逐个读取/写入器的控制。
- 通过Swift的
Codable
接口进行声明式地。
命令式读取/写入器
以下类型提供了对CSV数据的如何读取/写入的命令式控制。
-
完全解析输入。
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()
逐行解析,直到返回
;或者退出范围,阅读器将清理所有使用的内存。
// 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()
-
序列
语法解析。let reader = try CSVReader(input: URL(...), configuration: ...) for row in reader { // Do something with the row: [String] }
请注意,
序列
语法(即IteratorProtocol
)不会抛出错误;因此如果CSV数据无效,之前的代码会崩溃。如果您不控制CSV数据的来源,请使用readRow()
代替。 -
encoding
(默认nil
)指定CSV文件的编码。此
String.Encoding
值指定每个底层字节的表示方式(例如,.utf8
、.utf32littleEndian
等)。如果是nil
,该库将尝试通过文件的对齐标记码(BOM)来确定文件的编码。如果文件不含BOM,假定使用.utf8
。 -
delimiters
(默认(field: ",", row: "\n")
)指定字段和行分隔符。行内的CSV字段通过字段分隔符(通常为“逗号”)分隔。CSV行通过行分隔符(通常为“换行符”)分隔。您可以指定任何Unicode标量、
String
值或为未知分隔符指定nil
。 -
escapingStrategy
(默认“”)指定用于转义字段使用的Unicode标量。如果CSV字段包含特权字符,例如字段/行分隔符,可以将字段转义。通常转义字符是双引号(即“”),通过设置此配置值,您可以改变它(例如单引号),或禁用转义功能。
-
headerStrategy
(默认.none
)表示CSV数据是否有一个标题行。CSV文件可能在开头含有可选的标题行。此配置值让您指定文件是否有标题行,或者您想让库确定它。
-
trimStrategy
(默认空集合)在每个解析的字段的开头和结尾去除指定的字符。应用了转义字段和非转义字段。此集合不能包括任何分隔符字符或转义标量。如果是这样,在初始化期间将抛出错误。
-
presample
(默认false
)表示是否在解析开始之前将CSV数据完全加载到内存中。将所有数据加载到内存中可能为小型到中等大小的文件提供更快的迭代速度,因为您消除了管理
InputStream
的开销。 -
完整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要求所有行都包含相同数量的字段。
CSVWriter
通过在尝试写入多于预期的字段数时抛出错误,或者在调用endRow()
而没有写入所有字段时填充空字段,来强制执行这一规则。 -
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)来确定文件的编码。如果文件不含BOM,假定使用.utf8
。 -
bomStrategy
(默认.convention
)表示是否会在CSV表示的开始处包含一个字节顺序标记。操作系统约定从未写入BOM,除非指定了
.utf16
、.utf32
或.unicode
字符串编码。但是,您可以指定始终写入BOM(.always
)或永远不会写入BOM(.never
)。 type
:错误组类别。failureReason
:错误原因的解释。helpAnchor
:解决问题的建议。errorUserInfo
:与触发错误的操作相关的参数。underlyingError
:可选的底层错误,它引发了操作失败(大多数情况下是nil
)。localizedDescription
:返回包含在错误中的所有信息的人类可读字符串。
CSVReader
CSVReader
从指定的输入(String
,Data
,URL
或InputStream
)解析CSV数据,并以字符串数组的形式返回CSV行。CSVReader
可以在高层使用,在这种情况下,它会解析输入;或者在低层使用,每行在请求时进行解码。
阅读器配置
CSVReader
接受以下配置属性
在初始化期间设置配置值,可以通过结构或使用便利的闭包语法传递给CSVReader
实例。
let reader = CSVReader(input: ...) {
$0.encoding = .utf8
$0.delimiters.row = "\r\n"
$0.headerStrategy = .firstLine
$0.trimStrategy = .whitespaces
}
CSVWriter
CSVWriter
将CSV信息编码到指定的目标(即String
、Data
或文件)。它可以在高级别使用,通过编码准备好的信息集合;或者在低级别使用,在这种情况下,可以逐行或逐字段写入。
编写器配置
CSVWriter
接受以下配置属性
在初始化期间设置配置值,可以通过结构体或方便的闭包语法传递给CSVWriter
实例。
let writer = CSVWriter(fileURL: ...) {
$0.delimiters.row = "\r\n"
$0.headers = ["Name", "Age", "Pet"]
$0.encoding = .utf8
$0.bomStrategy = .never
}
CSVError
由于无效的配置值、无效的CSV输入、文件流失败等原因,CodableCSV
的大多数命令式函数可能会抛出错误。所有这些抛出操作仅抛出可由do-catch
子句轻松捕获的CSVError
。
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
。错误的属性提供丰富的注释,解释了出了什么问题,并说明了如何解决这个问题。
您可以通过简单打印错误或在合适地强制转换为CSVError
或CSVError
后调用localizedDescription
属性来获取所有信息。
声明式解码器/编码器
这个库提供的编码器/解码器可以使用Swift的Codable
声明式方法来编码/解码CSV数据。
-
nilStrategy
(默认:.empty
)指示如何在 CSV 中表示nil
(值不存在)概念。 -
boolStrategy
(默认:.insensitive
)定义如何将字符串解码到Bool
值。 -
nonConformingFloatStrategy
(默认.throw
)指定如何处理非数字(例如NaN
和无限大)。 -
decimalStrategy
(默认.locale
)指示如何将字符串解码到Decimal
值。 -
dateStrategy
(默认.deferredToDate
)指定如何将字符串解码到Date
值。 -
dataStrategy
(默认.base64
)指示如何将字符串解码到Data
值。 -
bufferingStrategy
(默认.keepAll
)控制KeyedDecodingContainer
的行为。选择缓冲策略会影响解码性能以及在解码过程中的内存使用量。有关更多信息,请参阅 README 的 使用
Codable
的技巧 部分,以及Strategy.DecodingBuffer
定义。 -
nilStrategy
(默认:.empty
)指示如何在 CSV 中表示nil
(值不存在)概念。 -
boolStrategy
(默认:.deferredToString
)定义了布尔值如何编码为String
值。 -
nonConformingFloatStrategy
(默认.throw
)指定了如何处理非数字(即NaN
和无穷大)。 -
decimalStrategy
(默认.locale
)表示十进制数字如何编码为String
值。 -
dateStrategy
(默认.deferredToDate
)指定日期如何编码为String
值。 -
dataStrategy
(默认.base64
)表示数据块如何编码为String
值。 -
bufferingStrategy
(默认.keepAll
)控制了KeyedEncodingContainer
的行为。选择缓冲策略将直接影响编码性能以及过程中的内存使用量。更多详细信息,请查看此 README 的 使用《Codable》的技巧部分和《
Strategy.EncodingBuffer
定义。
CSVDecoder
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
相同,此外还包括以下值
可以在 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()
函数将创建一个 Data
块、String
或实际文件系统中的 CSV 文件。
let encoder = CSVEncoder { $0.headers = ["name", "age", "hasPet"] }
try encoder.encode(value, into: URL("~/Desktop/Students.csv"))
如果您正在处理大量 CSV 内容,建议使用直接文件编码和 .sequential
或 .assembled
缓冲策略,因为这样可以大幅减少内存使用。
编码器配置
可以通过指定配置值来调整编码过程。《CSVEncoder》接受的配置值与《CSVWriter》相同,还包括以下配置:
配置值可以在 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
的技巧
使用 Codable
使用起来相当简单,Swift 标准库中的大多数类型已经遵守了它。然而,有时很难使自定义类型符合 Codable
以符合特定的功能。
-
在初始化时,将
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)
基本采纳。
当自定义类型遵守 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数据的特定行为。
在编码/解码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格式的数据通常与扁平层次结构(例如学生列表、汽车型号列表等)一起使用。CSV实现(例如用户列表,其中每个用户都有一个使用的服务列表,每个服务都有一个用户配置值的列表)默认情况下不支持嵌套结构。
您可以在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
的配置值。有两种设置配置值的方法:
标记为.custom
的策略允许您在编码/解码过程中插入行为,而无需手动遵循init(from:)
和encode(to:)
。当设置时,它们将参考整个过程中的目标类型。例如,如果您想将空字段用单词null
标记的CSV文件编码出来(出于某种原因),您可以这样做:
let decoder = CSVDecoder()
decoder.nilStrategy = .custom({ (encoder) in
var container = encoder.singleValueContainer()
try container.encode("null")
})
类型安全的标题行。
您可以使用Swift内省工具(例如Mirror
)或显式定义一个使用符合String
原始值和CaseIterable
的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 解决方案
- CSV.swift 包括一个命令式 CSV 读取/写入和一个 延迟 行解码器,并遵循 RFC4180 标准。
- SwiftCSV 是一个经过良好测试的只解析库,它会将整个 CSV 加载到内存中(不适用于大型文件)。
- CSwiftV 是一个只解析库,它会一次性将 CSV 加载到内存中并进行解析(没有命令式读取)。
- CSVImporter 是一个支持大 CSV 文件的异步只解析库(增量加载)。
- SwiftCSVExport 以命令式方式读取/写入 CSV,同时支持 Objective-C。
- swift-csv 基于Foundation流提供了一个命令式 CSV 读取/写入器。
Swift 社区之外也有很多不错的工具。由于编写所有这些工具是一项艰巨的任务,我将只向您推荐伟大的 AwesomeCSV github 仓库。那里有很多宝藏等着你去发现。