AWK编程语言指南
1. 概述
处理文本是许多开发人员日常工作的一部分。在本教程中,我们将学习如何使用AWK 编程语言处理大型文本数据集**。
2. 起源故事
除非我们知道 AWK 的起源故事,否则我们最终可能会误解它的含义。所以,让我们穿越到过去,读一读 AWK 创作者的思想。
在 1970 年代,贝尔实验室 专注于提供一系列非凡的计算发明,而这一切都始于前十年 Unix 操作系统的诞生。为了进一步增强 Unix 用户的能力,他们通过描绘一个工具箱来创建软件,其中每个工具都可以很好地完成一项小任务。
其中,基本计算需求之一是有效地处理大型文本文件。常规编辑器和现有的编程语言都无法有效地满足这一要求。一方面,普通编辑器几乎不可能打开大文本文件,另一方面,即使是小任务,也需要编写太多代码。
正如我们常说的必要性是发明之母,三位计算机科学家,即Alfred A ho 、Peter J. Weinberger 和Brian Kernighan ,很快就通过共同创作一种新的编程语言提出了解决方案。此外,他们最终将其命名为 AWK,方法是取每个姓氏的第一个字母。
即使经过这么多年,我们仍然可以欣赏这项发明的美妙之处,因为它在今天这个瞬息万变的技术世界中仍然具有相关性。
3. 顺序读/写
使用 AWK,我们可以处理大型文本文件,而不必担心它们的大小。这是可能的,因为我们不是将整个文本加载到内存中,而是按顺序读取它 chunks。让我们看看 AWK 的做法。
3.1. 输入记录
首先,我们需要告诉 AWK 它应该将整个文本输入解释为记录序列。为了分隔记录,我们使用正则表达式,其值可通过变量RS访问。如果我们没有为 RS 指定值,AWK 将选择一个默认值作为换行符。让我们看看当*RS=”\n”*时这个读取流程的模拟:
对于大多数实际目的,我们会很好地使用这种方法,因为文件中的单行文本很可能适合我们的可用内存。 但是,AWK 可能不是在某些情况下使用的理想工具。例如,让我们考虑一个假设的情况,我们需要找出文件的第一行和最后一行是否具有相同的内容。自然,AWK 在这种情况下将无效,因为它必须逐行扫描完整的文件。
相反,我们可以通过使用诸如head 和tail 之类的 Unix 实用程序同时从头到尾读取输入文件来有效地解决这个问题:
[[ "$(head -1 input.txt)" == "$(tail -1 input.txt)" ]]; echo $?
3.2. 输入字段
当 AWK 一次读取一条记录时,我们必须有一个好的策略来有效地处理这个文本单元。为此,我们可以借助字段分隔符将记录进一步拆分为一系列字段,该字段可以是任何正则表达式。
AWK 将字段分隔符的值存储在一个名为FS的内部变量中。由于将空格视为字段分隔符很常见,因此我们可以依赖FS 的默认值,它是一个正则表达式*[ \t\n]+,*表示存在一个或多个制表符、空格或换行符。当然,当默认值不适合我们的用例时,我们可以选择显式定义它。
此外,AWK 可以方便我们借助字段变量来引用字段文本。作为一般经验法则,字段变量是通过在任何字段索引之前使用 $ 前缀形成的。让我们想象一下,并准备好理解更高级的概念:
我们必须注意,与大多数编程语言中的数组索引不同,AWK 中的字段索引从值 1 开始,最后一个字段索引存储在名为NF的内部变量中。此外,当我们想要引用完整的记录时,我们可以使用*$0*变量。
3.3. 输出字段和记录
因此,我们的 AWK 程序将输入文本解释为由记录和字段组成。同样,它也将这种类比用于其输出文本。但同时,它为我们提供了**为输出记录分隔符 ( ORS ) 和输出字段分隔符 ( OFS )**选择不同分隔符的灵活性。
要打印组织为记录和字段的输出文本,我们需要为自己配备 AWK 的内置print命令:
print [expression ...]
现在,让我们利用这些知识以可读格式打印数字的平方,n*n = n 2 ,对于输入记录中存在的每个数字n:
{print $1 "*" $1 ,"=", $1*$1}
我们必须注意,我们的 print 语句对三个用逗号分隔的表达式进行操作:
- 第一个是字符串表达式,*$1 “*” $1,*代表等式的左边
- 第二个表达式是与等号“=”对应的字符串文字
- 第三个是数学表达式,*$1*$1,*形成等式的右手边
因此,分隔各个表达式的逗号充当 AWK 的提示,以识别输出字段。因此,在打印时,每个逗号都替换为存储在OFS中的字符串,其默认值为单个空格字符。此外,我们打印语句的结尾是 AWK 识别记录结尾的另一个提示。因此,每个打印语句都会打印存储在ORS中的字符串,其默认值为换行符。
最后,让我们看看当我们的程序对包含前四个自然数的样本输入进行操作时的输出结果,每个自然数位于单独的行中:
1*1 = 1
2*2 = 4
3*3 = 9
4*4 = 16
4. AWK 程序
4.1. 模式动作基础
在编写 AWK 程序之前,我们应该有一个处理策略来处理正在读取的输入文本的每一行:
- 我们需要在此行上执行的操作列表
- 要执行这些操作必须满足的条件或模式
之后,我们可以将我们的*程序制定为一系列*[pattern] [action]语句,根据这些语句处理输入文本的每一行。
让我们想象一下,我们有一个简单的应用程序,它从一个包含每一行评论的文本文件中显示用户的反馈评论。但是,从可读性的角度来看,我们只想显示那些少于 100 个字符的评论。让我们看看我们如何编写一个 AWK 程序来做到这一点:
length($0) < 100 { print }
是的,这只是一个单行程序。正如我们所看到的,我们的操作语句位于大括号内,就在使用内置length()函数和由$0标识的当前记录变量的条件模式之后。
4.2. 默认行为
在处理文本数据时,AWK 对开发人员非常友好,因为它在许多常见场景中都具有默认行为。因此,我们可以用很少的代码完成很多工作。 一般来说,模式和动作语句并不总是强制编写的。AWK 能够通过使用默认行为填充缺失的模式和操作来提供这种灵活性:
- 默认模式将匹配输入中的每条记录
- 默认操作将打印每条记录
知道了这个事实,我们可以跳过用户反馈过滤 AWK 程序中的操作部分,并通过保持模式单独获得相同的结果:
length($0) < 100
4.3. 执行流程
AWK 程序的执行流程很大程度上受到文本处理任务需求的启发。为了理解这一点,假设我们必须首先在白板上画出一份粗略的总结报告。为了有效地完成这项任务,我们需要一个过程来遵循:
- 获取相关项目,例如标记和橡皮擦
- 确定报告的标题
- 一次阅读来自原始来源的所有用户反馈,并对其长度应用过滤器
- 最后,我们可以通过给出积极反馈百分比等基本数字来总结
同样,AWK 程序首先加载必要的用户定义函数。接下来,它通过执行写在Begin块中的语句来执行一次性设置活动。之后,执行Main块中的语句以一次处理一行输入文本,直到没有更多行要处理。最后,它通过执行End块中的语句来进行一次性清理或汇总活动。
我们必须注意,这些活动中的每一个都是可选的。但是,在大多数情况下,我们至少会有主块。此外,我们可以根据需要拥有尽可能多的Begin和End块,但常见的样式是只使用一个。
4.4. BEGIN 和 END 模式
从我们迄今为止创建的简单用户反馈应用程序中,我们无法判断它列出的简短评论占原始文件总评论的百分比。此外,由于缺少可以设置上下文的标题,可读性会受到影响。
这些附加任务都不能用一般的 [pattern] [action] 序列有效地处理,因为我们只需要执行一次。因此,标题应该在生成主体之前出现。并且,百分比摘要应该出现在所有评论之后的底部。 为此,我们需要使用 BEGIN 和 END 模式,它们是独一无二的,原因有两个:
- 这些模式所遵循的所有操作仅执行一次
- 我们不能跳过这两种模式的动作
所以,让我们扩展我们原来的 AWK 程序来支持新的需求:
BEGIN { print "User Feedback (Only Short Comments)" }
length($0) < 100 { count++; print $0 }
END { print "Percentage of Short Comments:", (count/NR)*100, "%" }
我们必须注意,我们没有初始化变量,因为AWK将其所有变量的默认数值解释为 0。此外,我们还使用*了一个内置变量*NR,它为我们提供了到目前为止我们已经阅读的行数。
此外,随着我们的需求不断增长,我们现在需要执行多个语句作为模式的操作。因此,AWK 要求我们要么**将每个语句放在单独的行中,要么用分号分隔它们,**就像我们在 Main 和 End 块中所做的那样。
4.5. awk命令
到目前为止,我们完全专注于理解用 AWK 语言编写的程序的结构和流程。现在我们已经涵盖了基础知识,是时候开始执行我们的程序并通过比较 AWK 程序的预期输出和实际输出来巩固我们的概念了。
为了执行我们的 AWK 程序,我们需要使用awk 命令:
awk [ -F fs ] [ -v var=value ] [ 'prog' | -f progfile ] [ file ... ]
首先,让我们看看如何使用 awk 命令运行我们的用户反馈应用程序的初始单行版本:
awk 'length($0) < 100' input.txt
我们必须注意,我们需要用引号将我们的内联程序括起来。
尽管我们可以使用相同的方法来执行冗长的程序,但这会降低可读性并使我们的程序编辑容易出错。因此,对于我们的程序跨越多行的场景,让我们使用基于脚本的方法:
awk -f comments.awk input.txt
和之前一样,我们的输入源是同一个文本文件input.txt。但是,这一次,awk 命令正在执行包含我们的 AWK 程序的脚本comments.awk。
5. 搜索模式
大多数复杂的文本处理通常涉及在文本中搜索特定模式。为了迎合这些用例,AWK 支持使用正则表达式 来创建搜索模式。
5.1. 正则表达式
在我们的用户反馈应用程序中,假设我们现在收到了一个新要求,以估计表示他们对基础服务感到满意的客户百分比。 为了使用 AWK 程序将其作为概念验证,让我们首先做一些假设以保持简单:
- 我们应该考虑分析中的所有评论,而不仅仅是简短的评论
- 包含“好”或“快乐”字样的反馈评论表示满意的客户反馈
现在,让我们创建一个正则表达式,使其匹配满足这三个条件的注释:
- 位于关键字左侧的文本是行首或空格,因此匹配*(^|[\t]+)*
- 这两个关键字中的任何一个都可以出现在任何情况下,因此中间部分必须匹配表达式*(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY ]))*
- 位于关键字右侧的文本要么是行尾要么是空格,因此它匹配*($|[\t]+)*
最后,我们准备将它们存储在一个名为pattern的变量中:
pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
呸!一下子看完整个表情让人望而生畏。然而,帮助我们的是使用分而治之的方法将整个文本分成更小的块,这样更容易创建简单的正则表达式。后来,我们将它们组合成一个正则表达式。
5.2. 波浪号运算符
现在我们已经有了一个方便的正则表达式来匹配与我们相关的注释,让我们使用波浪号运算符 ~ 来匹配我们的记录:
BEGIN {
print "Positive Feedback Comments:"
pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
}
$0 ~ pattern {
count++
print $0
}
END {
print "Percentage:", (count/NR)*100, "%"
}
我们必须注意 ~ 是一个二元运算符,因此我们明确提到我们希望将正则表达式与*$0进行匹配。或者,我们可以选择将我们的正则表达式与完整的记录进行隐式匹配,方法是将其作为正则表达式文字括在两个正斜杠之间,/(^|[\t ]+)* (([gG][oO]{2}[ dD])|([hH][aA][pP]{2}[yY]))($|[\t ]+)/ 没有提及*$0*。
6. 高级文本处理
到目前为止,一切都很好。让我们准备好看看如何扩展我们的用户反馈应用程序来解决一个稍微棘手的问题,即报告用户积极反馈的连续性。
6.1. 跨多行跟踪数据
通过积极的用户反馈,我们指的是那些包含关键字“好”或“快乐”的用户评论。另一方面,负面用户反馈是那些包含关键字“糟糕”或“不开心”的用户评论。所有其他用户评论均被视为中立。
现在,正面反馈条纹是一系列评论,从正面评论开始,一直持续到输入源中出现负面用户反馈。相反,负面反馈以负面评论开始,以正面评论结束。在任何一种情况下,对于本练习,**中立的评论都不能结束连胜。**相反,它会影响穿过它的条纹的长度。让我们通过一个示例输入来使这个概念变得清晰:
I'm happy with this service. Keep it up.
ok to use
Please expand your services to Canada. We'll benefit from your good work.
Terribly bad
When are you coming up in Dubai?
We need more of such good services in India
You guys know how to make your customers happy
接下来,让我们找出示例文本的所有正面和负面条纹:
- 积极的连续性从第 1 行开始并在第 3 行停止,因为第 4 行有负面评论
- 负面的连续性从第 4 行开始并在第 5 行停止,因为第 6 行有正面评论
- 从第 6 行到第 7 行是正向
我们必须注意,第一个积极的连续性包括第 2 行的中性评论。同样,第 5 行的中性评论有助于消极的连续性。
6.2. 数组
作为打印正反馈条纹的解决方案的一部分,我们需要显示条纹的起点及其长度。自然地,实现这一点的一种方法是通过一种机制,我们可以在第一个正面评论出现的记录的起始位置和相应的长度之间保持映射。
为此,AWK 提供了**关联数组 的特性来存储(键,值)对,其中键和值可以是字符串或数字数据类型**。让我们定义几个关联数组,这将使我们的工作很容易找到解决方案。
首先,我们需要跟踪我们正在处理的当前正反馈条纹的开始和结束索引。因此,让我们分别通过*cur_streak[“start”]和cur_streak[“end”]*来引用这些值。
其次,让我们在名为len_streak的数组的帮助下,跟踪一个条纹的起始记录编号与其长度之间的映射。因此,如果一条连续记录从第 i 条记录开始,那么*len_streak[i]*会给我们它的长度。
6.3. 用户定义的函数
与许多其他编程语言一样,AWK 通过使用函数来支持代码的可重用性和模块化。因此,让我们首先编写一个名为*reset_streak()*的辅助函数来初始化或重置与我们当前的条纹相关的参数:
function reset_streak(streak) {
streak["start"] = -1
streak["len_so_far"] = 0
}
最终,我们需要打印每个条纹的长度及其起始行号。因此,让我们编写另一个名为print_streaks_info()的函数,它遍历数组arr的键值对:
function print_streaks_info(arr, index) {
for(index in arr) {
print index, arr[index]
}
}
我们必须注意,所有变量在 AWK 中都有一个全局范围。因此,我们遵循了一个流行的惯例,即在参数列表中添加一些额外的空间,以将真正的函数参数arr与第二个参数index分开。此外,变量索引将有效地用作局部变量,并且不需要在函数调用时作为参数传递。
6.4. 范围模式
如果我们仔细观察我们的积极用户反馈连续性,它会从文本与带有积极语气的关键字匹配开始。此外,它在文本与带有否定语气的关键字匹配时结束。
让我们从打印报告的标题开始,然后初始化用于模式匹配的正则表达式和参数以跟踪我们在BEGIN块中当前连胜的进度:
BEGIN {
print "Positive Feedback Streaks:"
positive_pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
negative_pattern="(^|[\\t ]+)(([bB][aA][dD])|([uU][nN][hH][aA][pP]{2}[yY]))($|[\\t ]+)"
reset_streak(cur_streak)
}
起初,这样的场景看起来有点难以处理,但 AWK 为我们准备了一个救援计划。为了有效地解决这个问题,我们可以使用 AWK 的范围模式来跟踪一系列连续的行,以便第一行匹配第一个模式begin_pattern而最后一行匹配第二个模式end_pattern:
$0 ~ begin_pattern, $0 ~ end_pattern
当我们从这些方面思考时,我们可以看到一个正条纹是一个可通过范围模式识别的闭开区间 :
$0 ~ positive_pattern, $0 ~ negative_pattern
同样,如果我们反转这些模式的位置,我们将能够将负条纹识别为闭开区间:
$0 ~ negative_pattern, $0 ~ positive_pattern
最重要的是,我们必须注意,对于这两种情况,条纹的结束与范围模式选择的范围的倒数第二行相同。这仅仅是因为每个范围模式的第二部分的作用仅限于确定第一行,在此之后该连胜将不再继续。因此,我们可以将输入文本可视化为一系列交替的正负反馈条纹:
6.5. 主块
到目前为止,我们已经准备好了我们的模式。现在,让我们使用两个重要的见解来帮助我们提出核心行动声明:
- 两个交替范围共享正负条纹之间的边界
- 代表边界的线将匹配两个范围模式
首先,我们可以将每条记录的cur_line_visited_count的值初始化为 0,以便我们以后可以使用它来识别边界线:
{
cur_line_visited_count = 0
}
接下来,如果没有设置当前连续记录的开始,我们可以将其设置为当前记录号NR。此外,我们需要增加cur_line_visited_count和*cur_streak[“len_so_far”]*因为与正条纹对应的范围模式匹配:
$0 ~ positive_pattern, $0 ~ negative_pattern {
cur_line_visited_count++
if(cur_streak["start"] == -1) {
cur_streak["start"] = NR
}
cur_streak["len_so_far"]++
}
最后,让我们写出对应于负条纹的范围模式的动作。因此,当我们看到当前行已经匹配了两次时,我们就知道它是共享边界。因此,根据我们是否有正在进行的连续性,我们可以终止当前的连续性或在当前行开始新的连续性,NR:
$0 ~ negative_pattern, $0 ~ positive_pattern {
cur_line_visited_count++
if(cur_line_visited_count == 2) {
if(cur_streak["start"] != -1 && cur_streak["start"] < NR) {
len_streak[cur_streak["start"]] = cur_streak["len_so_far"] - 1
reset_streak(cur_streak)
} else {
cur_streak["start"] = NR
}
}
}
我们必须注意,在len_streak数组中保存当前条纹的长度时,我们将其值减 1 以确保不包括最后一行。
6.6. 端块
作为最后一次正向条纹的例外,相应的负反馈条纹可能不存在。因此,在打印所有条纹之前,我们需要在End块中处理这种边缘情况:
END {
if(cur_streak["end"] == -1 ){
len_streak[cur_streak["start"]] = NR - cur_streak["start"] + 1
}
print_streaks_info(len_streak)
}
我们必须注意,当我们在结束块中使用变量NR时,它总是为我们提供 我们读取的文件中最后一条记录的索引。
6.7. AWK 脚本
现在我们已经完成了程序的所有部分,让我们完整地查看 AWK 脚本:
function print_streaks_info(arr, index) {
for(index in arr) {
print "starting index: " index, ", length of streak: " arr[index]
}
}
function reset_streak(streak) {
streak["start"] = -1
streak["len_so_far"] = 0
}
BEGIN {
print "Positive Feedback Streaks:"
positive_pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
negative_pattern="(^|[\\t ]+)(([bB][aA][dD])|([uU][nN][hH][aA][pP]{2}[yY]))($|[\\t ]+)"
reset_streak(cur_streak)
}
{
cur_line_visited_count = 0
}
$0 ~ positive_pattern, $0 ~ negative_pattern {
if(cur_streak["start"] == -1) {
cur_streak["start"] = NR
}
cur_line_visited_count++
cur_streak["len_so_far"]++
}
$0 ~ negative_pattern, $0 ~ positive_pattern {
cur_line_visited_count++
if(cur_line_visited_count == 2) {
if(cur_streak["start"] != -1 && cur_streak["start"] < NR) {
len_streak[cur_streak["start"]] = cur_streak["len_so_far"] - 1
reset_streak(cur_streak)
} else {
cur_streak["start"] = NR
}
}
}
END {
if(cur_streak["start"] != -1) {
len_streak[cur_streak["start"]] = NR - cur_streak["start"] + 1
}
print_streaks_info(len_streak)
}
最后,让我们用这个 AWK 脚本处理我们的示例输入:
$ awk -f streak_script.awk input_comments.txt
当然,我们可以验证结果是否符合我们之前的分析:
Positive Feedback Streaks:
starting index: 6 , length of streak: 2
starting index: 1 , length of streak: 3