通过添加 CSV 支持,让 45 岁的 AWK 焕发新生
我最近为GoAWK(我开发的兼容 POSIX 的 AWK 解释器)添加了对 CSV 文件的完善支持,我认为在这个数据无处不在的时代,这项功能将让 AWK 对开发者和数据分析人员更加实用。
无论是为电子表格生成输入、分析来自公共数据源的数据,还是编写脚本处理“大数据”,CSV 和 TSV 文件如今都无处不在。至于它是不是最好的格式还有待商榷,但正如有人所说,数据格式只有两种:一种是被人抱怨的,另一种是没人用的。
早在 2018 年发布 GoAWK 后不久,我就在 Hacker News 上看到一条评论,开始考虑为 GoAWK 添加 CSV 支持:
哈哈,我刚才还在想,如果能在原生支持 CSV 文件的工具里用上 awk 编程语言该多好。突然间,这事好像变得可行多了!
当时我给了一个模棱两可的回复:
有意思。我也觉得 AWK 应该有个能正确解析 CSV 引号的模式。也许我会加一个 -csv 选项(或者当 FS 是‘,’时就自动启用——不过那样就不向后兼容了)。
好了,现在我们真的有了这样的模式!最终的命令行选项是 -i csv(“input CSV”),也算大差不差了。
非常感谢安特卫普大学图书馆对这项功能的赞助。据我所知,有两个主要团队或项目在使用 GoAWK,他们是其中之一,另一个是 Benthos 流处理平台。
但为什么需要它?
我们为什么需要这个功能呢?遗憾的是,标准 AWK 无法处理带引号字段的 CSV 文件,而这对于处理真实世界的 CSV 文件至关重要。
你可以把字段分隔符设为逗号(-F, 或 FS=","),但这完全是权宜之计,只要一遇到带引号的字段就会出错。例如:
$ cat quoted.csv
"Smith, Bob",42
$ awk -F, '{ print $1 }' quoted.csv
"Smith # you want to print the first field: Smith, Bob目前已有几种用 AWK 处理 CSV 的变通方法,比如 Gawk 的 FPAT 功能、Gawk 的各种CSV 扩展,或是 Adam Gordon Bell 的 csvquote 工具——你需要先用它转换输入再运行 awk,之后再用 csvquote -u 还原。
还有由 Eli Rosenthal 开发的 frawk,它是一款原生支持 CSV 的出色工具,但遗憾的是它在一定程度上偏离了 POSIX 标准的 AWK。(Frawk 启发了 GoAWK 的部分 CSV 支持,包括 -i 和 -o 命令行选项。)
所以我认为,在兼容 POSIX 的 AWK 中确实需要完善的 CSV 支持,而 GoAWK 正好提供了这一点。
在深入介绍之前,我们先用 GoAWK 的 CSV 输入模式来修正上面例子中的引号问题:
$ goawk -i csv '{ print $1 }' quoted.csv
Smith, Bob # that's better!功能特性
CSV 输入模式。 -i csv 选项会让 GoAWK 进入 CSV 输入模式:也就是说,忽略标准的字段和记录分隔符(FS 和 RS),改用 CSV 解析。还有 TSV 模式,你也可以使用自定义分隔符。
此外,如果使用 -H 选项,GoAWK 会将输入的第一行作为字段名,并提供一个很有用的 @"named-field" 语法,让你可以通过字段名而非编号来获取字段。
例如,假设我们有一个 states.csv 文件,内容如下:
"State","Abbreviation"
"Alabama","AL"
"Alaska","AK"
"Arizona","AZ"
...我们可以用下面的脚本只输出州的缩写:
$ goawk -i csv -H '{ print @"Abbreviation" }' states.csv
AL
AK
AZ
...或者,统计州名中包含“New”的州的数量:
$ goawk -i csv -H '@"State" ~ /New/ { n++ } END { print n }' states.csv
4其中 ~ 是标准 AWK 中的正则匹配运算符,所以这段代码的意思是:对于 State 字段匹配正则表达式 New 的记录,将 n 加一;最后打印 n。
CSV 输出模式。 -o csv 命令行参数会让 GoAWK 进入 CSV 输出模式:这会使带有一个或多个参数的 print 语句以正确的 CSV 编码输出。
例如,要将 states.csv 转换为 TSV,可以这样做(这里没有使用 -H,因此会包含表头行):
$ goawk -i csv -o tsv '{ print $1, $2 }' states.csv
State Abbreviation
Alabama AL
Alaska AK
Arizona AZ
...另外,我们拥有标准 AWK 的所有功能,所以能做的事情还有很多。
阅读 CSV 文档了解完整详情和更多示例。
实现说明
实现大约有 2000 行代码,包括大量的测试。
当然也提供了 Go API,你可以通过 interp.Config 结构体来配置选项,从而启用 CSV 输入或输出模式。你也可以在 BEGIN 块中设置 INPUTMODE 或 OUTPUTMODE 这两个特殊变量。
出于各种原因,我无法在 CSV 输入模式中直接使用 encoding/csv.Reader,但我在自己的 csvSplitter 中复用了标准库代码的结构,并将其改造成一个能解析字段并将整行作为 token 返回的 bufio.SplitFunc。
除了我自己的大量测试外,我还在自己的实现上运行了标准库 csv.Reader 测试中的相关子集,以确保没有出错,并且仍然符合 RFC 4180。
对于 CSV 输出模式,我则可以直接使用 encoding/csv.Writer(不过写 CSV 比读要简单得多)。详情请参阅 writeCSV 函数。
性能
我还没有花太多时间优化性能。打算以后好好做一次性能分析,但就目前而言已经“够用”了:速度与直接使用 Go 的 encoding/csv 包相当,比 Python 的 csv 模块快得多。在我的笔记本上,它大约三秒就能读取 1GB 复杂的 CSV 输入。
与 frawk 相比,CSV 输入速度要慢不少,不过(有点出人意料的是)CSV 输出速度要快得多。
下面是使用 goawk 和 frawk 以及原生 Python 和 Go 进行的一些简单读写基准测试的结果。写入基准测试的输出是一个 1GB、350 万行、20 列(含带引号的列)的 CSV 文件;读取基准测试则使用同一个文件作为输入。时间单位为秒,取在配备 SSD 的 64 位 Linux 笔记本上三次运行中的最佳成绩:
| 测试 | goawk | frawk | Python | Go |
|---|---|---|---|---|
| 读取 1GB CSV | 3.18 | 1.01 | 13.4 | 3.22 |
| 写入 1GB CSV | 5.64 | 13.0 | 17.0 | 3.24 |
结语
如今的计算机速度已经非常快,因此你完全可以用 GoAWK 在自己的开发笔记本或一台相对小型的虚拟机上处理大多数 CSV 数据集,即使是 GB 级别的也不在话下。还要 Hadoop 做什么呢?
所以,请在下一次处理 CSV 的任务中试试 GoAWK。AWK 已经 45 岁了,依然被广泛使用,我希望这项功能能让它在开发者的工具箱中变得更加实用。
随机一篇博客
评论
登录后参与讨论