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 文本文件处理工具通常认识到数据经常以分隔文本文件的形式出现——也就是说,文本文件的行由换行符分隔,并被分成字段,字段由字段分隔符(例如,TAB 或其他不寻常的字符,例如竖线)分隔。第二个是电子表格等工具倾向于提供“导出”功能,该功能生成逗号分隔值文本文件,其第一行按照惯例是每个字段的名称,其余行由逗号(或者,在将逗号用作小数点的国家/地区,则使用分号)分隔的数据字段组成。
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 文件类型——它使用逗号作为字段分隔符,并引用可以包含字段或行分隔符的字段。
查看第三行,安哥拉;在第四个字段中,出现了短语“基于国际货币基金组织数据,国民账户”。在第九行,阿根廷,不仅在同一字段中出现逗号,而且还出现回车符/换行符对。最后,在第 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 语言站点 包含示例和参考文档
- Tim Roes 的 针对 Java 开发人员的 Groovy 优秀教程
- Mr. Haki(别名 Hubert Klein Ikkink)的 优秀的 Groovy 帖子系列
还有相当多的 Groovy 书籍提供了对该语言的良好介绍。
本 Groovy 系列的下一期将进一步探讨已介绍的主题:使最后一个示例更 Groovy,读取多个 CSV 文件,链接它们的数据,以及编写复合/摘要。
您是否有关于编程“操作指南”文章的想法?提交您的故事提案。
2 条评论