Unir vários arquivos delimitados por pipe com linhas / linhas desiguais em um baseado na primeira coluna

1

Una vários arquivos delimitados por pipe com linhas / linhas desiguais em um baseado na primeira coluna.

Ex:

test1.txt

1|1
2|2

test2.txt

1|4
2|5
3|6

test3.txt

1|7
2|8
3|9
4|10

saída:

1|1|4|7
2|2|5|8
3||6|9
4|||10

Exemplo 2: test1.txt

1|1|2
2|3|4

test2.txt

1|4
2|5
3|6

test3.txt

1|7
2|8
3|9
4|10

saída:

1|1|2|4|7
2|3|4|5|8
3||||6|9
4|||||10
    
por Karthik Reddy 05.10.2018 / 22:08

2 respostas

2

Somente para o caso mostrado, em que há duas colunas em cada arquivo e três arquivos:

$ join -t '|' -o0,1.2,2.2 -a 1 -a 2 test[12].txt | join -t '|' -o0,1.2,1.3,2.2 -a 1 -a 2 - test3.txt
1|1|4|7
2|2|5|8
3||6|9
4|||10

Isto é, execute uma junção externa completa relacional nos dois primeiros arquivos e junte a saída daquele com o terceiro arquivo da mesma maneira. É o -a 1 -a 2 que faz dele uma junção externa completa. Com o GNU join , você seria capaz de substituir a opção -o e seu argumento de opção por -o auto .

Isso pode ser generalizado em um script:

#!/bin/sh

# sanity check
if [ "$#" -lt 2 ]; then
    echo 'require at least two files' >&2
    exit 1
fi

# temporary files
result=$(mktemp)  # the result of a join
tmpfile=$(mktemp) # temporary file holding a previous result

# remove temporary files on exit
trap 'rm -f "$result" "$tmpfile"' EXIT

# join the first two files
join -t '|' -o auto -a 1 -a 2 "$1" "$2" >"$result"
shift 2

# loop over the remaining files, adding to the result with each
for pathname do
    mv "$result" "$tmpfile"
    join -t '|' -o auto -a 1 -a 2 "$tmpfile" "$pathname" >"$result"
done

# done, output result
cat "$result"

Esse script depende do GNU join para a opção -o auto e presume que a união ocorrerá no primeiro campo | -delimited em cada arquivo e que os arquivos são classificados lexicograficamente neste campo. / p>

Ele une os dois primeiros arquivos e, em seguida, adiciona ao resultado dessa junção, uma vez para cada arquivo restante.

Primeiro exemplo na pergunta:

$ ./script.sh test[123].txt
1|1|4|7
2|2|5|8
3||6|9
4|||10

Segundo exemplo na pergunta (observe que na pergunta, o número errado de campos vazios é mostrado):

$ ./script.sh test[123].txt
1|1|2|4|7
2|3|4|5|8
3|||6|9
4||||10

Se os arquivos não estiverem classificados, você poderá ordená-los em qualquer lugar (note: alternando para bash aqui para as substituições do processo):

#!/bin/bash

# sanity check
if [ "$#" -lt 2 ]; then
    echo 'require at least two files' >&2
    exit 1
fi

# temporary files
result=$(mktemp)  # the result of a join
tmpfile=$(mktemp) # temporary file holding a previous result

# remove temporary files on exit
trap 'rm -f "$result" "$tmpfile"' EXIT

# join the first two files
join -t '|' -o auto -a 1 -a 2 \
    <( sort -t '|' -k1,1 "$1" ) \
    <( sort -t '|' -k1,1 "$2" ) >"$result"
shift 2

# loop over the remaining files, adding to the result with each
for pathname do
    mv "$result" "$tmpfile"

    # note: $tmpfile" would already be sorted

    join -t '|' -o auto -a 1 -a 2 \
        "$tmpfile" \
        <( sort -t '|' -k1,1 "$pathname" ) >"$result"
done

# done, output result
cat "$result"

Para permitir que o usuário participe de outro campo (com -f ), use outro delimitador (com -d ) e use outro tipo de associação (com -j ),

#!/bin/bash

# default values
delim='|'
field='1'

join_type=( -a 1 -a 2 ) # full outer join by default

# override the above defaults with options given to us by the user
# on the command line
while getopts 'd:f:j:' opt; do
    case "$opt" in
        d) delim="$OPTARG" ;;
        f) field="$OPTARG" ;;
        j)
            case "$OPTARG" in
                inner) join_type=( ) ;;
                left)  join_type=( -a 1 ) ;;
                right) join_type=( -a 2 ) ;;
                full)  join_type=( -a 1 -a 2 ) ;;
                *) printf 'unknown join type "%s", expected inner, left, right or full\n' "$OPTARG" >&2
                   exit 1
            esac ;;
        *) echo 'error in command line parsing' >&2
           exit 1
    esac
done

shift "$(( OPTIND - 1 ))"

# sanity check
if [ "$#" -lt 2 ]; then
    echo 'require at least two files' >&2
    exit 1
fi

# temporary files
result=$(mktemp)  # the result of a join
tmpfile=$(mktemp) # temporary file holding a previous result

# remove temporary files on exit
trap 'rm -f "$result" "$tmpfile"' EXIT

# join the first two files
join -t "$delim" -j "$field" -o auto "${join_type[@]}" \
    <( sort -t "$delim" -k"$field,$field" "$1" ) \
    <( sort -t "$delim" -k"$field,$field" "$2" ) >"$result"
shift 2

# loop over the remaining files, adding to the result with each
for pathname do
    mv "$result" "$tmpfile"

    # note: $tmpfile would already be sorted and
    #       the join field is the first field in that file

    join -t "$delim" -2 "$field" -o auto "${join_type[@]}" \
        "$tmpfile" \
        <( sort -t "$delim" -k "$field,$field" "$pathname" ) >"$result"
done

# done, output result
cat "$result"

Teste executando novamente o segundo exemplo:

$ ./script.sh test[123].txt
1|1|2|4|7
2|3|4|5|8
3|||6|9
4||||10

Em execução nos mesmos arquivos, mas ingressando no segundo campo:

$ ./script.sh -f 2 test[123].txt
1|1|2||
10||||4
3|2|4||
4|||1|
5|||2|
6|||3|
7||||1
8||||2
9||||3

Fazendo uma junção interna:

$ ./script.sh -j inner test[123].txt
1|1|2|4|7
2|3|4|5|8
    
por 05.10.2018 / 22:37
1

com o GNU awk e seu segundo conjunto de dados de teste

BEGIN { FS = OFS = "|" }
# like the shell's shift function, returns the "former" first field
function shift(    value, i) {
    value = $1
    for (i=1; i<NF; i++) $i = $(i+1)
    NF--
    return value
}
# return a string with a character repeated n times
#    repeat("x", 5) ==> "xxxxx"
function repeat(char, n,       str) {
    str = sprintf("%*s", n, "")
    gsub(/ /, char, str)
    return str
}

FNR == 1 {fn++; nf[fn] = NF - 1}
{
    key = shift()
    data[fn][key] = $0
    seen[key]
}
END {
    for (key in seen) {
        printf "%s", key
        for (f=1; f<=fn; f++) {
            if (key in data[f])
                row = data[f][key]
            else
                row = repeat(FS, nf[f] - 1)
            printf "%s%s", FS, row
        }
        print ""
    }
}

então

gawk -f joiner.awk test{1,2,3}.txt
1|1|2|4|7
2|3|4|5|8
3|||6|9
4||||10
    
por 07.10.2018 / 17:01