จะใช้งานไฟล์มากกว่า 10 ล้านไฟล์ในไดเรกทอรีได้อย่างไร?


16

ฉันมีไดเรกทอรีที่มี 1,014,491 ไฟล์อยู่ในนั้น จนถึงตอนนี้ฉันได้ลองทำสิ่งต่อไปนี้:

  • for f in ls; do sed -i -e 's/blah/blee/g' $f; done

ชนเปลือกของฉันlsอยู่ในทิลด้า แต่ฉันไม่สามารถหาวิธีที่จะทำให้หนึ่ง

  • ls | xargs -0 sed -i -e 's/blah/blee/g'

มีจำนวนมากเกินไป sed

  • find . -name "*.txt" -exec sed -i -e 's/blah/blee/g' {} \;

ไม่สามารถแยกหน่วยความจำได้อีกแล้ว

แนวคิดอื่น ๆ เกี่ยวกับวิธีสร้างคำสั่งชนิดนี้? ไฟล์ไม่จำเป็นต้องสื่อสารกัน ls | wc -lดูเหมือนว่าจะทำงาน (ช้ามาก) ดังนั้นจึงต้องเป็นไปได้


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

@intuited: มันจะยิ่งเร็วกว่าที่จะไม่ทำอะไรกับไฟล์เลย ... อย่างจริงจัง? หากคุณต้องการเปลี่ยนรูปแบบในชุดของไฟล์ที่คุณต้องมองเข้าไปในแต่ละไฟล์เพื่อดูว่ามีรูปแบบหรือไม่ หากคุณรู้ล่วงหน้าว่าคุณสามารถข้ามไฟล์ 'บางส่วน' ไปได้เร็วกว่าที่จะไม่แตะแม้แต่ไฟล์ และเวลาเริ่มต้นสำหรับsedอาจจะเร็วกว่าการเปิดตัวpythonหรือperlเช่นกันยกเว้นถ้าคุณทำทุกอย่างในล่ามนั้น
akira

@akira: คุณกำลังบอกว่าการเปิดใช้ perl หรือ python หนึ่งครั้งสำหรับไฟล์จำนวนมากเท่าที่จะพอดีกับบรรทัดคำสั่งนั้นมีราคาแพงกว่าการเปิดตัว sed หนึ่งครั้งสำหรับแต่ละไฟล์เหล่านั้นหรือไม่? ฉันจะแปลกใจจริงๆถ้าเป็นเช่นนั้น —————— ฉันเดาว่าคุณไม่เข้าใจว่าคำแนะนำของฉันคือเรียกใช้ (เริ่มต้น) โปรแกรมแก้ไขหนึ่งครั้ง (หรืออย่างน้อยก็น้อยกว่า - ดูคำตอบของฉัน) และเปิดมันแก้ไขและบันทึกไฟล์แต่ละไฟล์ ในทางกลับกันแทนที่จะเรียกใช้โปรแกรมแก้ไขแยกต่างหากสำหรับแต่ละไฟล์เหล่านั้น
intuited

ความคิดเห็นแรกของคุณไม่ได้สะท้อนถึงสิ่งที่คุณต้องการจะพูดว่า: "replace sed ด้วย python / perl" .. เพียงแค่ทำอย่างนั้นและดู @ commandline OP ได้ให้ไว้ผู้อ่านที่ไร้เดียงสาอาจคิดว่า "find. -exec python" เร็วกว่า "ค้นหา. -exec sed" .. ซึ่งเห็นได้ชัดว่าไม่ใช่กรณี ในคำตอบของคุณคุณเรียกงูใหญ่บ่อยกว่าที่จำเป็น
akira

ฉันคิดว่าอากิระตีความคำแนะนำของคุณผิด ๆ ฉันเชื่อว่าคุณกำลังแนะนำให้รวมไฟล์เข้าด้วยกัน ฉันพยายามที่มี xargs ฉันพยายามที่เวลาที่จะลองอีกครั้ง :)
ซานโดร

คำตอบ:


19

ลองดูสิ:

find -name '*.txt' -print0 | xargs -0 -I {} -P 0 sed -i -e 's/blah/blee/g' {}

มันจะฟีดชื่อไฟล์เดียวเท่านั้นสำหรับการร้องขอแต่ละsedครั้ง ที่จะแก้ปัญหา "args มากเกินไปสำหรับ sed" ปัญหา -Pตัวเลือกที่จะช่วยให้กระบวนการหลายที่จะคดเคี้ยวในเวลาเดียวกัน หาก 0 ใช้งานไม่ได้ (ควรเรียกใช้ให้ได้มากที่สุด) ลองใช้หมายเลขอื่น (10 - 100 - จำนวนคอร์ที่คุณมี?) เพื่อ จำกัด จำนวน


3
อาจจะต้องมีการfind . -name \*.txt -print0หลีกเลี่ยงเปลือกขยาย glob และพยายามที่จะ alloc พื้นที่สำหรับ 10 ล้านข้อโต้แย้งที่จะพบ
Chris Johnsen

@ChrisJohnsen: ใช่ถูกต้อง ฉันรีบโพสต์คำตอบและพลาดรวมถึงส่วนสำคัญเหล่านั้น ฉันได้แก้ไขคำตอบของฉันด้วยการแก้ไขเหล่านั้น ขอบคุณ
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

ลองเลยตอนนี้ ... ข้ามนิ้ว
Sandro

7

ฉันได้ทดสอบวิธีนี้ (และอื่น ๆ ทั้งหมด) บนไฟล์10 ล้าน (ว่าง) ชื่อ "hello 00000001" เป็น "hello 10000000" (14 ไบต์ต่อชื่อ)

อัปเดต: ตอนนี้ฉันได้รวมการรันแบบ quad-coreใน'find |xargs'วิธีการ (ยังไม่มี 'sed' เพียงแค่ echo> / dev / null) ..

# Step 1. Build an array for 10 million files
#   * RAM usage approx:  1.5 GiB 
#   * Elapsed Time:  2 min 29 sec 
  names=( hello\ * )

# Step 2. Process the array.
#   * Elapsed Time:  7 min 43 sec
  for (( ix=0, cnt=${#names[@]} ; ix<$cnt; ix++ )) ; do echo "${names[ix]}" >/dev/null ; done  

นี่คือบทสรุปว่าคำตอบที่ให้ไว้นั้นมีลักษณะอย่างไรเมื่อทำงานกับข้อมูลการทดสอบที่กล่าวถึงข้างต้น ผลลัพธ์เหล่านี้เกี่ยวข้องกับค่าโสหุ้ยพื้นฐานเท่านั้น เช่น 'sed' ไม่ได้ถูกเรียก กระบวนการ sed นั้นเกือบจะใช้เวลานานที่สุด แต่ฉันคิดว่ามันน่าสนใจที่จะเห็นว่าวิธีการเปรียบเทียบแบบเปลือยเปล่า

'find |xargs'วิธีการของเดนนิสใช้แกนเดียวใช้เวลานานกว่าbash arrayวิธีในการno sedวิ่ง* 4 ชั่วโมง 21 นาที ** อย่างไรก็ตามข้อดีแบบมัลติคอร์ที่เสนอโดย 'ค้นหา' ควรมีค่ามากกว่าความแตกต่างของเวลาที่แสดงเมื่อมีการเรียก sed กำลังประมวลผลไฟล์ ...

           | Time    | RAM GiB | Per loop action(s). / The command line. / Notes
-----------+---------+---------+----------------------------------------------------- 
Dennis     | 271 min | 1.7 GiB | * echo FILENAME >/dev/null
Williamson   cores: 1x2.66 MHz | $ time find -name 'hello *' -print0 | xargs -0 -I {} echo >/dev/null {}
                               | Note: I'm very surprised at how long this took to run the 10 million file gauntlet
                               |       It started processing almost immediately (because of xargs I suppose),  
                               |       but it runs **significantly slower** than the only other working answer  
                               |       (again, probably because of xargs) , but if the multi-core feature works  
                               |       and I would think that it does, then it could make up the defecit in a 'sed' run.   
           |  76 min | 1.7 GiB | * echo FILENAME >/dev/null
             cores: 4x2.66 MHz | $ time find -name 'hello *' -print0 | xargs -0 -I {} -P 0 echo >/dev/null {}
                               |  
-----------+---------+---------+----------------------------------------------------- 
fred.bear  | 10m 12s | 1.5 GiB | * echo FILENAME >/dev/null
                               | $ time names=( hello\ * ) ; time for (( ix=0, cnt=${#names[@]} ; ix<$cnt; ix++ )) ; do echo "${names[ix]}" >/dev/null ; done
-----------+---------+---------+----------------------------------------------------- 
l0b0       | ?@#!!#  | 1.7 GiB | * echo FILENAME >/dev/null 
                               | $ time  while IFS= read -rd $'\0' path ; do echo "$path" >/dev/null ; done < <( find "$HOME/junkd" -type f -print0 )
                               | Note: It started processing filenames after 7 minutes.. at this point it  
                               |       started lots of disk thrashing.  'find' was using a lot of memory, 
                               |       but in its basic form, there was no obvious advantage... 
                               |       I pulled the plug after 20 minutes.. (my poor disk drive :(
-----------+---------+---------+----------------------------------------------------- 
intuited   | ?@#!!#  |         | * print line (to see when it actually starts processing, but it never got there!)
                               | $ ls -f hello * | xargs python -c '
                               |   import fileinput
                               |   for line in fileinput.input(inplace=True):
                               |       print line ' 
                               | Note: It failed at 11 min and approx 0.9 Gib
                               |       ERROR message: bash: /bin/ls: Argument list too long  
-----------+---------+---------+----------------------------------------------------- 
Reuben L.  | ?@#!!#  |         | * One var assignment per file
                               | $ ls | while read file; do x="$file" ; done 
                               | Note: It bombed out after 6min 44sec and approx 0.8 GiB
                               |       ERROR message: ls: memory exhausted
-----------+---------+---------+----------------------------------------------------- 


1

นี่เป็นหัวข้อนอกเรื่อง แต่คุณสามารถใช้ได้

find -maxdepth 1 -type f -name '*.txt' | xargs python -c '
import fileinput
for line in fileinput.input(inplace=True):
    print line.replace("blah", "blee"),
'

ประโยชน์หลักของที่นี่ (มากกว่า... xargs ... -I {} ... sed ...) คือความเร็ว: คุณหลีกเลี่ยงการเรียกใช้sed10 ล้านครั้ง มันจะเร็วขึ้นถ้าคุณหลีกเลี่ยงการใช้ Python (เนื่องจาก python ค่อนข้างช้า, ค่อนข้างมาก) ดังนั้น Perl อาจเป็นตัวเลือกที่ดีกว่าสำหรับงานนี้ ฉันไม่แน่ใจว่าจะทำสิ่งที่เทียบเท่ากับ Perl ได้อย่างสะดวก

วิธีการทำงานนี้คือการxargsเรียก Python ที่มีอาร์กิวเมนต์มากที่สุดเท่าที่จะสามารถใส่ในบรรทัดคำสั่งเดียวและดำเนินการต่อไปจนกว่าจะหมดอาร์กิวเมนต์ (ซึ่งถูกจัดหาโดยls -f *.txt) จำนวนข้อโต้แย้งสำหรับการเรียกใช้แต่ละครั้งจะขึ้นอยู่กับความยาวของชื่อไฟล์และอื่น ๆ อีกมากมาย fileinput.inputฟังก์ชั่นอัตราผลตอบแทนต่อเนื่องสายจากไฟล์ที่มีชื่อในการขัดแย้งแต่ละภาวนาของและinplaceตัวเลือกที่จะบอกว่ามันน่าอัศจรรย์ "จับ" การส่งออกและใช้มันเพื่อแทนที่แต่ละบรรทัด

โปรดทราบว่าreplaceวิธีสตริงของ Python ไม่ได้ใช้ regexps ถ้าคุณต้องการที่คุณต้องและการใช้งานimport re print re.sub(line, "blah", "blee")พวกเขาเป็น regexps Perl sed -rเข้ากันได้ซึ่งมีการจัดเรียงของรุ่นปราการแน่นหนาของคนที่คุณได้รับด้วย

แก้ไข

ตามที่อากิระกล่าวถึงในความคิดเห็นเวอร์ชันดั้งเดิมที่ใช้ glob ( ls -f *.txt) แทนที่findคำสั่งจะไม่ทำงานเพราะเชลล์จะประมวลผลโดยเชลล์ ( bash) เอง ซึ่งหมายความว่าก่อนที่คำสั่งจะทำงานแม้กระทั่งชื่อไฟล์ 10 ล้านชื่อจะถูกแทนที่ลงในบรรทัดคำสั่ง นี่ค่อนข้างรับประกันว่าจะเกินขนาดสูงสุดของรายการอาร์กิวเมนต์ของคำสั่ง คุณสามารถใช้xargs --show-limitsสำหรับข้อมูลเฉพาะระบบนี้

ขนาดสูงสุดของรายการอาร์กิวเมนต์จะถูกนำมาพิจารณาด้วยxargsเช่นกันซึ่ง จำกัด จำนวนอาร์กิวเมนต์ที่ส่งผ่านไปยังการร้องขอของ python แต่ละครั้งตามข้อ จำกัด นั้น เนื่องจากxargsยังคงต้องเรียกใช้ไพ ธ อนสองสามครั้งคำแนะนำของอากิระที่จะใช้os.path.walkในการรับรายชื่อไฟล์อาจจะช่วยคุณได้บ้าง


1
จุดประสงค์ของการใช้โอเปอเรเตอร์ glob (ซึ่งจะล้มเหลวสำหรับไฟล์จำนวนมากนั้นคืออะไร) ... จากนั้นให้ฟีดไฟล์ไปยังไพ ธ อนที่มีos.path.walk()?
akira

@akira: ผู้ประกอบการ glob คือการหลีกเลี่ยงการพยายามที่จะเปลี่ยนเนื้อหาของและ. ..แน่นอนมีวิธีอื่น ๆ ที่จะทำเช่นนั้น ( findแต่) ฉันพยายามที่จะติดอย่างใกล้ชิดกับสิ่งที่ OP เข้าใจ os.path.walkและนี่ก็เป็นเหตุผลที่ไม่ได้ใช้
intuited

@akira: คำแนะนำที่ดีแม้ว่าอาจจะเร็วกว่ามาก
intuited

ฉันคิดว่า OP จะเข้าใจos.path.walkได้ค่อนข้างง่าย
akira

0

ลอง:

ls | while read file; do (something to $file); done

2
ls -fจะดีกว่า; คุณต้องการที่จะรอให้มันstat()จัดเรียงไฟล์หลาย ๆ ไฟล์หรือไม่?
geekosaur

ตอนนี้ฉันพยายาม: สำหรับ f ใน * .txt; ทำ blah; เสร็จแล้ว ฉันจะให้มันตีถ้ามันล้มเหลว ขอขอบคุณ!
Sandro
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.