使用 Groovy 处理数据

学习如何开始使用 Groovy 编程,并将其添加到您的数据分析工具包中。
434 位读者喜欢这篇文章。
OpenStack Superuser

Opensource.com

Groovy 几乎是 Java 的完美补充,为我的使用提供了一个紧凑、高度表达且兼容的脚本环境。当然,Groovy 并非完全完美;与任何编程语言一样,它的设计基于一系列权衡,需要理解这些权衡才能产生高质量的结果。但对我而言,Groovy 的优点远远超过其缺点,使其成为我数据分析工具包中不可或缺的一部分。在一系列文章中,我将解释如何以及为什么。

在 1990 年代后期,我发现自己对 Java 编程语言越来越感兴趣,越来越多地将其用于 AWK 过于复杂的工作以及我过去在 C 中所做的大部分工作。到 2005 年,Linux 的低成本和强大功能使我相信放弃我心爱的但老化的 Sun 工作站。对于我的工作类型,AWK、sort(1)paste(1)join(1) 在 Linux 环境中面临着来自 Perl,然后来自 Python 的激烈竞争。我从来不喜欢 Perl 的语法,但我发现 Python 很有趣,因为它具有可读性、“内置电池”理念以及与各种其他事物的集成程度,例如分隔文本文件、电子表格、数据库、绘图,但有一件事——它没有给我感觉像是干净透明地访问整个 Java 世界,而这个世界正变得越来越成为我工作流程的中心。

然后我“发现”了 Groovy。

分隔文本文件

在以 AWK 为中心的数据分析世界中,处理数据通常意味着处理分隔文本文件。这源于两个因素的结合。第一个是 Unix 文本文件处理工具通常认识到数据经常以分隔文本文件的形式出现——也就是说,文本文件的行以换行符分隔,并被分隔为字段,字段以字段分隔符分隔(例如,制表符或某些其他不寻常的字符,例如竖线)。第二个是电子表格等工具倾向于提供“导出”功能,该功能生成逗号分隔值文本文件,其第一行按照惯例是每个字段的名称,其余行由逗号分隔的数据字段组成(或者,在将逗号用作小数点的国家/地区,则使用分号)。

AWK 非常擅长处理分隔文本文件,除非字段或行分隔符也出现在数据中。此外,AWK 还非常适合用于编写对呈现的数据做出反应的代码段,并且当数据表示复杂(例如,分层)时,它就不是那么有吸引力了。AWK 也没有真正提供任何读取或写入关系数据库、电子表格或二进制格式(如 dBase)的好方法,而无需通过中间分隔文本格式。

这就是更完整的编程语言(如 Python 或 Groovy)开始变得有趣的地方。但在介绍这些直接集成示例之前,我将回到分隔文本。让我们编写一些代码!但首先,让我们获取一些数据!但等等——我们最好先安装 Groovy。

获取 Groovy

找出如何安装 Groovy 的最佳方法是访问 groovy-lang.org 上的安装说明。我更喜欢为此目的使用 SDKMAN(安装说明中途记录),但您也可以安装存储库中的版本。请注意,Java 是先决条件。现在,我使用 Java 8。同样,您可以安装存储库中的版本。

获取数据

现在您已经安装了 Groovy,请使用您的浏览器访问 世界银行的开放世界人口数据 网站。在右侧,您会看到一个下载按钮。以 CSV 格式获取数据;它以压缩包形式包含在名为 API_SP.POP.TOTL_DS2_en_csv_v2 的目录中。将此目录解压缩到系统上的一个好位置。然后打开一个终端窗口并 cd 进入该目录。

终于——代码来了!

这是一个简单的 Groovy 脚本,用于读取您下载的 CSV 文件之一并将其打印到您的终端窗口

String mdCountryCSV = "Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv"

new File(mdCountryCSV).withReader { reader ->
    reader.eachLine { line ->
        println line
    }
}

此脚本概述了 Groovy 为 Java 程序员提供的功能。

首先是 String mdCountryCSV = ...。这“就像 Java”——我们声明一个 String 变量,该变量初始化为 String 字面量。哦,是的,Groovy 允许我们在大多数情况下删除行尾分号。

接下来,new File(mdCountryCSV).withReader { reader ->,它在四行之后被 } 关闭。new File() 部分也就像 Java;但是,Groovy 增强了许多 java.lang.*java.io.*java.util.* 和标准 Java 库的其他部分。在这种情况下,Groovy 使用名为 withReader 的方法增强了 File 类。此方法接受一个 闭包 作为参数,在本例中,我们将其表现为代码块 { reader -> ... }reader -> 将闭包的参数定义为变量 reader

这种 Groovy 新特性实现了什么?在功能上,withReader 创建一个 Reader 实例,并使用分配给变量 reader 的该实例调用闭包代码,最后关闭创建的 File 实例,释放其资源并处理发生的任何错误。实际上,这让 Groovy 程序员可以将匿名方法声明为其他方法调用的参数。此外,周围的上下文在闭包内部可用,无需任何特殊的技巧。

接下来,reader.eachLine { line ->,它在两行之后被 } 关闭。同样,我们看到一个 Groovy 增强的 Reader 方法 eachLine 被调用,并将闭包作为参数,在本例中,我们使用 { line -> ... } 来表示。在这里,Reader 实例为读取文件的每一行调用闭包。

最后,println line 只是打印 Reader 实例读取的行。在这里,Groovy 向我们展示了省略方法调用参数周围的括号是可以的,并且它实际上有一个 import System.out 作为执行代码的前言。

将此代码块另存为 ex01.groovy,与数据放在同一目录下,并使用以下终端命令行执行它

groovy ex01.groovy

您看到了什么?

此时,值得注意的是,Groovy 还悄悄地取消了 Java 程序中需要发生的导入和公共类定义,而这些程序可能会执行相同的任务。

处理字段

到目前为止,我们的 Groovy 脚本已经处理了行分隔符,但尚未将行拆分为字段。快速查看文件 Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv 将显示它是最复杂的 CSV 文件类型——它使用逗号作为字段分隔符,并引用可以包含字段或行分隔符的字段。

查看第三行,Angola;在第四个字段中,出现了短语“Based on IMF data, national accounts”。在第九行,Argentina,不仅在同一个字段中出现了逗号,而且还出现了回车符/换行符对。最后,在第 199 行,该字段包含一个双引号字符,该字符显示为两个连续的双引号;“引用的引号”,不要与作为字段唯一内容的两个连续双引号混淆,后者表示一个空字段。真丑陋!

在 AWK 中,处理这种混乱的东西不太令人愉快;但是,在 Groovy 中,我们可以使用一个优秀的 Java 库,名为 opencsv从 SourceForge 下载 .jar 文件。将该 .jar 文件放在 Groovy 的默认查找路径中——在您的主目录中,在 .groovy/lib 子目录中。

此时,第一个程序可以识别字段了


import com.opencsv.CSVReader

String mdCountryCSV = "Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv"

new File(mdCountryCSV).withReader { reader ->
    CSVReader csvReader = new CSVReader(reader)
    csvReader.each { fields ->
        println fields
    }
}

将此另存为 ex02.groovy,放在同一目录下,并使用 groovy 命令运行它。

这里有什么新内容?

首先,我们必须导入 CSVReader 功能。然后我们从 withReader 传递给我们的 reader 创建一个 CSVReader 实例。最后,我们打印 CSVReader 实例生成的字段。在这里,我们使用 Groovy 放在每个对象上的 each 方法和 opencsv 提供的行拆分来处理文件中的行。csvReader.each { fields -> 为我们提供了拆分为字段的每一行——即一个字符串数组。我们可以将第一个字段称为 fields[0],第二个字段称为 fields[1],依此类推。

鉴于这种 CSV 文件的第一行提供了字段名,我们可以调整上面的代码,以便我们可以按名称引用字段,如下所示

import com.opencsv.CSVReader String mdCountryCSV = "Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv" new File(mdCountryCSV).withReader { reader -> CSVReader csvReader = new CSVReader(reader) String[] csvFieldNames = csvReader.readNext() HashMap fieldValuesByName = new HashMap()
    csvReader.each { fieldValuesByNumber ->
        csvFieldNames.eachWithIndex { name, number ->
            fieldValuesByName[name] = fieldValuesByNumber[number]
        }
        println "fieldValuesByName[\"Country Code\"] = " +
            fieldValuesByName["Country Code"] +
            " fieldValuesByName[\"IncomeGroup\"] = " +
            fieldValuesByName["IncomeGroup"]
    }
}

将此代码另存为 ex03.groovy,放在同一目录下并运行它。

在上面的代码中,我们立即从 CSVReader 实例调用 readNext 以获取第一条记录,并将字段名保存在 String 数组中。然后,每次我们读取一条记录时,我们都会在字段名上执行 each 方法,以将值从 csvReader.each() 传递的字段数组复制到 map 中,其中键是字段名,值来自记录上的相应字段。

println 语句向我们展示了如何按名称访问字段值,例如 fieldValuesByName["Country Code"]

下一步去哪里?

入门 Groovy 应该足够了。以下是一些可以提升体验的优秀参考资料

还有相当多的 Groovy 书籍提供了对该语言的良好介绍。

本 Groovy 系列的下一篇文章将进一步探讨已介绍的主题:使最后一个示例更 Groovy,读取多个 CSV 文件,链接它们的数据,并写出复合/摘要。

您有关于编程“操作指南”文章的想法吗?提交您的故事提案

标签
Chris Hermansen portrait Temuco Chile
自从 1978 年毕业于不列颠哥伦比亚大学以来,我几乎没有离开过计算机,自 2005 年以来一直是全职 Linux 用户,1986 年至 2005 年一直是全职 Solaris 和 SunOS 用户,在此之前是 UNIX System V 用户。

2 条评论

嗨,感谢这篇文章,

我也经常处理 csv 文件.... 我写了一个小型库来帮助我完成此类任务。

https://github.com/kayr/fuzzy-csv

示例
import static fuzzycsv.FuzzyCSVTable.parseCsv

parseCsv(('Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv' as File).text)
.select('Country Code', 'IncomeGroup')
.printTable()

Ronald,非常感谢您提供如此有用的评论!我期待尝试您的库,做得好!

回复 作者:Ronald (未验证)

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.