วิธีที่มีประสิทธิภาพในการเปลี่ยนไฟล์ใน Bash


110

ฉันมีไฟล์ขนาดใหญ่ที่คั่นด้วยแท็บในรูปแบบนี้

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

ฉันต้องการเปลี่ยนมันอย่างมีประสิทธิภาพโดยใช้คำสั่ง bash เท่านั้น (ฉันสามารถเขียนสคริปต์ Perl สิบบรรทัดหรือมากกว่านั้นเพื่อทำสิ่งนั้น แต่ควรจะดำเนินการช้ากว่าฟังก์ชันทุบตีดั้งเดิม) ดังนั้นผลลัพธ์ควรมีลักษณะดังนี้

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 ในโพสต์นี้แต่ก็ยังช้าเกินไป มีความคิด / ข้อเสนอแนะ / ความคิดที่ยอดเยี่ยม? :-)


12
อะไรทำให้คุณคิดว่าจะมีสคริปต์ทุบตีที่เร็วกว่าสคริปต์ Perl นี่เป็นปัญหาที่ Perl เก่งมาก
Mark Pim

1
@mark หากทุบตีบริสุทธิ์มันอาจเร็วกว่าการผูกมัดเครื่องมือตัด / sed อื่น ๆ ทั้งหมดเข้าด้วยกัน แต่อีกครั้งหากคุณกำหนด "bash" ในการรวมเครื่องมือการเขียนสคริปต์ awk จะเปรียบได้กับการประมวลผลข้อความ Perl wrt
ghostdog74

เพิ่มอีกอันเผื่อไม่เข้าใจว่า perl จะช้าแค่ไหน เขียนโค้ดช้า? ดำเนินการช้า? ฉันไม่ชอบ perl อย่างแท้จริง แต่มันทำได้ดีในงานประเภทนี้
Corey Porter

หากคอลัมน์ / ฟิลด์ของคุณมีขนาด / ความกว้างคงที่คุณสามารถใช้ไฟล์ Python เพื่อหลีกเลี่ยงการอ่านไฟล์ของคุณในหน่วยความจำ คุณมีขนาด / ความกว้างคอลัมน์ / ฟิลด์คงที่หรือไม่?
tommy.carstensen

2
ใครก็ตามที่คิดว่าเชลล์สคริปต์จะเร็วกว่า awk หรือ perl ต้องอ่านunix.stackexchange.com/questions/169716/…เพื่อให้เข้าใจว่าทำไมถึงไม่เป็นเช่นนั้น
Ed Morton

คำตอบ:


115
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 บนไฟล์ 10,000 บรรทัด

$ 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

แก้ไขโดย Ed Morton (@ ghostdog74 อย่าลังเลที่จะลบหากคุณไม่อนุมัติ)

บางทีเวอร์ชันนี้อาจมีชื่อตัวแปรที่ชัดเจนกว่านี้จะช่วยตอบคำถามด้านล่างและโดยทั่วไปจะชี้แจงว่าสคริปต์กำลังทำอะไรอยู่ นอกจากนี้ยังใช้แท็บเป็นตัวคั่นซึ่ง 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==1END


และตอนนี้จัดการป้ายชื่อแถวและคอลัมน์ด้วยหรือไม่?
Jonathan Leffler

ตกลง - คุณถูกต้อง ข้อมูลตัวอย่างของคุณไม่ตรงกับข้อมูลตัวอย่างของคำถาม แต่โค้ดของคุณใช้งานได้ดีกับข้อมูลตัวอย่างของคำถามและให้ผลลัพธ์ที่ต้องการ (ให้หรือเว้นระยะห่างระหว่างแท็บว่าง) ส่วนใหญ่เป็นความผิดพลาดของฉัน
Jonathan Leffler

การกำหนดเวลาที่น่าสนใจ - ฉันยอมรับว่าคุณเห็นประโยชน์ด้านประสิทธิภาพใน AWK ฉันใช้ MacOS X 10.5.8 ซึ่งไม่ใช้ 'gawk'; และฉันใช้ Perl 5.10.1 (รุ่น 32 บิต) ฉันรวบรวมว่าข้อมูลของคุณเป็น 10,000 บรรทัดโดยมี 4 คอลัมน์ต่อบรรทัด? อย่างไรก็ตามมันไม่สำคัญมาก ทั้ง awk และ perl เป็นโซลูชันที่ใช้งานได้ (และโซลูชัน awk นั้นดีกว่า - การตรวจสอบ 'ที่กำหนด' ใน Perl ของฉันจำเป็นสำหรับการเตือนการวิ่งฟรีภายใต้คำเตือน / คำเตือนที่เข้มงวด) และทั้งสองอย่างไม่น่าจะเร็วกว่าแบบเดิม โซลูชันเชลล์สคริปต์
Jonathan Leffler

บนเมทริกซ์ 2.2GB ดั้งเดิมของฉันโซลูชัน perl เร็วกว่า awk เล็กน้อย - 350.103s เทียบกับ 369.410s ฉันใช้ perl 5.8.8 64 บิต
Federico Giorgi

1
@ zx8754 จำนวนฟิลด์สูงสุดที่ใช้กับ awk เก่าที่ไม่ใช่ POSIX เท่านั้น อาจเป็นชื่อที่น่าเสียดายอย่างไม่น่าเชื่อ "nawk" ไม่ใช้กับ gawk หรือ awks สมัยใหม่อื่น ๆ
Ed Morton

47

อีกทางเลือกหนึ่งคือการใช้rs:

rs -c' ' -C' ' -T

-cเปลี่ยนตัวคั่นคอลัมน์อินพุตเปลี่ยนตัวคั่นคอลัมน์-Cเอาต์พุตและ-Tเปลี่ยนแถวและคอลัมน์ อย่าใช้-tแทน-Tเนื่องจากใช้จำนวนแถวและคอลัมน์ที่คำนวณโดยอัตโนมัติซึ่งมักจะไม่ถูกต้อง rsซึ่งตั้งชื่อตามฟังก์ชันการปรับรูปร่างใหม่ใน 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
ฉันไม่คุ้นเคยrs- ขอบคุณสำหรับตัวชี้! (ลิงก์ไปยัง Debian ส่วนต้นน้ำดูเหมือนว่าจะเป็นmirbsd.org/MirOS/dist/mir/rs )
tripleee

2
@lalebarde อย่างน้อยในการใช้งานrsที่มาพร้อมกับ OS X -cเพียงอย่างเดียวตั้งค่าตัวคั่นคอลัมน์อินพุตเป็นแท็บ
nisetama

2
@lalebarde ลองอ้าง ANSI-Cของ bash เพื่อรับตัวอักษรแท็บ:$'\t'
glenn jackman

3
นี้เป็นกรณีที่รุนแรง แต่สำหรับไฟล์ขนาดใหญ่มากกับแถว ๆ อีกมากมายเช่นTTC TTA TTC TTC TTTทำงานให้rs -c' ' -C' ' -T < rows.seq > cols.seq rs: no memory: Cannot allocate memoryนี่คือระบบที่รัน FreeBSD 11.0-RELEASE พร้อม RAM ขนาด 32 GB ดังนั้นฉันเดาว่าrsทำให้ทุกอย่างอยู่ใน RAM ซึ่งดีสำหรับความเร็ว แต่ไม่ใช่สำหรับข้อมูลขนาดใหญ่
jrm

1
jq ใช้ ram 21Gb บนไฟล์ 766MB ฉันฆ่ามันหลังจากผ่านไป 40 นาทีโดยไม่มีผลลัพธ์ใด ๆ
Glubbdrubb

30

โซลูชัน 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))

รหัสนี้จะถือว่าทุกบรรทัดมีจำนวนคอลัมน์เท่ากัน (ไม่มีการเติมช่องว่าง)


3
ปัญหาเล็กน้อยที่นี่: แทนที่l.split()ด้วยl.strip().split()(Python 2.7) มิฉะนั้นบรรทัดสุดท้ายของเอาต์พุตจะพิการ การทำงานสำหรับคั่นคอลัมน์โดยพลการใช้งานl.strip().split(sep)และถ้าแยกของคุณจะถูกเก็บไว้ในตัวแปรsep.join(c) sep
krlmlr

21

transposeโครงการ SourceForge เป็น coreutil-เช่น C โปรแกรมสำหรับว่าที่

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

ขอบคุณสำหรับลิงค์ อย่างไรก็ตามต้องใช้หน่วยความจำมากเกินไปเมื่อต้องจัดการกับเมทริกซ์ / ไฟล์ขนาดใหญ่
tommy.carstensen

มันมีอาร์กิวเมนต์สำหรับ blockize และ fieldsize: ลองปรับแต่ง-bและ-fอาร์กิวเมนต์
แกะบิน

ขนาดบล็อกเริ่มต้น (- บล็อกหรือ -b) คือ 10kb และขนาดฟิลด์เริ่มต้น (--fieldmax หรือ -f) คือ 64 ดังนั้นจึงไม่สามารถเป็นได้ ฉันเหนื่อย. ขอบคุณสำหรับข้อเสนอแนะ
tommy.carstensen

1
ทำงานได้ดีกับ csv ขนาด 2 GB
discipulus

2
สำหรับไฟล์เมทริกซ์ที่มีขนาดประมาณ 11k คูณ 5k ฉันพบว่า transpose.c เร็วกว่า ~ 7 เท่าและมีประสิทธิภาพหน่วยความจำมากกว่า ~ 5 เท่าเมื่อเทียบกับโซลูชัน awk แรกของ ghostdog74 นอกจากนี้ฉันพบว่ารหัส awk "ใช้หน่วยความจำแทบไม่มีเลย" จาก ghostdog74 ทำงานไม่ถูกต้อง นอกจากนี้โปรดระวังแฟล็ก --limit ในโปรแกรม transpose.c ซึ่งโดยค่าเริ่มต้นจะ จำกัด เอาต์พุตไว้ที่ขนาด 1k คูณ 1k
ncemami

16

Pure 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

สิ่งนี้ใช้ได้ผลกับไฟล์ของฉันแม้ว่ามันจะพิมพ์รายการไดเรกทอรีสำหรับบรรทัดแรกของตารางอย่างน่าสนใจ ฉันไม่รู้ BASH มากพอที่จะหาสาเหตุได้
bugloaf

@bugloaf โต๊ะของคุณมี * ที่มุม
สวัสดี 71

2
@bugloaf: การอ้างอิงตัวแปรอย่างถูกต้องควรป้องกันไม่ให้:printf "%s\t" "${array[$COUNTER]}"
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบอีกครั้ง

16

ดูGNU datamashซึ่งสามารถใช้ได้เช่นdatamash transpose. เวอร์ชันในอนาคตจะรองรับการจัดตารางข้าม (ตาราง Pivot)


9

นี่คือสคริปต์ Perl ที่มั่นคงพอสมควรในการทำงาน มีการเปรียบเทียบโครงสร้างหลายอย่างกับawkโซลูชันของ @ 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";
}

ด้วยขนาดข้อมูลตัวอย่างความแตกต่างของประสิทธิภาพระหว่าง perl และ awk มีน้อยมาก (1 มิลลิวินาทีจากทั้งหมด 7) ด้วยชุดข้อมูลที่ใหญ่กว่า (เมทริกซ์ 100x100 รายการละ 6-8 อักขระ) perl มีประสิทธิภาพดีกว่า awk เล็กน้อย - 0.026s เทียบกับ 0.042s ไม่น่าจะเป็นปัญหา


การกำหนดเวลาที่เป็นตัวแทนสำหรับ Perl 5.10.1 (32 บิต) เทียบกับ awk (เวอร์ชัน 20040207 เมื่อกำหนด '-V') เทียบกับ 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 เห็นได้ชัดว่าระยะทางของคุณจะแตกต่างกันไป


ในระบบของฉัน gawk มีประสิทธิภาพดีกว่า perl คุณสามารถดูผลลัพธ์ของฉันได้ในโพสต์ที่แก้ไขของฉัน
ghostdog74

4
รวบรวมข้อสรุป: แพลตฟอร์มที่แตกต่างกันเวอร์ชันซอฟต์แวร์ที่แตกต่างผลลัพธ์ที่แตกต่างกัน
ghostdog74

6

หากคุณได้scติดตั้งคุณสามารถทำได้:

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

4
โปรดทราบว่าสิ่งนี้รองรับบรรทัดจำนวน จำกัด เนื่องจากscตั้งชื่อคอลัมน์เป็นหนึ่งหรือสองอักขระรวมกัน ขีด จำกัด คือ26 + 26^2 = 702.


5

สมมติว่าแถวทั้งหมดของคุณมีจำนวนฟิลด์เท่ากันโปรแกรม 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

5

GNU datamashเหมาะอย่างยิ่งสำหรับปัญหานี้ด้วยโค้ดเพียงบรรทัดเดียวและขนาดไฟล์ที่ใหญ่โตโดยพลการ!

datamash -W transpose infile > outfile

3

วิธีการแก้แฮ็ค perl อาจเป็นเช่นนี้ เป็นเรื่องที่ดีเพราะมันไม่ได้โหลดไฟล์ทั้งหมดในหน่วยความจำพิมพ์ไฟล์ temp ระดับกลางจากนั้นใช้การวางที่ยอดเยี่ยมทั้งหมด

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

การใช้ไฟล์ paste และ temp เป็นเพียงการดำเนินการพิเศษที่ไม่จำเป็น คุณสามารถจัดการภายในหน่วยความจำได้เองเช่นอาร์เรย์ / แฮช
ghostdog74

2
ใช่ แต่นั่นไม่ได้หมายความว่าจะเก็บทุกอย่างไว้ในความทรงจำเหรอ? ไฟล์ที่ฉันกำลังจัดการมีขนาดประมาณ 2-20GB
Federico Giorgi

3

การปรับปรุงเพียงอย่างเดียวที่ฉันเห็นในตัวอย่างของคุณคือการใช้ 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

3

โดยปกติฉันใช้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จำนวนคอลัมน์ที่ไฟล์เริ่มต้นมีเพื่อที่จะใช้เป็นจำนวนแถวที่จะพิมพ์กลับ


2

ฉันใช้โซลูชันของ 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

2

ฉันแค่มองหาชุดทุบตีที่คล้ายกัน แต่ด้วยการรองรับช่องว่างภายใน นี่คือสคริปต์ที่ฉันเขียนโดยอ้างอิงจากโซลูชันของ 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

2

ฉันกำลังมองหาวิธีแก้ปัญหาในการเปลี่ยนเมทริกซ์ชนิดใดก็ได้ (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

2

หากคุณต้องการดึงเพียงบรรทัดเดียว (คั่นด้วยจุลภาค) $ N จากไฟล์และเปลี่ยนเป็นคอลัมน์:

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

2

ไม่สวยหรูมาก แต่คำสั่ง "บรรทัดเดียว" นี้ช่วยแก้ปัญหาได้อย่างรวดเร็ว:

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

นี่คือคอลัมน์จำนวนคอลัมน์ที่คุณสามารถแทนที่ 4 head -n 1 input | wc -wโดย


2

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

2

บาง* nix standard ใช้ one-liners ไม่จำเป็นต้องใช้ไฟล์ temp หมายเหตุ: OP ต้องการการแก้ไขที่มีประสิทธิภาพ (เช่นเร็วกว่า) และคำตอบยอดนิยมมักจะเร็วกว่าคำตอบนี้ one-liners เหล่านี้เหมาะสำหรับผู้ที่ชอบ* nix software toolsไม่ว่าจะด้วยเหตุผลใดก็ตาม ในบางกรณี ( เช่น IO และหน่วยความจำที่หายาก) ตัวอย่างข้อมูลเหล่านี้อาจเร็วกว่าคำตอบด้านบนบางส่วน

โทรแฟ้มใส่foo

  1. ถ้าเรารู้ว่าfooมีสี่คอลัมน์:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
  2. หากเราไม่ทราบว่า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

  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

2
ทั้งหมดนี้จะเป็นคำสั่งของขนาดที่ช้ากว่าโซลูชัน awk หรือ perl และเปราะบาง อ่านunix.stackexchange.com/questions/169716/… .
Ed Morton

@EdMorton ขอบคุณบทนำที่มีคุณสมบัติเหมาะสมสำหรับคำตอบของฉันเพื่อจัดการกับข้อกังวลด้านความเร็วของคุณ Re "เปราะบาง": ไม่ใช่3)และอื่น ๆ เมื่อโปรแกรมเมอร์รู้ว่าข้อมูลปลอดภัยสำหรับเทคนิคที่กำหนด และรหัสเชลล์ที่เข้ากันได้กับ POSIX ไม่ใช่มาตรฐานที่เสถียรกว่าperlหรือไม่?
agc

ขออภัย idk มากเกี่ยวกับ perl ในกรณีนี้เครื่องมือที่จะใช้awkคือ cut, head, echoและอื่น ๆ มีไม่มาก POSIX รหัสเปลือกเข้ากันได้กว่าawkสคริปต์ - พวกเขาทั้งหมดเป็นมาตรฐานในทุกการติดตั้งระบบปฏิบัติการยูนิกซ์ ไม่มีเหตุผลที่จะใช้ชุดเครื่องมือที่ต้องใช้ร่วมกันเพื่อให้คุณระมัดระวังเกี่ยวกับเนื้อหาของไฟล์อินพุตและไดเร็กทอรีที่คุณเรียกใช้สคริปต์เมื่อคุณสามารถใช้ awk ได้และผลลัพธ์สุดท้ายก็เร็วขึ้นและมีประสิทธิภาพมากขึ้น .
Ed Morton

กรุณาผมไม่ต่อต้านawkแต่เงื่อนไขที่แตกต่างกันไป เหตุผล # 1: for f in cut head xargs seq awk ; do wc -c $(which $f) ; done เมื่อพื้นที่จัดเก็บช้าเกินไปหรือ IO ต่ำเกินไปล่ามที่มีขนาดใหญ่จะทำให้สิ่งต่างๆแย่ลงไม่ว่าพวกเขาจะอยู่ในสถานการณ์ที่ดีเพียงใดก็ตาม เหตุผล # 2: awk (หรือภาษาส่วนใหญ่) ก็ต้องทนทุกข์ทรมานจากช่วงการเรียนรู้ที่สูงชันกว่าเครื่องมือขนาดเล็กที่ออกแบบมาเพื่อทำสิ่งหนึ่งได้ดี เมื่อเวลาทำงานถูกกว่าชั่วโมงการทำงานของคนเขียนโค้ดการเข้ารหัสอย่างง่ายดายด้วย "เครื่องมือซอฟต์แวร์" จะช่วยประหยัดเงิน
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

รุ่นอื่นกับ set eval


อ่านunix.stackexchange.com/questions/169716/…เพื่อทำความเข้าใจบางส่วน แต่ไม่ใช่ทั้งหมดของปัญหาเกี่ยวกับวิธีแก้ปัญหานั้น
Ed Morton

1

ตัวแปรทุบตีอื่น

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

สคริปต์

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

เอาต์พุต

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

0

นี่คือโซลูชันของ Haskell เมื่อคอมไพล์ด้วย -O2 มันจะทำงานเร็วกว่า awk ของ ghostdog เล็กน้อยและช้ากว่าc python ที่ห่อบาง ๆของ Stephan บนเครื่องของฉันเล็กน้อยสำหรับบรรทัดอินพุต "Hello world" ซ้ำ ๆ น่าเสียดายที่ GHC รองรับการส่งรหัสบรรทัดคำสั่งนั้นไม่มีอยู่จริงเท่าที่ฉันสามารถบอกได้ดังนั้นคุณจะต้องเขียนลงในไฟล์ด้วยตัวเอง มันจะตัดทอนแถวให้มีความยาวของแถวที่สั้นที่สุด

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

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

0

โซลูชัน 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

ซึ่ง (สำหรับจำนวนแถวเอาต์พุตต่ำจะเร็วกว่าโค้ดก่อนหน้า)


0

นี่คือ 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
  1. สร้างtmp1ไฟล์เพื่อไม่ให้ว่างเปล่า

  2. อ่านแต่ละบรรทัดและแปลงเป็นคอลัมน์โดยใช้ tr

  3. วางคอลัมน์ใหม่ลงในtmp1ไฟล์

  4. tmp1สำเนาส่งผลให้กลับเข้ามา

PS: ฉันอยากใช้ io-descriptors แต่ไม่สามารถใช้งานได้


อย่าลืมตั้งนาฬิกาปลุกหากคุณจะใช้งานไฟล์ขนาดใหญ่ อ่านunix.stackexchange.com/questions/169716/…เพื่อทำความเข้าใจปัญหาเกี่ยวกับแนวทางดังกล่าวบางส่วน แต่ไม่ใช่ทั้งหมด
Ed Morton

0

ออนไลเนอร์โดยใช้ 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

ฉันเคยใช้สองสคริปต์ด้านล่างเพื่อดำเนินการที่คล้ายกันมาก่อน อันแรกคือใน awk ซึ่งเร็วกว่าอันที่สองที่อยู่ใน bash "pure" มาก คุณอาจสามารถปรับให้เข้ากับแอปพลิเคชันของคุณเองได้

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.