Contents

使用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的内置NRFNR变量来处理两个关联的输入文件。

3.1.了解NRFNR

NRFNR是两个内置的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,它将上面的两个文件作为输入,并打印每个文件中的行以及NRFNR 的值:

$ 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

上面的输出向我们展示了:

  • 对于第一个输入文件,NRFNR的值始终相同
  • awk读取一个新的输入文件时,FNR变量将被重置为1,而 NR保持递增

在下一节中,我们将看到如何区分来自NRFNR的输入文件并处理关系。

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

我们分两步解决了这个问题:

  1. 读取文件lines_to_show.txt并将行号保存在一个数组中。
  2. 当我们从文件all_lines.txt中读取行时,如果当前行号存在于数组中,我们会打印该行。

现在,让我们仔细看看上面的awk代码,了解它是如何工作的。

第 1 步:NR==FNR{ out[$1]=1; 下一个 }

  • awk从第一个文件**lines_to_show.txt 中读取第一行,即:2
  • NRFNR现在都具有相同的值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,因此,FNRNR具有不同的值
  • 在数组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

我们想要生成一个包含ProductDate和一个新列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. 处理两个以上相关的输入文件

通过比较FNRNR的值,我们学习了处理两个输入文件的紧凑方法。

但是,如果我们有两个以上的输入文件,则此方法将不起作用。

这是因为一旦输入文件更改, 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++ }

现在,让我们了解它是如何工作的:

  1. 我们声明一个变量fname来存储当前的FILENAME,并创建一个idx变量来存储当前输入文件的索引
  2. 当前输入文件更改时,fname != FILENAME将为True
  3. 然后我们用新的FILENAME更新fname并增加idx变量。
  4. 稍后,我们通过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

因此,我们更喜欢使用输入文件的索引而不是文件名来区分输入文件。