bash:清理三个文件的外连接,保留文件成员资格

时间:2013-07-06 22:45:15

标签: bash awk outer-join

考虑以下三个文件,第一行包含标题:

文件1:

id name in1
1 jon 1
2 sue 1

file2的:

id name in2
2 sue 1
3 bob 1

file3的:

id name in3
2 sue 1
3 adam 1

我想合并这些文件以获得以下输出,merged_files:

id name in1 in2 in3
1 jon 1 0 0
2 sue 1 1 1
3 bob 0 1 0
3 adam 0 0 1

此请求有几个特殊功能,我没有在grep / sed / awk / join等中以方便的方式实现。编辑:为简单起见,您可以假设这三个文件已经被排序。

4 个答案:

答案 0 :(得分:3)

这与Bash script to find matching rows from multiple CSV files中解决的问题非常相似。它不完全相同,但非常相似。 (如此相似,我只需删除三个sort命令,稍微更改三个sed命令,更改文件名,将“缺失”值从no更改为{{1} },并将最终0中的替换从逗号更改为空格。)

sed命令join(通常sed,但数据已经充分排序)是所需的主要工具。假设sort未出现在原始数据中。要记录文件中是否存在行,我们需要文件中的:字段(它几乎就在那里);如果没有匹配,我们会1提供join。每个非标题行末尾的0需要变为1,标题中的最后一个字段也需要以:1开头。然后,使用:的{​​{3}},我们可以写:

bash

$ sed 's/[ ]\([^ ]*\)$/:\1/' file1 | > join -t: -a 1 -a 2 -e 0 -o 0,1.2,2.2 - <(sed 's/[ ]\([^ ]*\)$/:\1/' file2) | > join -t: -a 1 -a 2 -e 0 -o 0,1.2,1.3,2.2 - <(sed 's/[ ]\([^ ]*\)$/:\1/' file3) | > sed 's/:/ /g' id name in1 in2 in3 1 jon 1 0 0 2 sue 1 1 1 3 adam 0 0 1 3 bob 0 1 0 $ 命令(三次)在文件的每一行的最后一个字段之前添加sed。连接几乎是对称的。 :指定字段分隔符是冒号; -t:-a 1表示当文件中没有匹配项时,该行仍将包含在输出中; -a 2表示如果文件中没有匹配项,则在输出中生成-e 0;并且0选项指定输出列。对于第一次连接,-o输出是连接列(0),然后是两个文件中的第二列(-o 0,1.2,2.2)。第二个连接在输入中有3列,因此它指定1。参数-o 0,1.2,1.3,2.2本身就意味着“读取标准输入”。 -表示法是“进程替换”,其中为join命令提供了文件名(通常为<(...)),它包含括号内的命令输出。然后输出再次通过/dev/fd/NN过滤,用空格替换冒号,产生所需的输出。

与期望输出的唯一区别在于sed之后3 bob的排序;在您所需的输出中反向命令它们的基础并不是特别清楚。如果它至关重要,可以设计一种方法来不同地解析订单(例如3 adam,除了在数据之后对标签行进行排序;如果需要,还有解决方法)。

答案 1 :(得分:3)

GNU代码

{
if ($1=="id") { v[i++]=$3; next }
b[$1,$2]=$1" "$2
c[i-1,$1" "$2]=$3
}

END {
printf ("id name")
for (x in v) printf (" %s", v[x]); printf ("\n")
for (y in b)  {
    printf ("%s", b[y])
    for (z in v) if (c[z,b[y]]==0) {printf (" 0")} else printf (" %s", c[z,b[y]])
    printf ("\n")
    }
}
$cat file?
id name in1
1 jon 1
2 sue 1
id name in2
2 sue 1
3 bob 1
id name in3
2 sue 1
3 adam 1

$awk -f prog.awk file?
id name in1 in2 in3
3 bob 0 1 0
3 adam 0 0 1
1 jon 1 0 0
2 sue 1 1 1

答案 2 :(得分:2)

awk脚本可以执行您想要的操作:

$1=="id"&&$2=="name"{
    ins[$3]= 1;
    lastin = $3;
}
$1!="id"||$2!="name" {
    ids[$1] = 1;
    names[$2] = 1;
    a[$1,$2,lastin]= $3
    used[$1,$2] = 1;
}
END {
    printf "id name"
    for (i in ins) {
        printf " %s", i
    }
    printf "\n"
    for (id in ids) {
        for (name in names) {
            if (used[id,name]) {
                printf "%s %s", id, name
                for (i in ins) {
                    printf " %d", a[id,name,i]
                }
                printf "\n"
            }
        }
    }
}

假设您的文件名为list1list2等,并且awk文件为script.awk,您可以像这样运行

$ cat list* | awk -f script.awk
id name in1 in2 in3
1 jon 1 0 0
2 sue 1 1 1
3 bob 0 1 0
3 adam 0 0 1

我确信这是一个更简单,更简单的方法,但这是我在凌晨1:30可以提出的所有内容:)

答案 3 :(得分:0)

我曾经写过这篇文章。在网上发布并在此发布,所以下次我查看它时,我可以找到它。它有点像kludge,但它支持外部,左侧,独占等连接,重复处理(删除或乘法)等。

https://code.google.com/p/ea-utils/source/browse/trunk/clipper/xjoin

TODO:更好地处理标题,处理流式输入。

Usage: xjoin [options] [:]<operator> <f1> <f2> [...*]

Joins file 1 and file 2 by the first column, suitable
for arbitratily large files (disk-based sort).

Operator is one of:

# Pasted ops, combines rows:

  in[ner]   return rows in common
  le[ft]    return rows in common, left joined
  ri[ght]   return rows in common, right joined
  ou[ter]   return all rows, outer joined

# Exclusive (not pasted) ops, only return rows from 1 file:

  ex[clude] return only those rows with nothing in common (see -f)
  xl[eft]   return left file rows that are not in right file
  xr[ight]  return right file rows that are not in left file

Common options:

  -1,-2=N     per file, column number to join on (def 1)
  -k=N        set the key column to N (for both files)
  -d    STR   column delimiter (def tab)
  -q    STR   quote char (def none)
  -h    [N]   files have headers (optionally, N is the file number)
  -u    [N]   files may contain duplicate entries, only output first match
  -s    [N]   files are already sorted, don't sort first
  -n          numeric sort key columns
  -p          prefix headers with filename/
  -f          prefix rows with the input file name (op:ex only)