Эффективный способ транспонирования файла в Bash
У меня есть огромный файл с разделителями разделов, отформатированный таким образом
X column1 column2 column3
row1 0 1 2
row2 3 4 5
row3 6 7 8
row4 9 10 11
Я хотел бы эффективно его транспонировать, используя только команды bash (я мог бы написать десять или около того строк Perl script для этого, но он должен быть медленнее для выполнения, чем собственные 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 в этом сообщении, но он все еще слишком медленный. Любые мысли/предложения/блестящие идеи?: -)
Ответы
Ответ 1
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 от Jonathan в файле с 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 от Ed Morton (@ghostdog74 не стесняйтесь удалять, если вы отклоняете).
Возможно, эта версия с некоторыми более явными именами переменных поможет ответить на некоторые из приведенных ниже вопросов и в целом прояснить, что делает script. Он также использует вкладки в качестве разделителя, изначально запрошенный 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
.
Ответ 2
Другой вариант - использовать 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. Оператор /
перегружен для разделения строк.
Ответ 3
Решение 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))
В этом коде предполагается, что каждая строка имеет одинаковое количество столбцов (никаких отступов не выполняется).
Ответ 4
transpose проект на sourceforge - это программа, подобная Coreutil, для этого.
gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.
Ответ 5
Чистый 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
Ответ 6
Посмотрите GNU datamash, который можно использовать как datamash transpose
.
Будущая версия также поддерживает перекрестные таблицы (сводные таблицы)
Ответ 7
Здесь выполняется умеренно твердый Perl script, чтобы выполнить эту работу. Существует много структурных аналогов с решением @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,026s против 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 столбцов в строке:
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. Очевидно, что ваш пробег будет отличаться.
Ответ 8
Если у вас установлен sc
, вы можете сделать:
psc -r < inputfile | sc -W% - > outputfile
Ответ 9
Для этого есть специальная утилита,
Утилита 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
Ответ 10
Предполагая, что все ваши строки имеют одинаковое количество полей, эта 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
Ответ 11
Решение hackish 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;
Ответ 12
Единственное улучшение, которое я вижу в вашем собственном примере, - это использование 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
Ответ 13
Обычно я использую этот небольшой фрагмент 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
imum, которое имеет исходный файл, так что оно используется как количество строк для печати.
Ответ 14
GNU datamash идеально подходит для этой задачи, имея всего одну строку кода и потенциально произвольно большой размер файла!
datamash -W transpose infile > outfile
Ответ 15
Я использовал решение fgm (спасибо fgm!), но нужно было удалить символы табуляции в конце каждой строки, поэтому изменил script таким образом:
#!/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
Ответ 16
Я просто искал аналогичный bash tranpose, но с поддержкой заполнения. Вот script Я написал на основе решения 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
Ответ 17
Не очень элегантный, но эта "однострочная" команда быстро решает проблему:
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
.
Ответ 18
Я искал решение для транспонирования любой матрицы (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
Ответ 19
Если вы хотите захватить единственную строку с разделителями-запятыми $N из файла и превратить ее в столбец:
head -$N file | tail -1 | tr ',' '\n'
Ответ 20
Еще одно решение 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
Это объединяет все одинаковые позиции с позицией вместе и в END
печатает результат, который будет первой строкой в первом столбце, второй строке во втором столбце и т.д. Будет выводиться:
X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11
Ответ 21
#!/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
Ответ 22
Некоторые * nix стандартные утилиты one-liners, временные файлы не нужны. 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 [email protected] ; done
Ответ 23
Здесь решение Хаскелла. Когда скомпилирован с -O2, он работает немного быстрее, чем ghostdog awk и немного медленнее, чем Stephan тонко завернутый c python на моей машине для повторных строк ввода "Hello world". К сожалению, поддержка GHC для передачи кода командной строки не существует, насколько я могу судить, поэтому вам придется записывать ее в файл самостоятельно. Он усекает строки до длины самой короткой строки.
transpose :: [[a]] -> [[a]]
transpose = foldr (zipWith (:)) (repeat [])
main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines
Ответ 24
Ниже приведен один 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-дескрипторы, но не мог заставить их работать.
Ответ 25
Решение 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
Какой (для низкого количества выходных строк быстрее, чем предыдущий код).