Эффективный способ транспонировать файл в 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 ]

1 голос
/ 10 апреля 2016

Некоторые * nix стандартные утилиты one-liners, временные файлы не требуются. NB: OP хотел эффективное исправление (то есть быстрее), и ответы на первые вопросы обычно быстрее, чем этот ответ. Эти однострочники предназначены для тех, кому нравится * 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 $@ ; done
    
1 голос
/ 19 августа 2015
#!/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

0 голосов
/ 26 августа 2014

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

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

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines
0 голосов
/ 07 декабря 2014

Вот строковая строка в Bash, основанная на простом преобразовании каждой строки в столбец и 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
  1. создает tmp1 файл, поэтому он не пустой.

  2. читает каждую строку и преобразует ее в столбец, используя tr

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

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

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

0 голосов
/ 29 января 2016

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

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

...