写点什么

提升 awk 技能的两个教程【译】

发布于: 2020 年 12 月 23 日
提升awk技能的两个教程【译】

原文:https://opensource.com/article/19/10/advanced-awk

作者:Dave Neary

副标题:通过邮件合并和单词计数,超越单行 awk 脚本

图片来源: Opensource.com


awk 是 Unix 和 Linux 用户工具箱中最古老的工具之一。由 Alfred Aho, Peter Weinberger, and Brian Kernighan (即 awk 命名中的 A, W, K)于 20 世纪 70 年代创建,用于文本流的复杂处理。awk 是流编辑器 sed 的配套工具,sed 是为逐行处理文本文件而设计的。awk 则允许更复杂的结构化编程,是一种完整的编程语言。

本文将阐述如何使用 awk 来处理更加结构化和更复杂的任务,包含一个简单的邮件合并应用程序。

awk 的程序结构

一个 awk 脚本由通过花括号{}作为边界的函数块组成。有两个特殊的函数块,BEGINEND,BEGIN 表示在处理第一行输入流之前执行,而 END 表示在最后一行处理完成之后执行。在二者之间,块的格式如下:


模式 { 行为语句 }
复制代码

每个块在当输入缓冲区中的行与模式匹配时执行。如果没有包含任何模式,那么这个函数块将对输入流中的每一行都会执行。

同样,下面的语法可以用于定义 awk 中的函数,并可以被任意函数块调用

function 函数名(参数列表) { 语句 }
复制代码

这种模式匹配块和函数的组合,使开发者能够开发结构化的 awk 程序,具备可重用和提升可读性。

awk 是怎样处理文本流的?


awk 从输入文件或流中每次读取一行文本,并使用字段分隔符将其解析为多个字段。awk 术语中,当前缓冲区(buffer)是一条记录。有许多特殊变量影响着 awk 如何去读取和处理文件:

  • FS (字段分隔符): 默认情况是任意空格(" "或 tab)

  • RS (记录/行分隔符): 默认是新行标记(\n)

  • NF (number of fields,字段数量): 当 awk 解析一行记录时,NF 代表已解析的字段数量

  • $0: 当前记录(行)

  • $1, $2, $3, ...: 当前记录的第一,二,三, ...,个字段

  • NR (记录行数): 截至当前 awk 脚本已解析的记录行数


除此之外,还有很多其他影响 awk 行为的变量,但作为开始了解这些已经足够了。

单行 awk 脚本


对于如此强大的工具,有趣的一点是大部分对 awk 的使用都是基本的单行代码。也许大部分常见的 awk 程序都是以 csv 文件、log 文件等作为输入,打印其中的指定字段。例如,下面的单行脚本打印了 /etc/passwd 中的用户名列表:


awk -F":" '{print $1 }' /etc/passwd
复制代码

如我们上面提到的,$1 是当前记录的第一个字段。-F 选项设置了字段分割变量为冒号 :。

字段分隔符也可以设置到 BEGIN 函数块中:

awk 'BEGIN { FS=":" } {print $1 }' /etc/passwd
复制代码

下面的示例中,每个 shell 不是/sbin/nologin 的用户,都可以通过在打印块前增加一个模式匹配来实现:

awk 'BEGIN { FS=":" } ! /\/sbin\/nologin/ {print $1 }' /etc/passwd
复制代码


awk 进阶:邮件合并

现在你已经具备了一些 awk 基础,下面通过一个更加结构化的示例:创建邮件合并,来尝试深入理解 awk。

邮件合并使用两个文件,其中一个文件(本示例中称为 email_template.txt)包含你想要发送的邮件模板:


From: Program committee <pc@event.org>To: {firstname} {lastname} <{email}>Subject: Your presentation proposal
Dear {firstname},
Thank you for your presentation proposal:  {title}
We are pleased to inform you that your proposal has been successful! Wewill contact you shortly with further information about the eventschedule.
Thank you,The Program Committee
复制代码


另一个是 csv 文件(名为 proposals.csv),是你想要发送邮件的那些人(接收人列表),内容如下:

firstname,lastname,email,titleHarry,Potter,hpotter@hogwarts.edu,"Defeating your nemesis in 3 easy steps"Jack,Reacher,reacher@covert.mil,"Hand-to-hand combat for beginners"Mickey,Mouse,mmouse@disney.com,"Surviving public speaking with a squeaky voice"Santa,Claus,sclaus@northpole.org,"Efficient list-making"
复制代码


你要读取这个 csv 文件,替换第一个文件中的相应字段(跳过 proposals.csv 的第一行),然后把结果写入名为 acceptanceN.txt 的文件中,其中 N 随着你解析每一行递增。

写出 awk 程序到 mail_merge.awk,awk 脚本中的语句通过 ;分隔。第一个任务是设置脚本所需的分割变量及其他变量。你也需要读取并丢弃 proposals.csv 的第一行,否则会创建出一个以 Dear firstname 开头的文件。为了做到这点,需要使用特定的函数 getline 并在读取之后,把记录计数器重置为 0。

BEGIN {  FS=",";  template="email_template.txt";  output="acceptance";  getline;  NR=0;}
复制代码


主函数很简洁:对处理的每一行,设置 firstnamelastnameemail, 和 title 变量的值。模板文件逐行读取,sub 函数用于替换相应的变量为指定的字符串。然后该行,连同所做的所有变量替换结果,被输出到输出文件。

由于处理的是模板文件和每行的不同输出文件,因此在处理下一条记录之前,需要清理并关闭这些文件的文件句柄。


{        # Read relevant fields from input file        firstname=$1;        lastname=$2;        email=$3;        title=$4;
        # Set output filename        outfile=(output NR ".txt");
        # Read a line from template, replace special fields, and        # print result to output file        while ( (getline ln < template) > 0 )        {                sub(/{firstname}/,firstname,ln);                sub(/{lastname}/,lastname,ln);                sub(/{email}/,email,ln);                sub(/{title}/,title,ln);                print(ln) > outfile;        }
        # Close template and output file in advance of next record        close(outfile);        close(template);}
复制代码


完成!使用下面的命令在命令行运行这个脚本:

awk -f mail_merge.awk proposals.csv
复制代码

awk -f mail_merge.awk < proposals.csv
复制代码

你将会在当前目录下找到生成的一系列文本文件。

awk 进阶: 词频统计

awk 的一个最强大的特性是关联数组。大部分编程语言中,数组元素通常是用数字作为索引,但 awk 中,数组通过一个 key 字符串来引用。你可以存储上一章 proposals.txt 文件中的元素,例如,一行记录可以存储为一个单关联数组:

		proposer["firstname"]=$1;    proposer["lastname"]=$2;    proposer["email"]=$3;    proposer["title"]=$4;
复制代码

这使得文本处理非常简单。一个使用这个概念的简单示例是词频计数器。你可以解析一个文件,提取出每行的单词(忽略标点符号),为该行中的每个单词的计数器递增,然后输出在文本中出现次数在前 20 的单词。

首先,在 wordcount.awk 文件中,设置字段分隔符为包含空格和标点符号的正则表达式:

BEGIN {        # ignore 1 or more consecutive occurrences of the characters        # in the character group below        FS="[ .,:;()<>{}@!\"'\t]+";}
复制代码

然后,在主循环函数中,遍历每个字段,忽略空字段(当行尾有标点符号时会出现这种情况),并对本行中的每个单词增加单词计数。

{        for (i = 1; i <= NF; i++) {                if ($i != "") {                        words[$i]++;                }        }}
复制代码

最终,在文本处理完成后,使用 END 函数来打印 words 数组内容,利用 awk 的管道能力输出到 shell 命令,来执行数字排序,并打印前 20 个出现次数最高的单词:

END {        sort_head = "sort -k2 -nr | head -n 20";        for (word in words) {                printf "%s\t%d\n", word, words[word] | sort_head;        }        close (sort_head);}
复制代码

以本文的早期草稿作为输入文件,执行上述脚本得到输出如下:

[dneary@dhcp-49-32.bos.redhat.com]$ awk -f wordcount.awk < awk_article.txt

the     79

awk     41

a       39

and     33

of      32

in      27

to      26

is      25

line    23

for     23

will    22

file    21

we      16

We      15

with    12

which   12

by      12

this    11

output  11

function        11

下一步?

如果你想学习更多 awk 编程的相关知识,我强烈推荐 Dale Dougherty 和 Arnold Robbins 所编写的书籍:Sed and awk 。

awk 编程的一个要点是掌握”扩展正则表达式“。awk 为你可能已经熟悉的 sed 正则表达式语法提供了强有力的补充。

另一个很棒的 awk 学习资源是 GNU awk user guide(GUN awk 用户指南). 它包含了完整的 awk 内置库,同时也提供了大量从简单到复杂的 awk 脚本示例。


更多 Linux 资源:


发布于: 2020 年 12 月 23 日阅读数: 26
用户头像

磨炼中成长,痛苦中前行 2017.10.22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
提升awk技能的两个教程【译】