Эффективный способ транспонирования файла в 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
но это медленно и не кажется самым эффективным решением. Я видел решение для ви этот пост, но это все еще слишком медленно. Любые мысли/предложения/идеи? : -)
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.010sEDIT by Ed Morton (@ghostdog74 не стесняйтесь удалять, если вы не одобряете).
возможно, эта версия с некоторыми более явными именами переменных поможет ответить на некоторые из приведенных ниже вопросов и в целом прояснить, что делает скрипт. Он также использует вкладки в качестве разделителя, который ОП изначально просил, чтобы он обрабатывал пустые поля и его по совпадению прихорашивает выход немного для этого конкретного случая.
$ 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.
другой вариант-использовать
rs:rs -c' ' -C' ' -T
-cизменяет разделитель входных столбцов,-Cизменяет разделитель выходных столбцов и-Tперемещение строк и столбцов. Не используйте-tвместо-T, потому что он использует автоматически вычисляется количество строк и столбцов обычно не правильно.rs, который назван в честь функции reshape в APL, поставляется с BSDs и 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))этот код предполагает, что каждая строка имеет одинаковое количество столбцов (без заполнения выполняется).
the транспонировать проект на sourceforge-это программа coreutil-like C именно для этого.
gcc transpose.c -o transpose ./transpose -t input > output #works with stdin, too.
Pure BASH, без дополнительного процесса. Хорошее упражнение:
declare -a array=( ) # we build a 1-D-array read -a line < "" # 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 < "" 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 s против 0.042 s. ни один из них, вероятно, не будет проблемой.
представитель тайминги 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. Очевидно, что ваш пробег будет отличаться.
посмотреть 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 < "" # 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 < "" 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, но с поддержкой заполнения. Вот сценарий, который я написал на основе решения 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 < "" 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], Так что он транспонирует данный ввод.это нужно отслеживать
maximum количество столбцов в исходном файле, так что он используется в качестве количества строк для обратной печати.
Если вы хотите только захватить одну (разделенную запятыми) строку $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другая версия с
seteval
еще один
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 в together и in
ENDвыводит результат, который будет первой строкой в первом столбце, второй строкой во втором столбце и т. д. Будет вывод:X row1 row2 row3 row4 column1 0 3 6 9 column2 1 4 7 10 column3 2 5 8 11
вот решение Haskell. При компиляции с-O2 он работает немного быстрее, чем awk ghostdog, и немного медленнее, чем Stephan
тонко обернутый cpython на моей машине для повторных строк ввода" Hello world". К сожалению, поддержка GHC для передачи кода командной строки не существует, насколько я могу судить, поэтому вам придется написать его в файл самостоятельно. Он будет усекать строки до длины самой короткой строки.transpose :: [[a]] -> [[a]] transpose = foldr (zipWith (:)) (repeat []) main :: IO () main = interact $ unlines . map unwords . transpose . map words . lines
решение awk, которое хранит весь массив в памяти
awk '!~/^$/{ i++; split(,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 "программные средства", по каким-либо причинам. В редкая случаи (например, скудный IO и память), эти фрагменты могут быть на самом деле быстрее.
звонок входной файл фу.
если мы знаем!--12-->фу имеет четыре колонки:
for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; doneесли мы не знаем, сколько столбцов фу есть:
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- ing их вместе:echo '' > tmp1; \ cat m.txt | while read l ; \ do paste tmp1 <(echo $l | tr -s ' ' \n) > tmp2; \ cp tmp2 tmp1; \ done; \ cat tmp1m.txt:
0 1 2 4 5 6 7 8 9 10 11 12
создает
tmp1файл, так что это не пустой.читает каждую строку и преобразует ее в столбец с помощью
trвставляет новый столбец в
tmp1fileкопирует результат обратно в
tmp1.PS: Я действительно хотел использовать io-дескрипторы, но не мог заставить их работать.
GNU datamash (https://www.gnu.org/software/datamash) идеально подходит для этой проблемы только с одной строкой кода и потенциально сколь угодно большим размером файла! datamash-W транспонировать input_file.txt > input_file_transposed.txt
Comments