众所周知,CSV是以逗号(“,”)分割的文本文件,而号称“Linux三剑客”之一的awk则是处理此类文件的重要而有效的工具。笔者在利用此工具处理一个比较大的CSV文件的时候,碰到了一个也许会比较常见,经过查找,最后比较有效地解决了这个问题。加之,这个问题来自实际的数据来源。因此,记录、整理并发布出来,供大家参考和讨论。
一、问题的描述
需要处理的CSV文件是一个“啤酒评级”(Beer Ratings)的结果,该文件来自于RateBeer.com(https://www.ratebeer.com/),大小118M,总记录数将近160万,记录内容包括啤酒厂商编号、啤酒厂商名称、啤酒名称、啤酒类别、酒精度数等13项内容。
我们的任务是:从这个文件中取出厂商名称、啤酒名称、酒精度数等5项内容,然后,再找出酒精度数最高的啤酒名称和厂商名称以及记录所在的行数。完成这个任务的第一步是简单的,那就是按照awk的要求找到对应的字段编号,只要检视一下该文件第一行就可以了,得到要取出的列数分别是:$2,$4,$8,$11,$12。问题难度在于某些记录是这样的:
281,"Tsingtao Brewery Co., Ltd.",1318061806,3,3.5,3,crusian,Foreign / Export Stout,2,2,Tsingtao Stout,7.5,52200
如果简单地用-F”,”来分割数据,就会导致后面的数据错位,而且数据量巨大;同时,其他字段也许还会有这种形式出现。因此,必须找到一个通用而有效的解决方案。
二、解决方案
如果这些数据的每个部分都是用双引号(")引起来的,那么,就可以简单地用
-F'\"*,\"*'
进行处理。但是,这里只有部分数据用到了双引号,就必须要使用到awk的另外一个参数FPAT。按照手册中对此参数的解释是:
FS:定义了参数不是什么;而FPAT则定义了参数是什么
针对这种情况,FPAT可以写成一个正则表达式
FPAT = "([^,]+)|(\"[^\"]+\")"
我们对上面的那行数据来进行一个简单的测试。首先,把上面的那个数据放到一个CSV文件中,然后再编写一个.awk的文件(这个后缀是任何字符都可以,只要你自己能明白就好),内容如下:
BEGIN {
FPAT = "([^,]+)|(\"[^\"]+\")"
}
{
print "NF = ", NF
for (i = 1; i <= NF; i++) {
printf("$%d = <%s>\n", i, $i)
}
}
然后,进行测试
awk -f ftap_test.awk ftap_test.csv
我们就会看到如下结果
这样就近乎完美地解决了这个问题。
按照这个思路,我们就可以着手解决“啤酒评级”那个文件了,为了避免以后再出现这个问题,因此,在结果输出的时候,用tab(\t)做了分隔符。
awk 'BEGIN{OFS="\t";FPAT="([^,]*)|(\"[^\"]+\")"} {print $2,$4,$8,$11,$12}' reviews.csv > reviews.tsv
这里有一点差异是,FPAT参数的第一个“+”被改成了“*”,是因为,在reviews.csv文件中,有些字段是缺失的,如果用“+”的话,会导致字段数量变少,引致新的问题。这也就是为什么上文说“近乎完美”的原因。
现在我们就可以来寻找一下“酒精度数”最高的那款啤酒了。这里之所以要加上一个if语句,就是因为有空数据的存在,会导致比较的时候出现问题。
awk -F"\t" 'NR > 1 && $5 > maxabv { if ($5~/[0-9]+/){maxabv = $5; brewery = $1; name = $4; num = NR} } END { print maxabv, "##", brewery, "##", name, "##", num }' reviews.tsv
结论应该非常出乎很多人的意料吧
57.7 ## Schorschbräu ## Schorschbräu Schorschbock 57% ## 12921
居然有将近60度的啤酒?!你是不是有兴趣找点来喝喝,然后和我分享一下感受呢。哈哈哈哈!
三、几句题外话
需要注意的是,FPAT参数是会覆盖FS参数的。而且awk中还有其他参数可以来完成这个目标,希望大家能一起来探索和分享。另外,这样处理后的字符串是带有双引号的,也可以在得到结果的时候,用substr函数进行处理。再有就是如果有两个双引号的话,这个应该是无法处理的,希望大家在使用的时候多加注意。