Эффективный способ транспонировать файл в Bash - PullRequest
102 голосов
/ 13 ноября 2009

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

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

Я хотел бы транспонировать эффективным способом, используя только команды bash (для этого я мог бы написать Perl-скрипт длиной около десяти строк, но он должен выполняться медленнее, чем встроенные функции 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 в этом посте , но оно все еще слишком медленное. Есть мысли / предложения / блестящие идеи? : -)

Ответы [ 25 ]

105 голосов
/ 13 ноября 2009
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 Джонатана для файла 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

РЕДАКТИРОВАТЬ Эд Мортон (@ ghostdog74 не стесняйтесь удалять, если вы не одобряете).

Может быть, эта версия с некоторыми более явными именами переменных поможет ответить на некоторые из приведенных ниже вопросов и в целом прояснить, что делает скрипт. Он также использует вкладки в качестве разделителя, который первоначально запрашивал 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.

42 голосов
/ 11 мая 2015

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

rs -c' ' -C' ' -T

-c изменяет разделитель входных столбцов, -C изменяет разделитель выходных столбцов, а -T транспонирует строки и столбцы. Не используйте -t вместо -T, поскольку в нем используется автоматически рассчитанное количество строк и столбцов, которое обычно не является правильным. rs, который назван в честь функции изменения формы в 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. Оператор / перегружен для разделения строк.

30 голосов
/ 13 ноября 2009

Решение 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))

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

20 голосов
/ 08 февраля 2013

проект transpose на sourceforge - это программа на языке C, похожая на coreutil.

gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.
15 голосов
/ 19 ноября 2009

Чистый 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
14 голосов
/ 07 января 2016

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

9 голосов
/ 14 ноября 2009

Вот умеренно солидный Perl-скрипт для выполнения этой работы. Существует множество структурных аналогий с решением awk @ ghostdog74.

#!/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.026 с против 0.042 с. Вероятно, ни одна из них не будет проблемой.


Типичные временные характеристики для Perl 5.10.1 (32-разрядная версия) против awk (версия 20040207 при наличии '-V') против gawk 3.1.7 (32-разрядная версия) в MacOS X 10.5.8 для файла, содержащего 10000 строк 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. Очевидно, ваш пробег будет отличаться.

6 голосов
/ 13 ноября 2009

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

psc -r < inputfile | sc -W% - > outputfile
6 голосов
/ 07 апреля 2017

Для этого есть специальная утилита,

Утилита GNU datamash

apt install datamash  

datamash transpose < yourfile

взято с этого сайта, https://www.gnu.org/software/datamash/ и http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods

5 голосов
/ 10 июня 2015

Предполагая, что все ваши строки имеют одинаковое количество полей, эта 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
...