使用 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 文本文件处理工具通常认识到数据经常以分隔文本文件的形式出现——也就是说,文本文件的行由换行符分隔,并被分成字段,字段由字段分隔符(例如,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 书籍提供了对该语言的良好介绍。

本 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 (未验证)

© . All rights reserved.