Эффективный способ транспонирования файла в Bash
У меня есть огромный разделенный вкладками файл, отформатированный так
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 в этом post , но все равно слишком медленно. Какие-нибудь мысли/предложения/блестящие идеи? :- )25 ответов:
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
EDIT by Ed Morton (@ghostdog74 не стесняйтесь удалять, если вы не одобряете).
Возможно, эта версия с некоторыми более явными именами переменных поможет ответить на некоторые вопросы ниже и в целом прояснить, что делает скрипт. Он также использует вкладки в качестве разделителя, который изначально просил ОП, чтобы он обрабатывал пустые поля, и это случайно приукрашивает выход немного для этого конкретного случая.
Вышеперечисленные решения будут работать в любом awk (кроме старого, сломанного awk конечно - там YMMV).$ 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
Вышеприведенные решения действительно считывают весь файл в память - если входные файлы слишком велики для этого, то вы можете сделать следующее:
$ 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
.
Другой вариант-использовать
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. Оператор/
перегружен для разделения строк.
Решение 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))
Этот код предполагает, что каждая строка содержит одинаковое количество столбцов (заполнение не выполняется).
Проектtranspose на sourceforge является программой coreutil-like C именно для этого.
gcc transpose.c -o transpose ./transpose -t input > output #works with stdin, too.
Чистый Баш, никакого дополнительного процесса. Хорошее упражнение:
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
Вот умеренно солидный Perl-скрипт для выполнения этой работы. Существует много структурных аналогий с решением @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,026 с против 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 столбцами в строке:
Обратите внимание, что gawk значительно быстрее awk на этой машине, но все же медленнее, чем perl. Очевидно, что ваш пробег будет отличаться.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:
Посмотрите на GNU datamash, который можно использовать как
datamash transpose
. Будущая версия также будет поддерживать перекрестную табуляцию (сводные таблицы)
Если у вас есть
sc
установленный, вы можете сделать:psc -r < inputfile | sc -W% - > outputfile
Предполагая, что все ваши строки имеют одинаковое количество полей, эта программа 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
Для этого есть специально построенная утилита,
apt install datamash datamash transpose < yourfile
Взято с этого сайта, https://www.gnu.org/software/datamash/ и http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods
Единственное улучшение, которое я могу видеть в вашем собственном примере, - это использование 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
Решение программистские 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;
Я использовал решение fgm (спасибо fgm!), но необходимо было исключить символы табуляции в конце каждой строки, поэтому модифицировали скрипт следующим образом:
#!/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
Я просто искал похожий bash tranpose, но с поддержкой padding. Вот сценарий, который я написал на основе решения 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
Я искал решение для транспонирования любого вида матрицы (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
Обычно я использую этот маленький фрагмент
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
количество столбцов в исходном файле, так что он используется в качестве количества строк для печати обратно.
Если вы хотите извлечь из файла только одну строку $N (через запятую) и превратить ее в столбец:
head -$N file | tail -1 | tr ',' '\n'
Не очень изящно, но эта "однострочная" команда быстро решает проблему:
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
.
#!/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
Другое
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
Это соединяет каждый такой же поданный номер positon в совместно и в
END
печатает результат который был бы первой строкой в первом столбце, второй строкой во втором столбце, etc. Выведет:X row1 row2 row3 row4 column1 0 3 6 9 column2 1 4 7 10 column3 2 5 8 11
Вот решение Хаскелла. При компиляции с-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
Решение 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
Который (для низкого количества выходных строк быстрее, чем предыдущий код).
Некоторые * Nix стандартные util однострочные, временные файлы не нужны. NB: ОП хотел эффективное исправление (т. е. быстрее), и верхние ответы обычно быстрее. Эти однострочные предназначены для тех, кто любит *Nix "программные средства", по каким-либо причинам. В редких случаях (например, при дефиците ввода-вывода и памяти) эти фрагменты могут быть на самом деле быстрее.
Вызовите входной файл foo .
Если мы знаем, что foo имеет четыре столбцы:
for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
Если мы не знаем, сколько столбцов 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
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
Используя
set
, которые любятxargs
, имеет аналогичные ограничения на размер командной строки:for f in 1 2 3 4 ; do set - $(cut -d ' ' -f $f foo) ; echo $@ ; done
Вот Bash one-liner, который основан на простом преобразовании каждой строки в столбец и
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-дескрипторы, но не смог их получить. работа.
GNU datamash (https://www.gnu.org/software/datamash ) идеально подходит для этой задачи только с одной строкой кода и потенциально сколь угодно большим размером файла! datamash-W транспонировать input_file.тхт > input_file_transposed.txt