使用AWK处理多个输入文件
1. 概述
*awk *是一个方便而强大的用于处理文本的命令行实用程序。有时,我们需要读取和处理多个输入文件。
在本教程中,我们将学习如何使用awk命令处理多个输入文件。
2. 处理多个文件
有时,我们想要处理一组数据文件并生成一些输出。 例如,假设我们有三个包含用户分数的输入文件:
$ head score*.txt
==> score1.txt <==
Tom 20
Jerry 40
Mark 25
Amanda 37
==> score2.txt <==
Mark 75
Tom 70
Jerry 7
Amanda 40
==> score3.txt <==
Mark 73
Amanda 47
Jerry 79
Tom 40
请注意,所有文件共享相同的格式:每一行包含一个名称和一个分数,由空格分隔。
让我们从上面的文件中计算每个用户的分数总和:
$ awk '{ sum[$1]+=$2 } END { for(user in sum) print user, sum[user] }' score*.txt
Tom 130
Jerry 126
Mark 173
Amanda 124
在上面的代码中,我们创建了一个关联数组 sum来计算和存储每个用户的得分总和。最后,在END 块中,我们打印了数组中的元素。
当我们的输入文件共享相同的格式时,**我们可以将多个输入文件视为一个合并的输入。**这是一个比较简单的情况。
然而,在实践中,我们经常需要处理输入文件之间的关联。在以下部分中,我们将详细了解这些情况。
3. 处理两个关联的输入文件
在我们的下一个示例中,我们将展示如何使用行号和awk的内置NR和FNR变量来处理两个关联的输入文件。
3.1.了解NR和FNR
NR和FNR是两个内置的awk变量。** NR告诉我们到目前为止我们已经读取的记录总数,而FNR告诉我们我们在当前输入文件中读取的记录数。**
让我们通过一个例子来理解这两个变量。首先,让我们创建两个文件:
$ head file1.txt file2.txt
==> file1.txt <==
file1-1
file1-2
file1-3
file1-4
file1-5
==> file2.txt <==
file2-1
file2-2
file2-3
file2-4
file2-5
然后我们创建一个简单的 awk one-liner,它将上面的两个文件作为输入,并打印每个文件中的行以及NR和FNR 的值:
$ awk '{ printf "Line:%s, NR:%d, FNR:%d\n", $0, NR, FNR}' file1.txt file2.txt
Line:file1-1, NR:1, FNR:1
Line:file1-2, NR:2, FNR:2
Line:file1-3, NR:3, FNR:3
Line:file1-4, NR:4, FNR:4
Line:file1-5, NR:5, FNR:5
Line:file2-1, NR:6, FNR:1
Line:file2-2, NR:7, FNR:2
Line:file2-3, NR:8, FNR:3
Line:file2-4, NR:9, FNR:4
Line:file2-5, NR:10, FNR:5
上面的输出向我们展示了:
- 对于第一个输入文件,NR和FNR的值始终相同
- 当awk读取一个新的输入文件时,FNR变量将被重置为1,而 NR保持递增
在下一节中,我们将看到如何区分来自NR和FNR的输入文件并处理关系。
3.2. 按定义的行号打印行
让我们从一个例子开始。 我们准备了两个文件:
$ head all_lines.txt lines_to_show.txt
==> all_lines.txt <==
line-01
line-02
line-03
line-04
line-05
line-06
line-07
line-08
line-09
line-10
==> lines_to_show.txt <==
2
3
4
5
7
在文件all_lines.txt中,我们有十行文本,而文件lines_to_show.txt存储行号。现在,我们只想从all_lines.txt文件中输出一行,前提是它的行号在文件lines_to_show.txt中定义。
让我们看一下解决方案,然后了解它是如何工作的:
$ awk 'NR==FNR { out[$1]=1; next } { if (out[FNR]==1) print $0 }' lines_to_show.txt all_lines.txt
line-02
line-03
line-04
line-05
line-07
我们分两步解决了这个问题:
- 读取文件lines_to_show.txt并将行号保存在一个数组中。
- 当我们从文件all_lines.txt中读取行时,如果当前行号存在于数组中,我们会打印该行。
现在,让我们仔细看看上面的awk代码,了解它是如何工作的。
第 1 步:NR==FNR{ out[$1]=1; 下一个 }
- awk从第一个文件**lines_to_show.txt 中读取第一行,即:2
- NR和FNR现在都具有相同的值1,因此我们创建一个名为**out的关联数组并设置 out[2]=1
- 下一条语句将使awk跳过剩余的处理并读取下一条记录
- 因为在处理第一个输入文件的过程中,NR==FNR总是True,在awk处理完文件lines_to_show.txt 之后,我们有:out[2]=out[3]=out[4]=out[5]=出[7]=1
第 2 步:{ if (out[FNR]==1) print $0 }
- 当我们开始处理第二个文件all_lines.txt 时,FNR被重置为 1,因此,FNR和NR具有不同的值
- 在数组out 中,我们没有元素 *out[1],*所以我们不打印
- awk读取下一行,line-02;现在FNR为 2,我们有out[2]=1,所以这一行将通过 print $0打印出来
- 这样, awk遍历第二个输入文件后,我们就会得到需要的输出
值得一提的是,在awk 中:
- **非零数将被评估为True ** — 换句话说,’ *{ if (out[FNR] == 1) print $0 }’*可以写成 ’ { if(out[FNR]) print $0 }'
- ** True值将触发默认操作 :打印当前记录,**因此 ’ { if(out[FNR]) print $0 }’可以写为‘out[FNR]’
因此,我们可以更紧凑地编写awk单线解决方案来解决这个问题:
$ awk 'NR==FNR { out[$1]=1; next } out[FNR]' lines_to_show.txt all_lines.txt
3.3. 加入并计算
在本节中,我们将看到另一个实际示例。和往常一样,我们先来看看这两个输入文件:
$ head price.txt purchasing.txt
==> price.txt <==
Product Price(USD/Kg) Supplier
Apple 3.20 Supplier_X
Orange 3.00 Supplier_Y
Peach 5.35 Supplier_Y
Pear 5.00 Supplier_X
Mango 12.00 Supplier_Y
Pineapple 7.70 Supplier_X
==> purchasing.txt <==
Product Volume(Kg) Date
Orange 120 2020-04-02
Apple 400 2020-04-03
Peach 70 2020-04-05
Pear 50 2020-04-17
我们想要生成一个包含Product、Date和一个新列Cost的成本报告,其中Cost = Price * Volume。 我们先来看一下解决方案:
$ awk 'BEGIN { print "Product Cost Date" }
FNR>1 && NR==FNR { price[$1]=$2; next }
FNR>1 { printf "%s $%.2f %s\n",$1, price[$1]*$2, $3}' price.txt purchasing.txt
Product Cost Date
Orange $360.00 2020-04-02
Apple $1280.00 2020-04-03
Peach $374.50 2020-04-05
Pear $250.00 2020-04-17
现在让我们仔细看看代码并了解它是如何工作的:
- BEGIN块打印标题
- FNR>1跳过输入文件的标题行
- NR==FNR{ 价格[$1]=$2; next }创建一个关联数组price,从第一个输入文件中读取每一行,并将Name:Price作为Key:Value元素存储在数组中
- 当我们处理第二个文件时,我们从关联数组price中找到价格值,计算Cost并使用**printf打印输出
3.4. 处理两个输入文件的通用模式
如果我们需要使用awk处理两个输入文件,我们可以考虑使用这种典型的模式来解决问题:
awk 'NR==FNR {
// read lines from the first input file
// do calculation and save required value
// in variables or arrays
next
}
{
// process the lines from the second file
// with the variables or arrays we prepared above
}' inputFile1 inputFile2
4. 处理两个以上相关的输入文件
通过比较FNR和NR的值,我们学习了处理两个输入文件的紧凑方法。
但是,如果我们有两个以上的输入文件,则此方法将不起作用。
这是因为一旦输入文件更改, FNR总是会重置为1 。**我们无法再通过FNR变量区分输入文件。
4.1. FILENAME变量
** FILENAME是一个内置变量,用于存储awk命令当前正在处理**的输入文件的名称:
$ awk '{ print $0 " => " FILENAME}' file1.txt file2.txt file3.txt
file1-1 => file1.txt
file1-2 => file1.txt
file1-3 => file1.txt
file1-4 => file1.txt
file1-5 => file1.txt
file2-1 => file2.txt
file2-2 => file2.txt
file2-3 => file2.txt
file2-4 => file2.txt
file2-5 => file2.txt
file3-1 => file3.txt
file3-2 => file3.txt
file3-3 => file3.txt
file3-4 => file3.txt
file3-5 => file3.txt
我们可以利用这个变量来区分输入文件并应用不同的处理逻辑。
4.2. 加入并计算修订
在前面的部分中,我们生成了一份关于水果采购成本的报告。 让我们快速回顾一下这个例子。我们有两个输入文件:
- price.txt:包含价格和供应商数据:产品、价格、供应商
- purchase.txt :存储采购活动:产品,体积(公斤),日期 由于与供应商的良好合作关系,他们同意为我们提供一些折扣。现在,我们将添加第三个文件discount.txt:
$ cat discount.txt
Supplier Discount
Supplier_X 0.10
Supplier_Y 0.20
让我们从三个输入文件中生成一份关于采购成本的新报告:
$ awk 'fname != FILENAME { fname = FILENAME; idx++ }
FNR > 1 && idx == 1 { discount[$1] = $2 }
FNR > 1 && idx == 2 { price[$1] = $2 * ( 1 - discount[$3] ) }
FNR > 1 && idx == 3 { printf "%s $%.2f %s\n",$1, price[$1]*$2, $3 }
' discount.txt price.txt purchasing.txt
Orange $288.00 2020-04-02
Apple $1152.00 2020-04-03
Peach $299.60 2020-04-05
Pear $225.00 2020-04-17
在上面的代码中,我们使用FNR>1来跳过输入文件的标题行。此外,我们创建了关联数组以在不同文件处理之间共享数据。
但是,区分输入文件的关键是这行代码:
fname != FILENAME{ fname = FILENAME; idx++ }
现在,让我们了解它是如何工作的:
- 我们声明一个变量fname来存储当前的FILENAME,并创建一个idx变量来存储当前输入文件的索引。
- 当前输入文件更改时,fname != FILENAME将为True。
- 然后我们用新的FILENAME更新fname并增加idx变量。
- 稍后,我们通过idx变量来区分输入文件,并对每个输入文件进行不同的处理。
这是处理多个输入文件的常用技术之一。
4.3. 输入文件索引与文件名
我们已经看到内置的FILENAME变量存储了当前输入文件的名称。在阅读上一节的代码时,我们可能会想到一个问题:为什么我们要通过每个输入的索引来区分输入文件,而不是直接比较文件名,如示例:
FNR > 1 && FILENAME == "discount.txt" {...}
FNR > 1 && FILENAME == "price.txt" {...}
FNR > 1 && FILENAME == "purchasing.txt" {...}
将FILENAME变量与文件名进行比较也适用于本示例。但是,它有一些缺点。
最值得注意的是,它将硬编码的文件名带入了我们的awk脚本。也就是说,当我们更改文件的名称时,我们也必须更新代码。
例如,如果我们将第二个文件price.txt更改为“ /full/path/to/price.txt”,我们就必须更改我们的脚本。
有时,我们必须使用 shell 变量传递文件名,例如“ $PWD/price.txt ”。在这种情况下,我们不知道FILENAME变量的确切值。
一种解决方法是使用正则表达式匹配运算符 ~ 而不是*==*,如下所示:
FNR > 1 && FILENAME ~ /\/price[.]txt$/ {...}
但是,当我们通过进程替换将 awk命令 作为输入“文件”提供时,解决方法将失败。
*通过进程替换,输入文件的名称将由*pipe()系统调用自动生成。文件名将是动态的。
让我们看一个这种情况的例子:
$ echo "a dummy line" > dummy.txt
$ awk '{print FILENAME}' dummy.txt <(cat dummy.txt )
dummy.txt
/proc/self/fd/11
因此,我们更喜欢使用输入文件的索引而不是文件名来区分输入文件。