awk实现多文件按列合并

应用场景

有三个文件,a.txt, b.txt, c.txt.

a.txt 文件格式:K A

$ cat a.txt
1 234
2 456
4 789

b.txt 文件格式:K B

$ cat b.txt
2 33333
3 44444

c.txt 文件格式:K C

$ cat c.txt
4 890
5 8324
7 1111

把a b c三个文件合并成一个文件,文件格式为:K A B C。其中,不存在的列值补为0。

用awk来处理文本:

awk '
    FNR == 1 {
        n++;
    }
    {
        a[$1] = 0;
        b[$1","n] = $2;
    }
    END {
        for(k in a){
            printf("%s\t", k); 
            for(m = 1; m <= n; m++) {
                printf("%s\t", b[k","m] == "" ? 0 : b[k","m]); 
            }
            print "";
        }
    }
' a.txt b.txt c.txt | sort

输出结果:

1       234     0       0
2       456     33333   0
3       0       44444   0
4       789     0       890
5       0       0       8324
7       0       0       1111

这里面存在的问题是,没有考虑存在空文件的情况。添加一个文件d.txt,文件格式为:K D

$ touch d.txt

如果不向d.txt写内容,把d.txt作为一个空文件放到脚本参数中,希望最终的结果格式是:K A B C D

awk '
    FNR == 1 {
        n++;
    }
    {
        a[$1] = 0;
        b[$1","n] = $2;
    }
    END {
        for(k in a){
            printf("%s\t", k); 
            for(m = 1; m <= n; m++) {
                printf("%s\t", b[k","m] == "" ? 0 : b[k","m]); 
            }
            print "";
        }
    }
' a.txt b.txt c.txt d.txt | sort

结果:

1       234     0       0
2       456     33333   0
3       0       44444   0
4       789     0       890
5       0       0       8324
7       0       0       1111

但我们想要的是:

1       234     0       0       0
2       456     33333   0       0
3       0       44444   0       0
4       789     0       890     0
5       0       0       8324    0
7       0       0       1111    0

这里我们可以先把K的集合收集一下

$ cat a.txt b.txt c.txt d.txt | awk '{print $1}' | sort | uniq > tmp.txt
$ cat tmp.txt
1
2
3
4
5
7

把空文件用K集合与0填充:

for file in a.txt b.txt c.txt d.txt; do
    if [[ ! -s $file ]]; then
        awk '{print $0" "0}' tmp.txt > $file
    fi
done 

$ cat d.txt
1       0
2       0
3       0
4       0
5       0
7       0

再次执行代码:

awk '
    FNR == 1 {
        n++;
    }
    {
        a[$1] = 0;
        b[$1","n] = $2;
    }
    END {
        for(k in a){
            printf("%s\t", k); 
            for(m = 1; m <= n; m++) {
                printf("%s\t", b[k","m] == "" ? 0 : b[k","m]); 
            }
            print "";
        }
    }
' a.txt b.txt c.txt d.txt | sort

结果:

1       234     0       0       0
2       456     33333   0       0
3       0       44444   0       0
4       789     0       890     0
5       0       0       8324    0
7       0       0       1111    0

这种方式一个不好的地方是修改了空文件。如果规定空文件不能改变,则一种方式是检查空文件的时候标记一下,等结果出来之后再把被覆盖的空文件置空。

如果不希望原始文件被改动,那么还有没有别的方式呢?

其实,上面的方式中那两段shell脚本的最终目的,是为了避免在存在空文件的情况下产生的结果中,空文件所占有的那列被忽略掉。这里我们试着用awk自身提供的特性来达到相应的目的。

最开始,可能会想用awk的FNR和NR变量来实现对文件是否为空的检验,但问题是当文件为空时,FNR不会被赋值,程序会直接跳到对下一个文件的处理。所以这里需要在开始处理文件之前,先把所有文件名与其所在文件参数列表中的顺序收集起来,这一步可以在BEGIN里做。

BEGIN {
    for(i = 1; i < ARGC; i++) {
        file_index_arr[ARGV[i]] = i;
    }
}

开始处理文件后,借助上面收集的文件名与文件名所在参数列表中的顺序的对应关系,把每个不空的文件的K集合、以及K与V的对应关系记录下来:

FNR == 1 {
    current_index = file_index_arr[FILENAME];
}
{
    key_arr[$1] = 0;    /* 收集 K */
    kv_arr[$1"-"current_index] = $2;    /* 记录当前文件的当前 K 对应数值 */
}

这里,key_arr的size就是最终结果的行数。

然后,遍历收集的K集合key_arr和KV对应关系kv_arr,有值的直接输出,没有值的则输出0。最终的程序如下:

awk '
    BEGIN {
        for(i = 1; i < ARGC; i++) {
            file_index_arr[ARGV[i]] = i;
        }
    }
    FNR == 1 {
        current_index = file_index_arr[FILENAME];
    }
    {
        key_arr[$1] = 0;    /* 收集 K */
        kv_arr[$1"-"current_index] = $2;    /* 记录当前文件的当前 K 对应数值 */
    }
    END {
        for(key in key_arr){
            printf("%s\t", key); 
            for(i = 1; i < ARGC; i++) {
                printf("%s\t", kv_arr[key"-"i] == "" ? 0 : kv_arr[key"-"i]); 
            }
            print "";
        }
    }
' a.txt b.txt c.txt d.txt | sort

结果:

1       234     0       0       0
2       456     33333   0       0
3       0       44444   0       0
4       789     0       890     0
5       0       0       8324    0
7       0       0       1111    0

与第一种方式方式相比,第二种省去了shell脚本的处理逻辑,变得更加简单了。