Một cách hiệu quả để chuyển đổi tệp trong Bash


110

Tôi có một tệp lớn được phân tách bằng tab được định dạng như thế này

X column1 column2 column3
row1 0 1 2
row2 3 4 5
row3 6 7 8
row4 9 10 11

Tôi muốn chuyển nó theo cách hiệu quả chỉ bằng cách sử dụng các lệnh bash (tôi có thể viết một tập lệnh Perl mười dòng hoặc lâu hơn để làm điều đó, nhưng nó sẽ chậm hơn để thực thi so với các hàm bash gốc). Vì vậy, đầu ra sẽ giống như

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11

Tôi đã nghĩ ra một giải pháp như thế này

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

Nhưng nó chậm và có vẻ không phải là giải pháp hiệu quả nhất. Tôi đã thấy một giải pháp cho vi trong bài đăng này , nhưng nó vẫn quá chậm. Bất kỳ suy nghĩ / đề xuất / ý tưởng tuyệt vời? :-)


12
Điều gì khiến bạn nghĩ rằng sẽ tồn tại một tập lệnh bash sẽ nhanh hơn tập lệnh Perl? Đây chính là loại vấn đề mà trội Perl trong.
Đánh dấu Pim

1
@mark, nếu bash thuần túy của nó, nó có thể nhanh hơn việc xâu chuỗi tất cả các công cụ cắt / sed vv lại với nhau. Nhưng một lần nữa, nếu bạn định nghĩa "bash" như trong việc kết hợp các công cụ, thì chỉ cần viết một tập lệnh awk sẽ có thể so sánh với xử lý văn bản Perl wrt.
ghostdog 74

Thêm một cái khác vì không hiểu perl sẽ chậm như thế nào ở đây. Chậm để viết mã? Chậm để thực hiện? Tôi thực sự không thích perl, nhưng nó thực sự xuất sắc trong loại nhiệm vụ này.
Corey Porter

Nếu các cột / trường của bạn có kích thước / chiều rộng cố định, thì bạn có thể sử dụng tìm kiếm tệp Python để tránh đọc tệp của mình vào bộ nhớ. Bạn có cố định kích thước / chiều rộng cột / trường không?
tommy.carstensen

2
Bất kỳ ai nghĩ rằng một tập lệnh shell sẽ nhanh hơn awk hoặc perl cần phải đọc unix.stackexchange.com/questions/169716/… để họ có thể hiểu tại sao không phải như vậy.
Ed Morton

Câu trả lời:


114
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

đầu ra

$ 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

Hiệu suất chống lại giải pháp Perl của Jonathan trên tệp 10000 dòng

$ 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

CHỈNH SỬA bởi Ed Morton (@ ghostdog74 vui lòng xóa nếu bạn không đồng ý).

Có thể phiên bản này với một số tên biến rõ ràng hơn sẽ giúp trả lời một số câu hỏi bên dưới và nói chung làm rõ tập lệnh đang làm gì. Nó cũng sử dụng các tab làm dấu phân cách mà OP đã yêu cầu ban đầu để nó xử lý các trường trống và nó ngẫu nhiên cải tiến đầu ra một chút cho trường hợp cụ thể này.

$ 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

Các giải pháp trên sẽ hoạt động trong bất kỳ awk nào (tất nhiên là ngoại trừ awk cũ, bị hỏng - có YMMV).

Tuy nhiên, các giải pháp trên có thể đọc toàn bộ tệp vào bộ nhớ - nếu các tệp đầu vào quá lớn thì bạn có thể thực hiện việc này:

$ 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

mà hầu như không sử dụng bộ nhớ nhưng đọc tệp đầu vào một lần cho mỗi số trường trên một dòng nên sẽ chậm hơn nhiều so với phiên bản đọc toàn bộ tệp vào bộ nhớ. Nó cũng giả định rằng số lượng trường là như nhau trên mỗi dòng và nó sử dụng GNU awk cho ENDFILEARGINDnhưng bất kỳ awk nào cũng có thể làm như vậy với các bài kiểm tra trên FNR==1END.


Và bây giờ để xử lý nhãn hàng và cột nữa?
Jonathan Leffler

OK - bạn đúng; dữ liệu mẫu của bạn không khớp với dữ liệu mẫu của câu hỏi, nhưng mã của bạn hoạt động tốt trên dữ liệu mẫu của câu hỏi và cung cấp kết quả đầu ra cần thiết (cho hoặc lấy trống so với khoảng cách tab). Chủ yếu là lỗi của tôi.
Jonathan Leffler

Thời gian thú vị - Tôi đồng ý rằng bạn thấy lợi ích về hiệu suất trong awk. Tôi đang sử dụng MacOS X 10.5.8, không sử dụng 'gawk'; và tôi đang sử dụng Perl 5.10.1 (bản dựng 32-bit). Tôi thu thập được rằng dữ liệu của bạn là 10000 dòng với 4 cột mỗi dòng? Dù sao, nó không phải là vấn đề lớn; cả awk và perl đều là các giải pháp khả thi (và giải pháp awk thì gọn gàng hơn - các kiểm tra 'được xác định' trong Perl của tôi là cần thiết để cảnh báo các lần chạy miễn phí dưới các cảnh báo / nghiêm ngặt) và cả hai đều không phải là một cách nhanh hơn ban đầu giải pháp shell script.
Jonathan Leffler

Trên ma trận 2.2GB ban đầu của tôi, giải pháp perl là nhanh hơn một chút so với awk - 350.103s vs 369.410s Tôi đã sử dụng perl 5.8.8 64bit
Federico Giorgi

1
@ zx8754 rằng số lượng trường tối đa chỉ áp dụng cho awk cũ, không phải POSIX. Có thể cái tên cực kỳ đáng tiếc là "nawk". Nó không áp dụng cho gawk hoặc awk hiện đại khác.
Ed Morton

47

Một tùy chọn khác là sử dụng rs:

rs -c' ' -C' ' -T

-cthay đổi dấu phân tách cột đầu vào, -Cthay đổi dấu phân tách cột đầu ra và -Thoán vị các hàng và cột. Không sử dụng -tthay thế -Tvì nó sử dụng số lượng hàng và cột được tính toán tự động thường không chính xác. rs, được đặt tên theo chức năng định hình lại trong APL, đi kèm với BSD và OS X, nhưng nó sẽ có sẵn từ các trình quản lý gói trên các nền tảng khác.

Tùy chọn thứ hai là sử dụng Ruby:

ruby -e'puts readlines.map(&:split).transpose.map{|x|x*" "}'

Tùy chọn thứ ba là sử dụng jq:

jq -R .|jq -sr 'map(./" ")|transpose|map(join(" "))[]'

jq -R .in từng dòng đầu vào dưới dạng ký tự chuỗi JSON, -s( --slurp) tạo một mảng cho các dòng đầu vào sau khi phân tích cú pháp mỗi dòng dưới dạng JSON và -r( --raw-output) xuất nội dung của chuỗi thay vì ký tự chuỗi JSON. Các /nhà điều hành được nạp chồng để chuỗi phân chia.


3
Tôi không quen thuộc rs- cảm ơn vì con trỏ! (Liên kết là đến Debian; ngược dòng có vẻ là mirbsd.org/MirOS/dist/mir/rs )
tripleee

2
@lalebarde Ít nhất trong quá trình triển khai rsđiều đó đi kèm với OS X, -cmột mình đặt dấu phân tách cột đầu vào thành một tab.
nisetama

2
@lalebarde, thử bash của ANSI-C trích dẫn để có được một ký tự tab:$'\t'
glenn Jackman

3
Đây là một trường hợp cực đoan, nhưng đối với một tệp rất lớn với nhiều hàng như TTC TTA TTC TTC TTT, chạy rs -c' ' -C' ' -T < rows.seq > cols.seqcho phép rs: no memory: Cannot allocate memory. Đây là hệ thống chạy FreeBSD 11.0-RELEASE với 32 GB ram. Vì vậy, tôi đoán là rsđặt mọi thứ vào RAM, tốt cho tốc độ, nhưng không tốt cho dữ liệu lớn.
jrm

1
jq đã sử dụng 21Gb ram trên một tệp 766MB. Tôi đã giết nó sau 40 phút mà không có bất kỳ đầu ra nào.
Glubbdrubb

30

Một giải pháp 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

Trên đây là dựa trên những điều sau:

import sys

for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip())):
    print(' '.join(c))

Mã này giả định rằng mọi dòng có cùng số cột (không có phần đệm được thực hiện).


3
Một vấn đề nhỏ ở đây: Thay thế l.split()bằng l.strip().split()(Python 2.7), nếu không dòng cuối cùng của đầu ra bị tê liệt. Hoạt động cho các dấu phân tách cột tùy ý, sử dụng l.strip().split(sep)sep.join(c)nếu dấu phân tách của bạn được lưu trữ trong biến sep.
krlmlr

21

các transpose dự án trên SourceForge là một giống như coreutil C chương trình cho chính xác điều đó.

gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.

Cảm ơn các liên kết. Tuy nhiên, nó đòi hỏi quá nhiều bộ nhớ khi xử lý các ma trận / tệp lớn.
tommy.carstensen

nó có các đối số cho kích thước khối và kích thước trường: hãy thử điều chỉnh các đối số -b-f.
bay cừu

Kích thước khối mặc định (--block hoặc -b) là 10kb và kích thước trường mặc định (--fieldmax hoặc -f) là 64, vì vậy không thể có được. Tôi đã thử. Nhờ đề nghị mặc dù.
tommy.carstensen

1
Hoạt động tốt với csv có kích thước 2 GB.
discipulus

2
Đối với tệp ma trận có kích thước khoảng 11k x 5k, tôi thấy transpose.c nhanh hơn ~ 7 lần và tiết kiệm bộ nhớ hơn ~ 5x so với giải pháp awk đầu tiên của ghostdog74. Ngoài ra, tôi thấy rằng mã awk "sử dụng hầu như không có bộ nhớ" từ ghostdog74 không hoạt động bình thường. Ngoài ra, hãy chú ý đến cờ --limit trong chương trình transpose.c, theo mặc định, nó giới hạn đầu ra cho thứ nguyên 1k x 1k.
ncemami

16

BASH nguyên chất, không có quy trình bổ sung. Một bài tập hay:

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

Điều này đã làm việc cho tệp của tôi, mặc dù thú vị là nó in ra danh sách thư mục cho dòng đầu tiên của bảng. Tôi không biết đủ BASH để tìm ra lý do tại sao.
bugloaf

@bugloaf bảng của bạn có dấu * ở góc.
Xin chào 71

2
@bugloaf: Trích dẫn các biến đúng cách sẽ ngăn chặn điều đó:printf "%s\t" "${array[$COUNTER]}"
Tạm dừng cho đến khi có thông báo mới.

16

Hãy xem GNU datamash có thể được sử dụng như thế nào datamash transpose. Một phiên bản trong tương lai cũng sẽ hỗ trợ lập bảng chéo (bảng tổng hợp)


9

Đây là một tập lệnh Perl vừa phải chắc chắn để thực hiện công việc. Có nhiều phép tương tự về cấu trúc với awkgiải pháp của @ ghostdog74 .

#!/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";
}

Với kích thước dữ liệu mẫu, sự khác biệt về hiệu suất giữa perl và awk là không đáng kể (1 mili giây trên tổng số 7). Với tập dữ liệu lớn hơn (ma trận 100x100, mỗi mục nhập 6-8 ký tự), perl có phần nhỉnh hơn awk - 0,026 giây so với 0,042 giây. Không có khả năng là một vấn đề.


Thời gian đại diện cho Perl 5.10.1 (32-bit) so với awk (phiên bản 20040207 khi được cung cấp '-V') so với gawk 3.1.7 (32-bit) trên MacOS X 10.5.8 trên tệp chứa 10.000 dòng với 5 cột mỗi hàng:

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: 

Lưu ý rằng gawk nhanh hơn awk trên máy này rất nhiều, nhưng vẫn chậm hơn perl. Rõ ràng, số dặm của bạn sẽ khác nhau.


trên hệ thống của tôi, gawk hoạt động tốt hơn perl. bạn có thể xem kết quả của tôi trong bài đăng đã chỉnh sửa của tôi
ghostdog74

4
kết luận thu thập được: nền tảng khác nhau, phiên bản phần mềm khác nhau, kết quả khác nhau.
ghostdog 74

6

Nếu bạn đã sccài đặt, bạn có thể làm:

psc -r < inputfile | sc -W% - > outputfile

4
Lưu ý rằng điều này hỗ trợ một số dòng giới hạn vì scđặt tên các cột của nó là một hoặc kết hợp của hai ký tự. Giới hạn là 26 + 26^2 = 702.
Thor


5

Giả sử tất cả các hàng của bạn có cùng số trường, chương trình awk này giải quyết vấn đề:

{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}

Nói cách khác, khi bạn lặp qua các hàng, đối với mỗi trường fsẽ phát triển một ':' - chuỗi được phân tách col[f]chứa các phần tử của trường đó. Sau khi bạn hoàn thành tất cả các hàng, hãy in từng chuỗi đó trong một dòng riêng biệt. Sau đó, bạn có thể thay thế ':' cho dấu phân cách bạn muốn (giả sử, một dấu cách) bằng cách chuyển đầu ra qua tr ':' ' '.

Thí dụ:

$ 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

5

GNU datamash hoàn toàn phù hợp cho vấn đề này chỉ với một dòng mã và kích thước tệp có thể lớn tùy ý!

datamash -W transpose infile > outfile

3

Một giải pháp hackish perl có thể như thế này. Thật tuyệt vì nó không tải tất cả tệp trong bộ nhớ, in các tệp tạm thời trung gian và sau đó sử dụng cách dán tuyệt vời

#!/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;

sử dụng các tệp dán và tạm thời chỉ là các thao tác bổ sung không cần thiết. bạn chỉ có thể làm thao tác bên trong bộ nhớ riêng của mình, ví dụ như mảng / băm
ghostdog74

2
Đúng, nhưng điều đó không có nghĩa là giữ mọi thứ trong bộ nhớ? Các tệp tôi đang xử lý có kích thước khoảng 2-20gb.
Federico Giorgi,

3

Cải tiến duy nhất mà tôi có thể thấy đối với ví dụ của riêng bạn là sử dụng awk, điều này sẽ giảm số lượng quy trình được chạy và lượng dữ liệu được ghép nối giữa chúng:

/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

3

Tôi thường sử dụng awkđoạn mã nhỏ này cho yêu cầu này:

  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

Thao tác này chỉ tải tất cả dữ liệu vào một mảng bidimensional a[line,column]và sau đó in nó trở lại a[column,line], để nó chuyển đổi đầu vào đã cho.

Điều này cần theo dõi maxsố lượng cột mà tệp ban đầu có, để nó được sử dụng làm số hàng cần in trở lại.


2

Tôi đã sử dụng giải pháp của fgm (cảm ơn fgm!), Nhưng cần phải loại bỏ các ký tự tab ở cuối mỗi hàng, vì vậy đã sửa đổi tập lệnh do đó:

#!/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

2

Tôi chỉ đang tìm kiếm tranpose bash tương tự nhưng có hỗ trợ đệm. Đây là kịch bản tôi đã viết dựa trên giải pháp của fgm, có vẻ như hoạt động. Nếu nó có thể giúp được ...

#!/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

2

Tôi đang tìm giải pháp để hoán vị bất kỳ loại ma trận nào (nxn hoặc mxn) với bất kỳ loại dữ liệu nào (số hoặc dữ liệu) và nhận được giải pháp sau:

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

2

Nếu bạn chỉ muốn lấy một dòng duy nhất (được phân cách bằng dấu phẩy) $ N ra khỏi tệp và biến nó thành một cột:

head -$N file | tail -1 | tr ',' '\n'

2

Không thanh lịch lắm, nhưng lệnh "một dòng" này giải quyết vấn đề một cách nhanh chóng:

cols=4; for((i=1;i<=$cols;i++)); do \
            awk '{print $'$i'}' input | tr '\n' ' '; echo; \
        done

Ở đây cols là số cột, nơi bạn có thể thay thế 4 bằng head -n 1 input | wc -w.


2

Một awkgiải pháp khác và đầu vào hạn chế với dung lượng bộ nhớ bạn có.

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

Điều này nối từng positon số đã được đệ trình lại với nhau và ENDin ra kết quả sẽ là hàng đầu tiên trong cột đầu tiên, hàng thứ hai trong cột thứ hai, v.v. Sẽ xuất ra:

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11

2

Một số tiêu chuẩn * nix sử dụng một lớp lót, không cần tệp tạm thời. NB: OP muốn một bản sửa lỗi hiệu quả , (tức là nhanh hơn), và các câu trả lời hàng đầu thường nhanh hơn câu trả lời này. Những phần mềm này dành cho những người thích công cụ phần mềm * nix , vì bất kỳ lý do gì. Trong một số trường hợp hiếm hoi, ( ví dụ: IO và bộ nhớ khan hiếm), những đoạn mã này thực sự có thể nhanh hơn một số câu trả lời hàng đầu.

Gọi foo tệp đầu vào .

  1. Nếu chúng ta biết foo có bốn cột:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. Nếu chúng ta không biết foo có bao nhiêu cột :

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n) ; do cut -d ' ' -f $f foo | xargs echo ; done

    xargscó giới hạn về kích thước và do đó sẽ làm cho hoạt động không hoàn chỉnh với một tệp dài. Giới hạn kích thước nào phụ thuộc vào hệ thống, ví dụ:

    { timeout '.01' xargs --show-limits ; } 2>&1 | grep Max

    Độ dài tối đa của lệnh mà chúng tôi thực sự có thể sử dụng: 2088944

  3. tr& echo:

    for f in 1 2 3 4; do cut -d ' ' -f $f foo | tr '\n\ ' ' ; echo; done

    ... hoặc nếu số cột không xác định:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
  4. Việc sử dụng set, giống như xargs, có các giới hạn dựa trên kích thước dòng lệnh tương tự:

    for f in 1 2 3 4 ; do set - $(cut -d ' ' -f $f foo) ; echo $@ ; done

2
Tất cả những thứ đó sẽ là những thứ tự có cường độ chậm hơn so với giải pháp awk hoặc perl và dễ vỡ. Đọc unix.stackexchange.com/questions/169716/… .
Ed Morton

@EdMorton, cảm ơn, phần giới thiệu câu trả lời đủ điều kiện của tôi để giải quyết mối lo ngại về tốc độ của bạn. Re "dễ vỡ": không phải 3) , và cũng không phải những thứ khác khi lập trình viên biết dữ liệu là an toàn cho một kỹ thuật nhất định; và mã shell tương thích với POSIX có phải là tiêu chuẩn ổn định hơn perl không?
agc

xin lỗi, idk nhiều về perl. Trong trường hợp này, công cụ để sử dụng sẽ là awk. cut, head, echo, Vv là không tương thích POSIX mã shell hơn là một awkkịch bản là - tất cả họ đều là tiêu chuẩn trên tất cả các cài đặt UNIX. Đơn giản là không có lý do gì để sử dụng một bộ công cụ kết hợp yêu cầu bạn phải cẩn thận về nội dung của tệp đầu vào của bạn và thư mục bạn thực thi tập lệnh từ khi bạn có thể chỉ cần sử dụng awk và kết quả cuối cùng nhanh hơn cũng như mạnh mẽ hơn .
Ed Morton

Xin vui lòng, tôi không phải là chống awk , nhưng điều kiện khác nhau. Lý do số 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done Khi bộ nhớ quá chậm hoặc IO quá thấp, các trình thông dịch lớn hơn sẽ khiến mọi thứ trở nên tồi tệ hơn cho dù chúng có tốt đến đâu trong những hoàn cảnh lý tưởng hơn. Lý do thứ 2: awk , (hoặc hầu hết bất kỳ ngôn ngữ nào), cũng gặp phải một đường cong học tập dốc hơn so với một ứng dụng nhỏ được thiết kế để làm tốt một việc. Khi thời gian chạy rẻ hơn giờ làm việc của lập trình viên, việc viết mã dễ dàng bằng "công cụ phần mềm" sẽ tiết kiệm tiền.
agc

1
#!/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

phiên bản khác với set eval


Đọc unix.stackexchange.com/questions/169716/… để hiểu một số, nhưng không phải tất cả, các vấn đề với giải pháp đó.
Ed Morton

1

Một biến thể bash khác

$ cat file 
XXXX    col1    col2    col3
row1    0       1       2
row2    3       4       5
row3    6       7       8
row4    9       10      11

Kịch bản

#!/bin/bash

I=0
while read line; do
    i=0
    for item in $line; { printf -v A$I[$i] $item; ((i++)); }
    ((I++))
done < file
indexes=$(seq 0 $i)

for i in $indexes; {
    J=0
    while ((J<I)); do
        arr="A$J[$i]"
        printf "${!arr}\t"
        ((J++))
    done
    echo
}

Đầu ra

$ ./test 
XXXX    row1    row2    row3    row4    
col1    0       3       6       9   
col2    1       4       7       10  
col3    2       5       8       11

0

Đây là một giải pháp Haskell. Khi được biên dịch với -O2, nó chạy nhanh hơn một chút so với awk của ghostdog và chậm hơn một chút so với python c được bọc mỏng của Stephan trên máy của tôi đối với các dòng nhập "Hello world" lặp lại. Thật không may, GHC hỗ trợ truyền mã dòng lệnh không tồn tại theo như tôi có thể nói, vì vậy bạn sẽ phải tự ghi nó vào một tệp. Nó sẽ cắt ngắn các hàng theo độ dài của hàng ngắn nhất.

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

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines

0

Một giải pháp awk lưu toàn bộ mảng trong bộ nhớ

    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

Nhưng chúng tôi có thể "đi" tệp nhiều lần khi các hàng xuất cần thiết:

#!/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

Cái nào (đối với số lượng hàng đầu ra thấp sẽ nhanh hơn mã trước đó).


0

Đây là một lớp lót Bash dựa trên việc chuyển đổi đơn giản mỗi dòng thành một cột và pasteghép chúng lại với nhau:

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. tạo tmp1tệp để nó không trống.

  2. đọc từng dòng và chuyển nó thành một cột bằng cách sử dụng tr

  3. dán cột mới vào tmp1tệp

  4. bản sao kết quả trở lại tmp1.

Tái bút: Tôi thực sự muốn sử dụng io-descriptors nhưng không thể làm cho chúng hoạt động.


Đảm bảo đặt đồng hồ báo thức nếu bạn định thực hiện điều đó trên một tệp lớn. Đọc unix.stackexchange.com/questions/169716/… để hiểu một số, nhưng không phải tất cả, các vấn đề với cách tiếp cận đó.
Ed Morton

0

Một tấm lót sử dụng R ...

  cat file | Rscript -e "d <- read.table(file('stdin'), sep=' ', row.names=1, header=T); write.table(t(d), file=stdout(), quote=F, col.names=NA) "

0

Tôi đã sử dụng hai tập lệnh dưới đây để thực hiện các hoạt động tương tự trước đây. Đầu tiên là ở awk, nhanh hơn rất nhiều so với thứ hai ở bash "thuần túy". Bạn có thể điều chỉnh nó cho ứng dụng của riêng bạn.

awk '
{
    for (i = 1; i <= NF; i++) {
        s[i] = s[i]?s[i] FS $i:$i
    }
}
END {
    for (i in s) {
        print s[i]
    }
}' file.txt
declare -a arr

while IFS= read -r line
do
    i=0
    for word in $line
    do
        [[ ${arr[$i]} ]] && arr[$i]="${arr[$i]} $word" || arr[$i]=$word
        ((i++))
    done
done < file.txt

for ((i=0; i < ${#arr[@]}; i++))
do
    echo ${arr[i]}
done
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.