2.4 摘要
Awk可用于从包含表格数据的文件中快速获取摘要信息:最大值、最小值、列值的和,及其他类似的内容。这些也能用于数据校验:每个域里面有什么值,是否有空值等等。本节包含了几个这样的例子,下一章会有更多(下一章会讨论探索性数据分析)
下面的 addup 脚本将输入的每列值加起来,并在末尾输出总和。它也是个数组下标的简单例子:
# addup: 将每个域的值分别加起来
{ for (i = 1; i<= NF; i++)
field[i] += $i
if (NF > maxnf)
maxnf = NF
}
END {
for (i = 1; i <= maxnf; i++)
printf("%6g\t", field[i])
printf("\n")
}
如第一章所述,第二行的 += 操作符将左边的变量加上右边表达式的值。它被称为“赋值操作符”,而该语句是 field[i] = field[i] + $i 的简写。所有的算术运算符都允许这种简写方式。
如果某列的一部分,甚至整列都不是数值的话会怎么样?完全没问题。Awk将字符串开头的数值部分当作是数值,如果字符串不是由数值开始的,则它的数值为0。比如字符串 "50% off" 的数值是 50。
在作者们自己使用的脚本中,包含了 addup 的各种变体,比如计算每个域的最大最小值,计算简单的统计值如平均值和方差,统计非空值的数量,打印出现频率最高和最低的项,等等。所有这些都能用来快速了解数据的属性,或者数据中的异常或潜在的错误。
一些电子表格工具也能提供类似的功能,比如Google Sheets,还有Python的Panda库。用Awk的优点,是你能按自己的要求来定制,当然对应的缺点是你要自己写点儿代码。
2.5 个人数据库
Awk的另一个应用领域是维护个人数据库。如果你有健身,你可能已经保存了每天走或跑了多远,每天的体重,以及其他感兴趣的数值。有大量的app都包含这些功能,还带有不错的界面,漂亮的图表。而潜在的问题是某些app会侵犯隐私,而且它们也不一定总是能准确满足你的需求。
一个替代方案是把数据保存在纯文本文件里面,并用Awk和其他工具来处理。下面有个简单的例子。假如你记录了每天走过的步数,而目标是每天一万步。创建一个叫 steps 的文件,每行有两列,分别是日期和步数:
6/24/23 9342
6/25/23 4493
6/26/23 4924
6/27/23 16611
6/28/23 8762
6/29/23 15370
6/30/23 17897
7/1/23 6087
7/2/23 7595
7/3/23 14347
7/4/23 15762
7/5/23 20021
...
上面这些其实是我们其中一个作者的真实数据,他去了一个很棒的地方度假,天气好的时候每天都散步。
你可以根据难易程度来决定,按时间顺序还是倒序来写文件,我们用的是顺序。
不管顺序倒序,都能用脚本来计算一段时间内的平均步数。下面这个写得稍微复杂的版本,可以计算出周期是7天、30天、90天、1年和一生的滑动平均值。
awk '
{ s += $2; x[NR] = $2 }
END {
for (i = NR-6; i <= NR; i++) w += x[i]
for (i = NR-30; i <= NR; i++) m += x[i]
for (i = NR-90; i <= NR; i++) q += x[i]
for (i = NR-365; i <= NR; i++) yr += x[i]
printf(" 7: %.0f 30: %.0f 90: %.0f 1yr: %.0f %.1fyr: %.0f\n",
w/7, m/30, q/90, yr/365, NR/365, s/NR)
} ' $*
输出为
7: 9679 30: 11050 90: 11140 1yr: 10823 13.7yr: 10989
文本文件很适用于医疗数据(体重、血糖、血压),个人理财(股票价格、投资价值)及其他很多领域。用Awk或类似的工具来处理简单的平面文件,有着实在的好处:数据是你的,而不是别人的;很容易用你喜爱的文本编辑器来更新数据;可以用你最初没想到的方式来处理数据。
比如,我们可以对上面的程序做个扩展,用柱状图方式输出所走的不同步数的频率,以便知道你的锻炼习惯是否规律:
awk '
{ s += $2; x[NR] = $2; dist[int($2/2000)]++ }
END {
for (i = NR-6; i <= NR; i++) w += x[i]
for (i = NR-30; i <= NR; i++) m += x[i]
for (i = NR-90; i <= NR; i++) q += x[i]
for (i = NR-365; i <= NR; i++) yr += x[i]
printf(" 7: %.0f 30: %.0f 90: %.0f 1yr: %.0f %.1fyr: %.0f\n",
w/7, m/30, q/90, yr/365, NR/365, s/NR)
scale = 0.05
for (i = 1; i <= 10; i++) {
printf("%5d: ", i*2000)
for (j = 0; j < scale * dist[i]; j++)
printf("*")
printf("\n")
}
} ' $*
这个程序打印星号组成的行,星号的个数与走了对应步数的天数成正比。
2000: ****
4000: *********************
6000: ************************************
8000: *****************************************
10000: *******************************************
12000: **************************************
14000: **********************************
16000: *********************
18000: *******
20000: *
如果步数大于20000的话,这个程序会有问题。另外要有足够的数据输入,否则那些更长时间范围的平均步数就没有意义。有时输出行也可能太长,所以程序用了比例因子来使它们保持在范围内。
现实中,通常很少人会用上面这种图表,因为有太多优秀的图表库/包可用了。在7.2节,我们会展示一个程序,它生成的Python程序能画出漂亮的图表。另外,大家的标准实践都先是生成CSV文件,接着用Excel、Google Sheets或其他类似的程序来创建高质量的图表,虽然这样需要多几次手工操作。不过,在把数据传给这些工具之前,用Awk来管理和清洗数据,是不错的做法。
股票价格
还有一种很多人都非常感兴趣的个人数据:投资数据。我的钱放哪里了?收益如何?本小节给出了一个专用的例子,即从网页抓取股票价格的脚本。
网页抓取是个通用的应用。有些网站有你需要的信息,但格式不对,而你想要把这些信息一次性或周期性地提取出来。这里演示的是股票价格的抓取,但同样的方法也能用在其他类型的数据上。
网页为人类读者做了格式化,因此程序的任务是在保留所需信息的情况下去掉格式化信息。如果有个不错的HTML解析器(如Python优秀的BeautifulSoup库),这个操作会非常简单,不过Awk的优势是已经随操作系统装好了,而且很容易上手。
这里使用的网站是 bigcharts.marketwatch.com,查询地址如下(网络变化很快,有可能当你读到这篇文章时,这个地址已经不能用了)
bigcharts.marketwatch.com/quotes/multi.asp?view=q&msymb=tickers
其中的 tickers 是股票代码,多个股票代码用加号分隔,比如
$ quote aapl+amzn+fb+goog
AAPL 134.76
AMZN 98.12
FB 42.75
GOOG 92.80
$
这里我们使用Unix上的宝贵工具 curl 来获取网页,并用Awk去掉所有的HTML。找到有用的数据需要实验:研究输出样本,并使用正则表达式清除掉没用的东西。幸运的是这个网站很干净且很有条理,所以做起来不难。程序如下。注意,程序刚开始用了反斜杠来实现超长行的续行。
# quote - 根据股票代码获取股价
curl "https://bigcharts.marketwatch.com/quotes/\
multi.asp?view=q&msymb=$1" 2>/dev/null |
awk '
/<td class="symb-col"/ {
sub(/.*<td class="symb-col">/, "")
sub(/<.*/, "")
symb = $0
next
}
/<td class="last-col"/ {
sub(/.*<td class="last-col">/, "")
sub(/<.*/, "")
price = $0
gsub(/,/, "", price)
printf("%6s %s\n", symb, price)
}
'
使用这个程序时,命令行参数即股票代码的列表,被放在shell参数 $1 中 传给了curl。而 2>/dev/null 是shell的惯用结构,用于丢弃 curl 程序在处理过程中的输出报告;在这是可选的。
练习2-4 自己写个股价跟踪程序,让它输出CSV格式,这样输出结果就能用Excel或其他程序来生成图表了。