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


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

533   25  

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 не стесняйтесь удалять, если вы не одобряете).

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

$ 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

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

утилита 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

единственное улучшение, которое я вижу в вашем собственном примере, - это использование 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 тонко обернутый 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 '!~/^$/{    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 и память), эти фрагменты могут быть на самом деле быстрее.

звонок входной файл фу.

  1. если мы знаем!--12-->фу имеет четыре колонки:

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

    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
    

вот 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 tmp1

m.txt:

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

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

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

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

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

GNU datamash (https://www.gnu.org/software/datamash) идеально подходит для этой проблемы только с одной строкой кода и потенциально сколь угодно большим размером файла! datamash-W транспонировать input_file.txt > input_file_transposed.txt

Comments

    Ничего не найдено.