สุ่มสุ่มจำนวนบรรทัดจากไฟล์ข้อมูล


13

ฉันมีรายการข้อมูลเช่น

12345
23456
67891
-20000
200
600
20
...

สมมติว่าขนาดของชุดข้อมูลนี้ (เส้นเช่นของไฟล์) Nเป็น ฉันต้องการสุ่มวาดmเส้นจากไฟล์ข้อมูลนี้ ดังนั้นผลลัพธ์ควรเป็นสองไฟล์หนึ่งไฟล์เป็นไฟล์ที่รวมmบรรทัดข้อมูลเหล่านี้และอีกไฟล์หนึ่งมีN-mข้อมูลอยู่ด้วย

มีวิธีการที่ใช้คำสั่ง Linux หรือไม่


1
คุณกังวลเกี่ยวกับลำดับของเส้นหรือไม่? เช่น. คุณต้องการรักษาลำดับของแหล่งที่มาหรือไม่หรือคุณต้องการให้ลำดับนั้นเป็นแบบสุ่มรวมถึงตัวเลือกของบรรทัดที่สุ่ม
Peter.O

คำตอบ:


18

นี่อาจไม่ใช่วิธีที่มีประสิทธิภาพที่สุด แต่ใช้ได้ผล:

shuf <file> > tmp
head -n $m tmp > out1
tail -n +$(( m + 1 )) tmp > out2

ด้วย$mที่มีจำนวนบรรทัด


@userunknown sort -Rดูแลการสุ่ม ไม่แน่ใจว่าคุณลงคะแนนคำตอบนั้นหรือไม่ให้ค้นหาใน manpage ก่อน
Rob Wouters

2
โปรดทราบว่าsort -Rไม่เรียงลำดับการป้อนข้อมูลแบบสุ่ม: จัดกลุ่มบรรทัดที่เหมือนกัน ดังนั้นถ้าใส่เป็นเช่นfoo, foo, bar, barและ m = 2 แล้วหนึ่งไฟล์จะมีทั้งสองfooและอื่น ๆ ที่จะมีทั้งbars coreutils ของ GNU ก็มีshufเช่นกันซึ่งสุ่มอินพุตบรรทัด นอกจากนี้คุณไม่จำเป็นต้องใช้ไฟล์ชั่วคราว
Gilles 'ดังนั้น - หยุดความชั่วร้าย'

ทำไมไม่shuf <file> |head -n $m?
emanuele

@Manuele: เพราะเราต้องการทั้งส่วนหัวและส่วนท้ายในสองไฟล์แยกกัน
Rob Wouters

5

สคริปต์ bash / awk นี้เลือกบรรทัดแบบสุ่มและรักษาลำดับดั้งเดิมในไฟล์เอาต์พุตทั้งสอง

awk -v m=4 -v N=$(wc -l <file) -v out1=/tmp/out1 -v out2=/tmp/out2 \
 'BEGIN{ srand()
         do{ lnb = 1 + int(rand()*N)
             if ( !(lnb in R) ) {
                 R[lnb] = 1
                 ct++ }
         } while (ct<m)
  } { if (R[NR]==1) print > out1 
      else          print > out2       
  }' file
cat /tmp/out1
echo ========
cat /tmp/out2

เอาท์พุทขึ้นอยู่กับข้อมูลในคำถาม

12345
23456
200
600
========
67891
-20000
20

4

เช่นเดียวกับทุกสิ่ง Unix มีประโยชน์สำหรับTMนั้น

โปรแกรมของวัน: split
splitจะแบ่งไฟล์ในหลายวิธี, -bไบต์, -lเส้น, -nจำนวนไฟล์ที่ส่งออก เราจะใช้-lตัวเลือก เนื่องจากคุณต้องการเลือกบรรทัดสุ่มและไม่ใช่แค่บรรทัดแรกmเราจะsortสุ่มเลือกไฟล์ก่อน หากคุณต้องการที่จะอ่านเกี่ยวกับการsortอ้างถึงคำตอบของฉันที่นี่

ตอนนี้รหัสจริง มันค่อนข้างง่ายจริงๆ:

sort -R input_file | split -l $m output_prefix

นี้จะทำให้ทั้งสองไฟล์หนึ่งมีmเส้นและเป็นหนึ่งเดียวกับN-mเส้นชื่อและoutput_prefixaa output_prefixabให้แน่ใจว่าmเป็นไฟล์ที่มีขนาดใหญ่ที่คุณต้องการหรือคุณจะได้รับหลายไฟล์ของความยาวm(และเป็นหนึ่งเดียวกับN % m)

หากคุณต้องการให้แน่ใจว่าคุณใช้ขนาดที่ถูกต้องนี่เป็นรหัสเล็กน้อยที่จะทำ:

m=10 # size you want one file to be
N=$(wc -l input_file)
m=$(( m > N/2 ? m : N - m ))
sort -R input_file | split -l $m output_prefix

แก้ไข: ฉันพบว่าsortการใช้งานบางอย่างไม่มีการ-Rตั้งค่าสถานะ ถ้าคุณมีคุณสามารถใช้แทนperlperl -e 'use List::Util qw/shuffle/; print shuffle <>;'


1
น่าเสียดายที่sort -Rดูเหมือนจะมีเฉพาะในบางรุ่นเท่านั้น (อาจเป็นรุ่น gnu) สำหรับแพลตฟอร์มอื่นฉันเขียนเครื่องมือที่เรียกว่า 'randline' ซึ่งไม่ทำอะไรเลยนอกจากการสุ่ม stdin มันอยู่ที่beesbuzz.biz/codeสำหรับทุกคนที่ต้องการมัน (ฉันมักจะสับเปลี่ยนเนื้อหาไฟล์ค่อนข้างมาก)
ปุย

1
โปรดทราบว่าsort -Rไม่เรียงลำดับการป้อนข้อมูลแบบสุ่ม: จัดกลุ่มบรรทัดที่เหมือนกัน ดังนั้นถ้าใส่เป็นเช่นfoo, foo, bar, barและ m = 2 แล้วหนึ่งไฟล์จะมีทั้งสองfooและอื่น ๆ ที่จะมีทั้งbars coreutils ของ GNU ก็มีshufเช่นกันซึ่งสุ่มอินพุตบรรทัด นอกจากนี้ท่านสามารถเลือกชื่อไฟล์ที่ส่งออกโดยใช้headและtailsplitแทน
Gilles 'SO- หยุดความชั่วร้าย'

4

หากคุณไม่คิดใหม่ในการจัดเรียงบรรทัดใหม่และคุณมี GNU coreutils (เช่นบน Linux หรือ Cygwin ที่ไม่ได้ฝังตัวไม่เก่าเกินไปนับตั้งแต่shufปรากฏในเวอร์ชัน 6.0), shuf(“ สับเปลี่ยน”) เรียงลำดับบรรทัดของไฟล์แบบสุ่ม ดังนั้นคุณสามารถสับเปลี่ยนไฟล์และส่งบรรทัด m แรกลงในไฟล์เดียวและที่เหลือลงในอีกไฟล์

ไม่มีวิธีที่เหมาะที่จะทำอย่างนั้น คุณไม่สามารถโยงheadและเพียงtailเพราะheadบัฟเฟอร์ล่วงหน้า คุณสามารถใช้splitแต่คุณไม่ได้รับความยืดหยุ่นใด ๆ เกี่ยวกับชื่อไฟล์ที่ส่งออก แน่นอนคุณสามารถใช้awk:

<input shuf | awk -v m=$m '{ if (NR <= m) {print >"output1"} else {print} }'

คุณสามารถใช้sedซึ่งไม่ชัดเจน แต่อาจเร็วกว่าสำหรับไฟล์ขนาดใหญ่

<input shuf | sed -e "1,${m} w output1" -e "1,${m} d" >output2

หรือคุณสามารถใช้teeเพื่อทำสำเนาข้อมูลหากแพลตฟอร์มของคุณมี/dev/fd; ไม่เป็นไรถ้า m มีขนาดเล็ก:

<input shuf | { tee /dev/fd/3 | head -n $m >output1; } 3>&1 | tail -n +$(($m+1)) >output2

คุณสามารถใช้ awk เพื่อส่งแต่ละบรรทัดในทางกลับกัน โปรดทราบว่า awk นั้นไม่ค่อยดีนักในการเริ่มต้นตัวสร้างตัวเลขแบบสุ่ม การสุ่มไม่เพียง แต่ไม่เหมาะสำหรับการเข้ารหัสเท่านั้น แต่ไม่เหมาะสำหรับการจำลองเชิงตัวเลข เมล็ดจะเหมือนกันสำหรับการร้องขอ awk ทั้งหมดในระบบใด ๆ ที่มีระยะเวลาหนึ่งวินาที

<input awk -v N=$(wc -l <input) -v m=3 '
    BEGIN {srand()}
    {
        if (rand() * N < m) {--m; print >"output1"} else {print >"output2"}
        --N;
    }'

หากคุณต้องการการสุ่มที่ดีกว่าคุณสามารถทำสิ่งเดียวกันใน Perl ซึ่งหว่าน RNG ของมันอย่างเหมาะสม

<input perl -e '
    open OUT1, ">", "output1" or die $!;
    open OUT2, ">", "output2" or die $!;
    my $N = `wc -l <input`;
    my $m = $ARGV[0];
    while (<STDIN>) {
        if (rand($N) < $m) { --$m; print OUT1 $_; } else { print OUT2 $_; }
        --$N;
    }
    close OUT1 or die $!;
    close OUT2 or die $!;
' 42

@Gilles:สำหรับawkตัวอย่าง: -v N=$(wc -l <file) -v m=4... และมันจะพิมพ์เฉพาะบรรทัด"สุ่ม"เมื่อค่าสุ่มน้อยกว่า$mแทนที่จะพิมพ์$mแบบสุ่ม ... ดูเหมือนว่ามันperlอาจจะทำสิ่งเดียวกันกับแรนด์แต่ฉันไม่ ไม่ทราบperlดีพอที่จะได้รับข้อผิดพลาดในการรวบรวม: ข้อผิดพลาดทางไวยากรณ์ที่ -e บรรทัด 7, ใกล้ ") พิมพ์"
Peter.O

@ Peter.O ขอบคุณนั่นคือสิ่งที่มาจากการพิมพ์ในเบราว์เซอร์และแก้ไขอย่างไม่ระมัดระวัง ฉันแก้ไขรหัส awk และ perl แล้ว
Gilles 'หยุดความชั่วร้าย'

ทั้ง 3 วิธีทำงานได้ดีและรวดเร็ว .. ขอบคุณ (+1) ... ฉันค่อย ๆ เอาหัวไปรอบ ๆ perl ... และนั่นก็เป็นไฟล์ที่น่าสนใจและมีประโยชน์โดยเฉพาะในshufตัวอย่าง
Peter.O

ปัญหาการบัฟเฟอร์? . ฉันพลาดอะไรไปรึเปล่า? head catคำสั่งผสมทำให้เกิดการสูญเสียข้อมูลในต่อไปนี้การทดสอบที่สอง3-4 .... ทดสอบ 1-2 { for i in {00001..10000} ;do echo $i; done; } | { head -n 5000 >out1; cat >out2; } .. TEST 3-4 { for i in {00001..10000} ;do echo $i; done; } >input; cat input | { head -n 5000 >out3; cat >out4; } ... wc -lผลการค้นหาสำหรับผลของการทดสอบ 1-2เป็น5000 5000 (ดี) แต่สำหรับการทดสอบ 3-4คือ5000 4539 (ไม่ดี) .. ความแตกต่างนั้นขึ้นอยู่กับขนาดไฟล์ที่เกี่ยวข้อง ... นี่คือลิงค์ไปยังรหัสทดสอบ
Peter.O

@ Peter.O ขอบคุณอีกครั้ง แน่นอนheadอ่านล่วงหน้า; สิ่งที่อ่านล่วงหน้าและไม่พิมพ์ออกจะถูกยกเลิก ฉันได้อัปเดตคำตอบของฉันด้วยรูปลักษณ์ที่สวยงามน้อยลง แต่ฉันมั่นใจว่าโซลูชันที่ถูกต้อง
Gilles 'หยุดชั่วร้าย'

2

สมมติm = 7และN = 21:

cp ints ints.bak
for i in {1..7}
do
    rnd=$((RANDOM%(21-i)+1))
    # echo $rnd;  
    sed -n "${rnd}{p,q}" 10k.dat >> mlines 
    sed -i "${rnd}d" ints 
done

หมายเหตุ: หากคุณแทนที่7ด้วยตัวแปรเช่น$1หรือ$mคุณต้องใช้seqไม่ใช่{from..to}-notation ซึ่งไม่ได้ทำการขยายตัวแปร

มันทำงานได้โดยการลบทีละบรรทัดจากไฟล์ซึ่งสั้นลงเรื่อย ๆ ดังนั้นหมายเลขบรรทัดที่สามารถลบออกได้จะต้องเล็กลงเรื่อย ๆ

นี้ไม่ควรถูกนำมาใช้สำหรับไฟล์อีกต่อไปและหลายสายตั้งแต่หมายเลขทุกโดยเฉลี่ยความต้องการครึ่งไฟล์ที่จะอ่านที่ 1 และแฟ้มทั้งหมดสำหรับ 2 sedรหัส


เขาต้องการไฟล์ที่มีบรรทัดที่ถูกลบด้วย
Rob Wouters

ฉันคิดว่า "รวมถึงบรรทัดข้อมูล m เหล่านี้" ควรหมายถึงincluding themแต่บรรทัดดั้งเดิมด้วย - ดังนั้นincludingไม่ใช่consisting ofและไม่ใช้onlyแต่ฉันเดาว่าการตีความของคุณคือความหมายของผู้ใช้ 288609 ฉันจะปรับสคริปต์ของฉันตาม
ผู้ใช้ที่ไม่รู้จัก

ดูดี. `` ``
Rob Wouters

@ ผู้ใช้ที่ไม่รู้จัก: คุณ+1อยู่ผิดที่ มันควรจะเป็นrnd=$((RANDOM%(N-i)+1))ที่ N = 21 ในตัวอย่างของคุณ .. มันกำลังทำให้เกิดความsedผิดพลาดเมื่อมีการประเมินเพื่อrnd 0.. นอกจากนี้มันยังปรับขนาดได้ไม่ดีนักเมื่อเขียนไฟล์ใหม่ทั้งหมด เช่น123 วินาทีในการสกัด 5,000 เส้นสุ่มจากแฟ้ม 10,000 เส้นเทียบกับ 0.03 วินาทีสำหรับวิธีการโดยตรงมากขึ้น ...
Peter.O

@ Peter.O: ถูกต้อง (แก้ไขแล้ว) และคุณพูดถูก
ผู้ใช้ที่ไม่รู้จัก
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.