Эффективный способ транспонирования файла в Bash

У меня есть огромный файл с разделителями разделов, отформатированный таким образом

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

Я хотел бы эффективно его транспонировать, используя только команды bash (я мог бы написать десять или около того строк Perl script для этого, но он должен быть медленнее для выполнения, чем собственные bash функции). Таким образом, результат должен выглядеть как

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

Я думал о таком решении

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

Но он медленный и не кажется наиболее эффективным решением. Я видел решение для vi в этом сообщении, но он все еще слишком медленный. Любые мысли/предложения/блестящие идеи?: -)

Ответ 1

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

Выход

$ 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

Производительность против решения Perl от Jonathan в файле с 10000 строками

$ 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 от Ed Morton (@ghostdog74 не стесняйтесь удалять, если вы отклоняете).

Возможно, эта версия с некоторыми более явными именами переменных поможет ответить на некоторые из приведенных ниже вопросов и в целом прояснить, что делает script. Он также использует вкладки в качестве разделителя, изначально запрошенный OP, чтобы он обрабатывал пустые поля, и он по совпадению преувеличивает вывод для этого конкретного случая.

$ 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

Вышеупомянутые решения будут работать в любом awk (кроме старого, сломанного awk, конечно, там YMMV).

Вышеупомянутые решения действительно читают весь файл в памяти - если входные файлы слишком велики для этого, вы можете сделать это:

$ 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

который почти не использует память, но считывает входной файл один раз за количество полей в строке, поэтому он будет намного медленнее, чем версия, которая считывает весь файл в память. Он также предполагает, что количество полей одинаково для каждой строки и использует GNU awk для ENDFILE и ARGIND, но любой awk может сделать то же самое с тестами на FNR==1 и END.

Ответ 2

Другой вариант - использовать rs:

rs -c' ' -C' ' -T

-c изменяет разделитель входных столбцов, -c изменяет разделитель выходных столбцов, а -T переносит строки и столбцы. Не используйте -T вместо -T, потому что он использует автоматически рассчитанное количество строк и столбцов, которое обычно не является правильным. rs, который назван в честь функции reshape в APL, поставляется с BSD и OS X, но он должен быть доступен менеджерам пакетов на других платформах.

Второй вариант - использовать Ruby:

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

Третий вариант - использовать jq:

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

jq -R . печатает каждую строку ввода как строковый литерал JSON, -s (--slurp) создает массив для входных строк после разбора каждой строки как JSON, а -r (--raw-output) выводит содержимое строк вместо строковых литералов JSON. Оператор / перегружен для разделения строк.

Ответ 3

Решение Python:

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

Вышеуказанное основано на следующем:

import sys

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

В этом коде предполагается, что каждая строка имеет одинаковое количество столбцов (никаких отступов не выполняется).

Ответ 4

transpose проект на sourceforge - это программа, подобная Coreutil, для этого.

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

Ответ 5

Чистый BASH, никакого дополнительного процесса. Хорошее упражнение:

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

Ответ 6

Посмотрите GNU datamash, который можно использовать как datamash transpose. Будущая версия также поддерживает перекрестные таблицы (сводные таблицы)

Ответ 7

Здесь выполняется умеренно твердый Perl script, чтобы выполнить эту работу. Существует много структурных аналогов с решением @ghostdog74 awk.

#!/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";
}

При размере данных выборки разница в производительности между perl и awk была незначительной (1 миллисекунда из 7 баллов). С большим набором данных (матрица 100x100, записи 6-8 символов каждая), perl немного превосходит awk - 0,026s против 0,042. Ни одна из них не может быть проблемой.


Типовые тайминги для Perl 5.10.1 (32-бит) vs awk (версия 20040207 при задании "-V" ) vs gawk 3.1.7 (32-разрядная версия) на MacOS X 10.5.8 на файл, содержащий 10 000 строк с 5 столбцов в строке:

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: 

Обратите внимание, что gawk намного быстрее, чем awk на этой машине, но все же медленнее, чем perl. Очевидно, что ваш пробег будет отличаться.

Ответ 8

Если у вас установлен sc, вы можете сделать:

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

Ответ 10

Предполагая, что все ваши строки имеют одинаковое количество полей, эта awk-программа решает проблему:

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

В словах, когда вы петляете по строкам, для каждого поля f выражаем строку с разделителем:: - col[f], содержащую элементы этого поля. После того как вы закончите со всеми строками, распечатайте каждую из этих строк в отдельной строке. Затем вы можете подставить ':' для разделителя, который вы хотите (скажем, пробел), путем подачи вывода через tr ':' ' '.

Пример:

$ 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

Ответ 11

Решение hackish perl может быть таким. Это хорошо, потому что он не загружает весь файл в память, печатает промежуточные временные файлы, а затем использует все замечательную пасту

#!/usr/bin/perl
use warnings;
use strict;

my $counter;
open INPUT, "<$ARGV[0]" or die ("Unable to open input file!");
while (my $line = <INPUT>) {
    chomp $line;
    my @array = split ("\t",$line);
    open OUTPUT, ">temp$." or die ("unable to open output file!");
    print OUTPUT join ("\n",@array);
    close OUTPUT;
    $counter=$.;
}
close INPUT;

# paste files together
my $execute = "paste ";
foreach (1..$counter) {
    $execute.="temp$counter ";
}
$execute.="> $ARGV[1]";
system $execute;

Ответ 12

Единственное улучшение, которое я вижу в вашем собственном примере, - это использование awk, которое уменьшит количество запущенных процессов и количество данных, которые передаются между ними:

/bin/rm output 2> /dev/null

cols=`head -n 1 input | wc -w` 
for (( i=1; i <= $cols; i++))
do
  awk '{printf ("%s%s", tab, $'$i'); tab="\t"} END {print ""}' input
done >> output

Ответ 13

Обычно я использую этот небольшой фрагмент awk для этого требования:

  awk '{for (i=1; i<=NF; i++) a[i,NR]=$i
        max=(max<NF?NF:max)}
        END {for (i=1; i<=max; i++)
              {for (j=1; j<=NR; j++) 
                  printf "%s%s", a[i,j], (j==NR?RS:FS)
              }
        }' file

Это просто загружает все данные в двумерный массив a[line,column], а затем печатает его как a[column,line], так что он переносит данный вход.

Для этого нужно отслеживать количество столбцов max imum, которое имеет исходный файл, так что оно используется как количество строк для печати. ​​

Ответ 14

GNU datamash идеально подходит для этой задачи, имея всего одну строку кода и потенциально произвольно большой размер файла!

datamash -W transpose infile > outfile

Ответ 15

Я использовал решение fgm (спасибо fgm!), но нужно было удалить символы табуляции в конце каждой строки, поэтому изменил script таким образом:

#!/bin/bash 
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" ${array[$COUNTER]}
    if [ $COUNTER -lt $(( ${#array[@]} - $COLS )) ]
    then
        printf "\t"
    fi
  done
  printf "\n" 
done

Ответ 16

Я просто искал аналогичный bash tranpose, но с поддержкой заполнения. Вот script Я написал на основе решения fgm, который, похоже, работает. Если это может помочь...

#!/bin/bash 
declare -a array=( )                      # we build a 1-D-array
declare -a ncols=( )                      # we build a 1-D-array containing number of elements of each row

SEPARATOR="\t";
PADDING="";
MAXROWS=0;
index=0
indexCol=0
while read -a line; do
    ncols[$indexCol]=${#line[@]};
((indexCol++))
if [ ${#line[@]} -gt ${MAXROWS} ]
    then
         MAXROWS=${#line[@]}
    fi    
    for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do
        array[$index]=${line[$COUNTER]}
        ((index++))

    done
done < "$1"

for (( ROW = 0; ROW < MAXROWS; ROW++ )); do
  COUNTER=$ROW;
  for (( indexCol=0; indexCol < ${#ncols[@]}; indexCol++ )); do
if [ $ROW -ge ${ncols[indexCol]} ]
    then
      printf $PADDING
    else
  printf "%s" ${array[$COUNTER]}
fi
if [ $((indexCol+1)) -lt ${#ncols[@]} ]
then
  printf $SEPARATOR
    fi
    COUNTER=$(( COUNTER + ncols[indexCol] ))
  done
  printf "\n" 
done

Ответ 17

Не очень элегантный, но эта "однострочная" команда быстро решает проблему:

cols=4; for((i=1;i<=$cols;i++)); do \
            awk '{print $'$i'}' input | tr '\n' ' '; echo; \
        done

Здесь cols - это количество столбцов, где вы можете заменить 4 на head -n 1 input | wc -w.

Ответ 18

Я искал решение для транспонирования любой матрицы (nxn или mxn) с любыми данными (числами или данными) и получил следующее решение:

Row2Trans=number1
Col2Trans=number2

for ((i=1; $i <= Line2Trans; i++));do
    for ((j=1; $j <=Col2Trans ; j++));do
        awk -v var1="$i" -v var2="$j" 'BEGIN { FS = "," }  ; NR==var1 {print $((var2)) }' $ARCHIVO >> Column_$i
    done
done

paste -d',' `ls -mv Column_* | sed 's/,//g'` >> $ARCHIVO

Ответ 19

Если вы хотите захватить единственную строку с разделителями-запятыми $N из файла и превратить ее в столбец:

head -$N file | tail -1 | tr ',' '\n'

Ответ 20

Еще одно решение awk и ограниченный ввод с размером памяти, который у вас есть.

awk '{ for (i=1; i<=NF; i++) RtoC[i]= (RtoC[i]? RtoC[i] FS $i: $i) }
    END{ for (i in RtoC) print RtoC[i] }' infile

Это объединяет все одинаковые позиции с позицией вместе и в END печатает результат, который будет первой строкой в первом столбце, второй строке во втором столбце и т.д. Будет выводиться:

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

Ответ 21

#!/bin/bash

aline="$(head -n 1 file.txt)"
set -- $aline
colNum=$#

#set -x
while read line; do
  set -- $line
  for i in $(seq $colNum); do
    eval col$i="\"\$col$i \$$i\""
  done
done < file.txt

for i in $(seq $colNum); do
  eval echo \${col$i}
done

другая версия с set eval

Ответ 22

Некоторые * nix стандартные утилиты one-liners, временные файлы не нужны. NB: ОП хотел эффективное исправление (т.е. быстрее), и лучшие ответы обычно быстрее, чем этот ответ. Эти однострочники предназначены для тех, кто любит программные инструменты * nix по тем или иным причинам. В редких случаях (например, дефицит ввода-вывода и памяти) эти фрагменты могут быть быстрее, чем некоторые из наиболее популярных ответов.

Назовите входной файл foo.

  1. Если мы знаем, что foo имеет четыре столбца:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
    
  2. Если мы не знаем, сколько столбцов имеет foo:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n) ; do cut -d ' ' -f $f foo | xargs echo ; done
    

    xargs имеет ограничение по размеру и поэтому может привести к неполной работе с длинным файлом. Какое ограничение размера зависит от системы, например:

    { timeout '.01' xargs --show-limits ; } 2>&1 | grep Max
    

    Максимальная длина команды, которую мы могли бы фактически использовать: 2088944

  3. tr & echo:

    for f in 1 2 3 4; do cut -d ' ' -f $f foo | tr '\n\ ' ' ; echo; done
    

    ... или если число столбцов неизвестно:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
    
  4. Использование set, которое подобно xargs, имеет аналогичные ограничения на размер командной строки:

    for f in 1 2 3 4 ; do set - $(cut -d ' ' -f $f foo) ; echo [email protected] ; done
    

Ответ 23

Здесь решение Хаскелла. Когда скомпилирован с -O2, он работает немного быстрее, чем ghostdog awk и немного медленнее, чем Stephan тонко завернутый c python на моей машине для повторных строк ввода "Hello world". К сожалению, поддержка GHC для передачи кода командной строки не существует, насколько я могу судить, поэтому вам придется записывать ее в файл самостоятельно. Он усекает строки до длины самой короткой строки.

transpose :: [[a]] -> [[a]]
transpose = foldr (zipWith (:)) (repeat [])

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines

Ответ 24

Ниже приведен один paste -связывая их вместе:

echo '' > tmp1;  \
cat m.txt | while read l ; \
            do    paste tmp1 <(echo $l | tr -s ' ' \\n) > tmp2; \
                  cp tmp2 tmp1; \
            done; \
cat tmp1

m.txt:

0 1 2
4 5 6
7 8 9
10 11 12
  • создает файл tmp1, чтобы он не был пустым.

  • читает каждую строку и преобразует ее в столбец с помощью tr

  • вставляет новый столбец в файл tmp1

  • копирует результат обратно в tmp1.

PS: Я действительно хотел использовать io-дескрипторы, но не мог заставить их работать.

Ответ 25

Решение awk, которое хранит весь массив в памяти

    awk '$0!~/^$/{    i++;
                  split($0,arr,FS);
                  for (j in arr) {
                      out[i,j]=arr[j];
                      if (maxr<j){ maxr=j}     # max number of output rows.
                  }
            }
    END {
        maxc=i                 # max number of output columns.
        for     (j=1; j<=maxr; j++) {
            for (i=1; i<=maxc; i++) {
                printf( "%s:", out[i,j])
            }
            printf( "%s\n","" )
        }
    }' infile

Но мы можем "ходить" по файлу столько раз, сколько нужны выходные строки:

#!/bin/bash
maxf="$(awk '{if (mf<NF); mf=NF}; END{print mf}' infile)"
rowcount=maxf
for (( i=1; i<=rowcount; i++ )); do
    awk -v i="$i" -F " " '{printf("%s\t ", $i)}' infile
    echo
done

Какой (для низкого количества выходных строк быстрее, чем предыдущий код).