使用 Groovy 读取和写入 CSV 文件
作者:Paul King
发布时间:2022-07-25 02:26PM
简介
在这篇文章中,我们将介绍如何使用 Groovy 读取和写入 CSV 文件。
CSV 文件不就只是文本文件吗?
对于简单的用例,我们可以像对待其他文本文件一样对待 CSV 文件。假设我们有以下数据,我们希望将其写入 CSV 文件。
def data = [
['place', 'firstname', 'lastname', 'team'],
['1', 'Lorena', 'Wiebes', 'Team DSM'],
['2', 'Marianne', 'Vos', 'Team Jumbo Visma'],
['3', 'Lotte', 'Kopecky', 'Team SD Worx']
]
Groovy 使用与 Java 相似的 File
或 Path
对象。我们将在这里使用 File
对象,并且为了我们的目的,我们将只使用一个临时文件,因为我们只需要读取它并将其与我们的数据进行比较。以下是如何创建临时文件
def file = File.createTempFile('FemmesStage1Podium', '.csv')
写入我们的 CSV(在这个简单的例子中)就像用逗号连接数据,用行分隔符连接行一样简单
file.text = data*.join(',').join(System.lineSeparator())
我们一次性写入了整个文件内容,但也可以选择一次写入一行、一个字符或一个字节。
读取数据同样简单。我们读取行,并以逗号为分隔符进行分割
assert file.readLines()*.split(',') == data
一般来说,我们可能想进一步处理数据。Groovy 也提供了不错的选择。假设我们有以下现有的 CSV 文件
我们可以读取文件,并使用以下代码选择我们感兴趣的各种列
def file = new File('HommesStageWinners.csv')
def rows = file.readLines().tail()*.split(',')
int total = rows.size()
Set names = rows.collect { it[1] + ' ' + it[2] }
Set teams = rows*.getAt(3)
Set countries = rows*.getAt(4)
String result = "Across $total stages, ${names.size()} riders from " +
"${teams.size()} teams and ${countries.size()} countries won stages."
assert result == 'Across 21 stages, 15 riders from 10 teams and 9 countries won stages.'
这里,tail()
方法跳过了标题行。第 0 列包含阶段号,我们忽略它。第 1 列包含名字,第 2 列包含姓氏,第 3 列包含车队,第 4 列包含车手的国籍。我们将全名、车队和国籍存储在集合中,以消除重复项。然后,我们使用这些集合的大小创建一个总体结果消息。
虽然在这个简单的例子中,编码相当简单,但并不建议以这种方式手动处理 CSV 文件。CSV 的细节很快就会变得混乱。如果值本身包含逗号或换行符怎么办?也许我们可以用双引号包围它们,但如果值包含双引号怎么办?等等。因此,建议使用 CSV 库。
我们将简要介绍三个库,但首先,让我们通过查看多个获胜者来总结环法自行车赛的一些亮点。以下代码总结了我们的 CSV 数据
def byValueDesc = { -it.value }
def bySize = { k, v -> [k, v.size()] }
def isMultiple = { it.value > 1 }
def multipleWins = { Closure select -> rows
.groupBy(select)
.collectEntries(bySize)
.findAll(isMultiple)
.sort(byValueDesc)
.entrySet()
.join(', ')
}
println 'Multiple wins by country:\n' + multipleWins{ it[4] }
println 'Multiple wins by rider:\n' + multipleWins{ it[1] + ' ' + it[2] }
println 'Multiple wins by team:\n' + multipleWins{ it[3] }
这个总结与 CSV 文件本身无关,但为了纪念环法自行车赛中精彩的骑行而总结!以下是输出结果
好了,现在让我们看看我们的三个 CSV 库。
Commons CSV
Apache Commons CSV 库使写入和解析 CSV 文件变得更容易。以下是使用 CSVPrinter
类写入我们的 CSV 的代码
file.withWriter { w ->
new CSVPrinter(w, CSVFormat.DEFAULT).printRecords(data)
}
以下是使用 RFC4180
解析器工厂单例读取它的代码
file.withReader { r ->
assert RFC4180.parse(r).records*.toList() == data
}
还有其他单例工厂用于制表符分隔值和其他常见格式,以及构建器,允许您设置各种选项,如转义字符、引号选项、是否使用枚举定义标题名称,以及是否忽略空行或空值。
对于我们更复杂的例子,我们需要做一些额外的工作。我们将使用构建器告诉解析器跳过标题行。我们也可以选择使用之前用过的 tail()
技巧,但我们决定使用解析器功能。代码如下
file.withReader { r ->
def rows = RFC4180.builder()
.setHeader()
.setSkipHeaderRecord(true)
.build()
.parse(r)
.records
assert rows.size() == 21
assert rows.collect { it.firstname + ' ' + it.lastname }.toSet().size() == 15
assert rows*.team.toSet().size() == 10
assert rows*.country.toSet().size() == 9
}
您可以看到,我们在处理过程中使用了列名称而不是列号。使用列名称是使用 CSV 库的另一个优点;手动执行这方面的工作会相当繁琐。另外请注意,为了简便起见,我们没有像之前的例子那样创建完整的 result 消息。相反,我们只是检查了之前计算的所有相关集合的大小。
OpenCSV
OpenCSV 库在需要时会处理混乱的 CSV 细节,但在简单的情况下不会造成阻碍。对于我们的第一个例子,CSVReader
和 CSVWriter
类将很适合。以下是与之前相同的代码,用于写入我们的 CSV 文件
file.withWriter { w ->
new CSVWriter(w).writeAll(data.collect{ it as String[] })
}
以下是读取数据的代码
file.withReader { r ->
assert new CSVReader(r).readAll() == data
}
如果我们查看生成的 文件,它比之前的 文件要更复杂一些,因为所有数据都用双引号括起来
如果我们想进行更复杂的处理,CSVReaderHeaderAware
类将识别初始标题行及其列名称。以下是我们更复杂的示例,它进一步处理了一些数据
file.withReader { r ->
def rows = []
def reader = new CSVReaderHeaderAware(r)
while ((next = reader.readMap())) rows << next
assert rows.size() == 21
assert rows.collect { it.firstname + ' ' + it.lastname }.toSet().size() == 15
assert rows*.team.toSet().size() == 10
assert rows*.country.toSet().size() == 9
}
您可以看到,我们在处理过程中再次使用了列名称而不是列号。为了简便起见,我们遵循与 Commons CSV 示例相同的风格,只是检查了之前计算的所有相关集合的大小。
OpenCSV 还支持将 CSV 文件转换为 JavaBean 实例。首先,我们定义目标类(或对现有的域类进行注释)
class Cyclist {
@CsvBindByName(column = 'firstname')
String first
@CsvBindByName(column = 'lastname')
String last
@CsvBindByName
String team
@CsvBindByName
String country
}
对于两列,我们指示 CSV 文件中的列名称与我们的类属性不匹配。注释属性可以处理这种情况。
然后,我们可以使用以下代码将我们的 CSV 文件转换为域对象列表
file.withReader { r ->
List<Cyclist> rows = new CsvToBeanBuilder(r).withType(Cyclist).build().parse()
assert rows.size() == 21
assert rows.collect { it.first + ' ' + it.last }.toSet().size() == 15
assert rows*.team.toSet().size() == 10
assert rows*.country.toSet().size() == 9
}
OpenCSV 有很多我们没有展示的选项。在写入文件时,您可以指定分隔符和引号字符;在读取 CSV 时,您可以指定列位置、类型并验证数据。
Jackson Databind CSV
Jackson Databind 库支持 CSV 格式(以及许多其他格式)。
从现有数据写入 CSV 文件很简单,如运行示例所示
file.withWriter { w ->
new CsvMapper().writeValue(w, data)
}
这将数据写入我们的临时文件,就像我们在前面的示例中看到的那样。一个细微的差别是,默认情况下,只有包含空格的值才会用双引号括起来,但与其他库一样,有很多配置选项可以调整这些设置。
可以使用以下代码读取数据
def mapper = new CsvMapper().readerForListOf(String).with(CsvParser.Feature.WRAP_AS_ARRAY)
file.withReader { r ->
assert mapper.readValues(r).readAll() == data
}
我们更复杂的示例也是以类似的方式完成的
def schema = CsvSchema.emptySchema().withHeader()
def mapper = new CsvMapper().readerForMapOf(String).with(schema)
file.withReader { r ->
def rows = mapper.readValues(r).readAll()
assert rows.size() == 21
assert rows.collect { it.firstname + ' ' + it.lastname }.toSet().size() == 15
assert rows*.team.toSet().size() == 10
assert rows*.country.toSet().size() == 9
}
在这里,我们告诉库使用我们的标题行,并将每行数据存储在一个映射中。
Jackson Databind 还支持写入类,包括 JavaBean 以及记录。让我们创建一个记录来保存我们的自行车手信息
@JsonCreator
record Cyclist(
@JsonProperty('stage') int stage,
@JsonProperty('firstname') String first,
@JsonProperty('lastname') String last,
@JsonProperty('team') String team,
@JsonProperty('country') String country) {
String full() { "$first $last" }
}
请注意,我们再次可以指示记录组件名称与 CSV 文件中使用的名称不匹配的地方,我们只需在指定属性时提供替代名称即可。还有其他选项,例如指示某个字段是必需的,或提供其列位置,但我们在这个示例中不需要这些选项。我们还添加了一个 full()
辅助方法来返回自行车手的全名。
Groovy 将在支持的平台(JDK16+)上使用原生记录,或在更早的平台上使用模拟记录。
现在,我们可以编写记录反序列化的代码
def schema = CsvSchema.emptySchema().withHeader()
def mapper = new CsvMapper().readerFor(Cyclist).with(schema)
file.withReader { r ->
List<Cyclist> records = mapper.readValues(r).readAll()
assert records.size() == 21
assert records*.full().toSet().size() == 15
assert records*.team.toSet().size() == 10
assert records*.country.toSet().size() == 9
}
结论
我们已经介绍了将 CSV 文件写入字符串、域类和记录以及从这些地方读取 CSV 文件的方法。我们已经介绍了如何手动处理简单情况,并介绍了 OpenCSV、Commons CSV 和 Jackson Databind CSV 库。
其他使用 Groovy 进行数据科学示例的代码
https://github.com/paulk-asert/groovy-data-science