应用场景
有三个文件,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脚本的处理逻辑,变得更加简单了。