如何使用Unix连接获取外连接中的所有字段?

时间:2013-03-02 20:44:52

标签: shell unix join gnu gnu-coreutils

假设我有两个文件en.csvsp.csv,每个文件都包含两个以逗号分隔的记录:

en.csv

1,dog,red,car
3,cat,white,boat

sp.csv

2,conejo,gris,tren
3,gato,blanco,bote

如果我执行

join -t, -a 1 -a 2 -e MISSING en.csv sp.csv

我得到的输出是:

1,dog,red,car
2,conejo,gris,tren
3,cat,white,boat,gato,blanco,bote

请注意,所有丢失的字段都已折叠。要获得“正确的”全外连接,我需要指定一种格式;从而

join -t, -a 1 -a 2 -e MISSING -o 0,1.2,1.3,1.4,2.2,2.3,2.4 en.csv sp.csv

产量

1,dog,red,car,MISSING,MISSING,MISSING
2,MISSING,MISSING,MISSING,conejo,gris,tren
3,cat,white,boat,gato,blanco,bote

这种生成完整外连接的方法的一个缺点是需要明确指定最终表的格式,这在程序化应用程序中可能并不容易(其中连接表的标识仅在运行时)。

最新版本的GNU join通过支持特殊格式auto消除了这一缺点。因此,使用join这样的版本,上面的最后一个命令可以被更通用的

替换
join -t, -a 1 -a 2 -e MISSING -o auto en.csv sp.csv

如何使用不支持join选项的-o auto版本实现同样的效果?


背景和详情

我有一个Unix shell(zsh)脚本,用于处理多个CSV平面文件,并通过广泛使用GNU join的'-o auto'选项来实现。我需要修改此脚本,以便它可以在可用join命令不支持-o auto选项的环境中工作(如BSD join以及旧版本的情况GNU join)。

脚本中此选项的典型用法如下:

_reccut () {
    cols="1,$1"
    shift
    in=$1
    shift
    if (( $# > 0 )); then
        join -t, -a 1 -a 2 -e 'MISSING' -o auto \
          <( cut -d, -f $cols $in | sort -t, -k1 ) \
          <( _reccut "$@" )
    else
        cut -d, -f $cols $in | sort -t, -k1
    fi
}

我展示这个例子来说明用显式格式替换-o auto是很困难的,因为要包含在这种格式中的字段在运行时才会知道。

上面的函数_reccut基本上从文件中提取列,并将结果表连接到第一列。要了解_reccut的运作方式,想象一下,除了上面提到的文件,我们还有文件

de.csv

2,Kaninchen,Grau,Zug
1,Hund,Rot,Auto

然后,例如,要显示en.csv的并排第3列,sp.csv的第2列和第4列,以及de.csv的第3列,将运行:

% _reccut 3 en.csv 2,4 sp.csv 3 de.csv | cut -d, 2-
red,MISSING,MISSING,Rot
MISSING,conejo,tren,Grau
white,gato,bote,MISSING

1 个答案:

答案 0 :(得分:2)

这是一个可能适用于您的数据的解决方案,也可能不适用于您的数据。它通过按行号对齐csv文件中的记录来解决问题,即记录2最终在行2上,记录3123在行号3123上等等。丢失的记录/行用MISSING字段填充,因此输入文件将被修改为如下所示:

en.csv

1,dog,red,car
2,MISSING,MISSING,MISSING
3,cat,white,boat

de.csv

1,Hund,Rot,Auto
2,Kaninchen,Grau,Zug
3,MISSING,MISSING,MISSING

sp.csv

1,MISSING,MISSING,MISSING
2,conejo,gris,tren
3,gato,blanco,bote

从那里可以很容易地删除感兴趣的列,并使用paste并排打印。

为实现这一目标,我们先对输入文件进行排序,然后应用一些愚蠢的awk魔法:

  • 如果记录显示在预期的行号上,请将其打印
  • 否则,打印包含预期数量的行数(这是基于文件中第一行的字段数,与join -o auto的{​​{1}}字段相同)MISSING字段,直到对齐再次正确
  • 并非所有输入文件都会使用相同数量的记录,因此在所有这些记录之前搜索最大值。然后,打印带有MISSING字段的更多行,直到达到最大值。

代码

reccut.sh

#!/bin/bash

get_max_recnum()
{
    awk -F, '{ if ($1 > max) { max = $1 } } END { print max }' "$@"
}

align_by_recnum()
{
    sort -t, -k1 "$1" \
        | awk -F, -v MAXREC="$2" '
            NR==1 { for(x = 1; x < NF; x++) missing = missing ",MISSING" }
            {
                i = NR
                if (NR < $1)
                {
                    while (i < $1)
                    {
                        print i++ missing
                    }
                    NR+=i
                }
            }1
            END { for(i++; i <= MAXREC; i++) { print i missing } }
            '
}

_reccut()
{
    local infiles=()
    local args=( $@ )
    for arg; do
        infiles+=( "$2" )
        shift 2
    done
    MAXREC="$(get_max_recnum "${infiles[@]}")" __reccut "${args[@]}"
}

__reccut()
{
    local cols="$1"
    local infile="$2"
    shift 2

    if (( $# > 0 )); then
        paste -d, \
            <(align_by_recnum "${infile}" "${MAXREC}" | cut -d, -f ${cols}) \
            <(__reccut "$@")
    else
        align_by_recnum "${infile}" "${MAXREC}" | cut -d, -f ${cols}
    fi
}

_reccut "$@"

运行

$ ./reccut.sh 3 en.csv 2,4 sp.csv 3 de.csv
red,MISSING,MISSING,Rot
MISSING,conejo,tren,Grau
white,gato,bote,MISSING