Frage Ein effizienter Weg, um eine Datei in Bash zu transponieren


Ich habe eine große Tab-getrennte Datei, die so formatiert ist

X column1 column2 column3
row1 0 1 2
row2 3 4 5
row3 6 7 8
row4 9 10 11

Ich würde gern transponieren Es ist in einer effizienten Art und Weise mit nur Bash-Befehle (Ich könnte schreiben ein zehn oder so Zeilen Perl-Skript, um das zu tun, aber es sollte langsamer sein als die native Bash-Funktionen ausführen). So sollte die Ausgabe aussehen

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11

Ich dachte an eine solche Lösung

cols=`head -n 1 input | wc -w`
for (( i=1; i <= $cols; i++))
do cut -f $i input | tr $'\n' $'\t' | sed -e "s/\t$/\n/g" >> output
done

Aber es ist langsam und scheint nicht die effizienteste Lösung zu sein. Ich habe eine Lösung für vi in ​​gesehen dieser Beitrag, aber es ist immer noch übermäßig langsam. Irgendwelche Gedanken / Vorschläge / brillante Ideen? :-)


87
2017-11-13 15:13


Ursprung


Antworten:


awk '
{ 
    for (i=1; i<=NF; i++)  {
        a[NR,i] = $i
    }
}
NF>p { p = NF }
END {    
    for(j=1; j<=p; j++) {
        str=a[1,j]
        for(i=2; i<=NR; i++){
            str=str" "a[i,j];
        }
        print str
    }
}' file

Ausgabe

$ more file
0 1 2
3 4 5
6 7 8
9 10 11

$ ./shell.sh
0 3 6 9
1 4 7 10
2 5 8 11

Performance gegen Perl Lösung von Jonathan auf einer 10000 Zeilen Datei

$ head -5 file
1 0 1 2
2 3 4 5
3 6 7 8
4 9 10 11
1 0 1 2

$  wc -l < file
10000

$ time perl test.pl file >/dev/null

real    0m0.480s
user    0m0.442s
sys     0m0.026s

$ time awk -f test.awk file >/dev/null

real    0m0.382s
user    0m0.367s
sys     0m0.011s

$ time perl test.pl file >/dev/null

real    0m0.481s
user    0m0.431s
sys     0m0.022s

$ time awk -f test.awk file >/dev/null

real    0m0.390s
user    0m0.370s
sys     0m0.010s

EDIT von Ed Morton (@ ghostdog74 können Sie gerne löschen, wenn Sie es ablehnen).

Vielleicht hilft diese Version mit einigen expliziten Variablennamen, einige der folgenden Fragen zu beantworten und generell zu klären, was das Skript macht. Es verwendet auch Registerkarten als das Trennzeichen, das das OP ursprünglich angefordert hatte, so dass es mit leeren Feldern umgehen würde und es zufälligerweise die Ausgabe ein bisschen für diesen speziellen Fall prettiert.

$ cat tst.awk
BEGIN { FS=OFS="\t" }
{
    for (rowNr=1;rowNr<=NF;rowNr++) {
        cell[rowNr,NR] = $rowNr
    }
    maxRows = (NF > maxRows ? NF : maxRows)
    maxCols = NR
}
END {
    for (rowNr=1;rowNr<=maxRows;rowNr++) {
        for (colNr=1;colNr<=maxCols;colNr++) {
            printf "%s%s", cell[rowNr,colNr], (colNr < maxCols ? OFS : ORS)
        }
    }
}

$ awk -f tst.awk file
X       row1    row2    row3    row4
column1 0       3       6       9
column2 1       4       7       10
column3 2       5       8       11

Die obigen Lösungen werden in jedem awk funktionieren (außer alt, gebrochen awk natürlich - dort YMMV).

Die obigen Lösungen lesen die gesamte Datei in den Speicher - wenn die Eingabedateien zu groß sind, können Sie dies tun:

$ cat tst.awk
BEGIN { FS=OFS="\t" }
{ printf "%s%s", (FNR>1 ? OFS : ""), $ARGIND }
ENDFILE {
    print ""
    if (ARGIND < NF) {
        ARGV[ARGC] = FILENAME
        ARGC++
    }
}
$ awk -f tst.awk file
X       row1    row2    row3    row4
column1 0       3       6       9
column2 1       4       7       10
column3 2       5       8       11

Das Programm verwendet fast keinen Speicher, liest aber die Eingabedatei einmal pro Anzahl der Felder in einer Zeile, so dass es wesentlich langsamer ist als die Version, die die gesamte Datei in den Speicher liest. Es nimmt auch an, dass die Anzahl der Felder in jeder Zeile gleich ist und verwendet GNU awk für ENDFILE und ARGIND aber jedes awk kann das gleiche mit Tests tun FNR==1 und END.


91
2017-11-13 15:34



Eine andere Option ist zu verwenden rs:

rs -c' ' -C' ' -T

-c ändert das Trennzeichen der Eingabespalte, -C ändert das Ausgabespaltentrennzeichen und -T transponiert Zeilen und Spalten. Verwende nicht -t Anstatt von -T, weil es eine automatisch berechnete Anzahl von Zeilen und Spalten verwendet, die normalerweise nicht korrekt ist. rs, die nach der Umformungsfunktion in APL benannt ist, wird mit BSDs und OS X geliefert, sollte aber von Paketmanagern auf anderen Plattformen verfügbar sein.

Eine zweite Option ist die Verwendung von Ruby:

ruby -e'puts readlines.map(&:split).transpose.map{|x|x*" "}'

Eine dritte Option ist zu verwenden jq:

jq -R .|jq -sr 'map(./" ")|transpose|map(join(" "))[]'

jq -R . druckt jede Eingabezeile als JSON-String-Literal, -s (--slurp) erstellt ein Array für die Eingabezeilen, nachdem jede Zeile als JSON und -r (--raw-output) gibt den Inhalt von Zeichenfolgen anstelle von JSON-Zeichenfolgenliteralen aus. Das / Operator ist überladen, um Strings zu teilen.


36
2018-05-11 17:28



Eine Python-Lösung:

python -c "import sys; print('\n'.join(' '.join(c) for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip()))))" < input > output

Das obige basiert auf Folgendem:

import sys

for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip())):
    print(' '.join(c))

Dieser Code geht davon aus, dass jede Zeile die gleiche Anzahl von Spalten hat (es wird kein Padding ausgeführt).


28
2017-11-13 17:21



das transponieren Projekt auf Sourceforge ist ein Coreutil-ähnliches C-Programm für genau das.

gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.

19
2018-02-08 17:36



Reine BASH, kein zusätzlicher Prozess. Eine schöne Übung:

declare -a array=( )                      # we build a 1-D-array

read -a line < "$1"                       # read the headline

COLS=${#line[@]}                          # save number of columns

index=0
while read -a line ; do
    for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do
        array[$index]=${line[$COUNTER]}
        ((index++))
    done
done < "$1"

for (( ROW = 0; ROW < COLS; ROW++ )); do
  for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do
    printf "%s\t" ${array[$COUNTER]}
  done
  printf "\n" 
done

14
2017-11-19 15:11



Hier ist ein mäßig solides Perl-Skript, um die Aufgabe zu erledigen. Es gibt viele strukturelle Analogien zu @ ghostdog74's awk Lösung.

#!/bin/perl -w
#
# SO 1729824

use strict;

my(%data);          # main storage
my($maxcol) = 0;
my($rownum) = 0;
while (<>)
{
    my(@row) = split /\s+/;
    my($colnum) = 0;
    foreach my $val (@row)
    {
        $data{$rownum}{$colnum++} = $val;
    }
    $rownum++;
    $maxcol = $colnum if $colnum > $maxcol;
}

my $maxrow = $rownum;
for (my $col = 0; $col < $maxcol; $col++)
{
    for (my $row = 0; $row < $maxrow; $row++)
    {
        printf "%s%s", ($row == 0) ? "" : "\t",
                defined $data{$row}{$col} ? $data{$row}{$col} : "";
    }
    print "\n";
}

Mit der Größe der Beispieldaten war der Leistungsunterschied zwischen Perl und Awk vernachlässigbar (1 Millisekunde von insgesamt 7 Millisekunden). Bei einem größeren Datensatz (Matrix 100 x 100, Einträge je 6-8 Zeichen) übertraf Perl geringfügig awk - 0,026s gegenüber 0,042s. Beides ist wahrscheinlich kein Problem.


Repräsentative Timings für Perl 5.10.1 (32-Bit) vs awk (Version 20040207 bei Angabe von '-V') vs Gawk 3.1.7 (32-Bit) auf MacOS X 10.5.8 auf eine Datei mit 10.000 Zeilen mit 5 Spalten pro Linie:

Osiris JL: time gawk -f tr.awk xxx  > /dev/null

real    0m0.367s
user    0m0.279s
sys 0m0.085s
Osiris JL: time perl -f transpose.pl xxx > /dev/null

real    0m0.138s
user    0m0.128s
sys 0m0.008s
Osiris JL: time awk -f tr.awk xxx  > /dev/null

real    0m1.891s
user    0m0.924s
sys 0m0.961s
Osiris-2 JL: 

Beachten Sie, dass Gawk auf diesem Rechner wesentlich schneller ist als awk, aber immer noch langsamer als Perl. Natürlich wird Ihre Laufleistung variieren.


9
2017-11-14 19:54



Wenn Sie haben sc installiert, können Sie Folgendes tun:

psc -r < inputfile | sc -W% - > outputfile

6
2017-11-13 16:54



Schau es dir an GNU-Datamash welches wie verwendet werden kann datamash transpose. Eine zukünftige Version unterstützt auch Kreuztabellen (Pivot-Tabellen)


6
2018-01-07 09:08



Angenommen, alle Zeilen haben die gleiche Anzahl von Feldern, löst dieses awk-Programm das Problem:

{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}

In Worten, wenn Sie die Zeilen für jedes Feld durchlaufen f Wachsen Sie eine ':' - getrennte Zeichenkette col[f] enthält die Elemente dieses Feldes. Wenn Sie mit allen Zeilen fertig sind, drucken Sie jeden dieser Strings in einer separaten Zeile. Sie können dann ":" für das gewünschte Trennzeichen (z. B. ein Leerzeichen) ersetzen, indem Sie die Ausgabe durchleiten tr ':' ' '.

Beispiel:

$ echo "1 2 3\n4 5 6"
1 2 3
4 5 6

$ echo "1 2 3\n4 5 6" | awk '{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}' | tr ':' ' '
 1 4
 2 5
 3 6

4
2018-06-10 17:57