Bagaimana menjalankan sed pada lebih dari 10 juta file dalam suatu direktori?

16

Saya memiliki direktori yang memiliki 10144911 file di dalamnya. Sejauh ini saya sudah mencoba yang berikut ini:

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

Hancur cangkang saya, lsada di tilda tetapi saya tidak tahu cara membuatnya.

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

Terlalu banyak argumen untuk sed

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

Tidak dapat melakukan percabangan lagi, tidak ada lagi memori

Ada ide lain tentang cara membuat perintah semacam ini? File tidak perlu berkomunikasi satu sama lain. ls | wc -ltampaknya berfungsi (sangat lambat) sehingga harus dimungkinkan.

Sandro
sumber
1
Akan lebih cepat jika Anda dapat menghindari memohon seduntuk setiap file. Saya tidak yakin apakah ada cara untuk membuka, mengedit, menyimpan, dan menutup serangkaian file di sed; jika kecepatan sangat penting Anda mungkin ingin menggunakan program yang berbeda, mungkin perl atau python.
intuited
@intuited: akan lebih cepat untuk tidak melakukan apa pun pada file sama sekali ... serius? jika Anda ingin mengubah pola dalam satu set file Anda harus melihat ke dalam setiap file untuk melihat, jika ada polanya. jika Anda tahu sebelumnya bahwa Anda dapat melewatkan file 'beberapa', maka itu jelas lebih cepat untuk bahkan tidak menyentuh file. dan waktu startup untuk sedmungkin lebih cepat daripada meluncurkan pythonatau perljuga, kecuali jika Anda melakukan segalanya dalam penerjemah itu.
akira
@ Akira: Apakah Anda mengatakan bahwa meluncurkan perl atau python sekali untuk banyak file seperti yang sesuai pada baris perintah lebih mahal daripada meluncurkan sed sekali untuk masing-masing file? Saya akan sangat terkejut jika itu yang terjadi. —————— Saya kira Anda tidak mengerti bahwa saran saya adalah untuk memohon (memulai) program pengeditan sekali (atau setidaknya lebih sedikit kali - lihat jawaban saya), dan minta itu dibuka, modifikasi dan resave masing-masing file pada gilirannya, daripada menjalankan program pengeditan secara terpisah untuk masing-masing file tersebut.
intuited
komentar pertama Anda tidak mencerminkan apa yang benar-benar ingin Anda katakan: "ganti sed dengan python / perl" .. dengan hanya melakukan itu dan melihat @ commandline yang telah diberikan OP, pembaca yang tidak bersalah dapat menganggap bahwa "find. -exec python" adalah lebih cepat daripada "find. -exec sed" .. yang jelas bukan itu masalahnya. dalam jawaban Anda sendiri, Anda memanggil python lebih sering daripada yang sebenarnya dibutuhkan.
akira
Saya pikir akira salah menafsirkan saran Anda (intuisi). Saya percaya bahwa Anda menyarankan untuk menggabungkan banyak file. Saya mencobanya dengan upaya xargs saya, saatnya mencobanya lagi :)
Sandro

Jawaban:

19

Cobalah ini:

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

Itu hanya akan memberi makan satu nama file untuk setiap doa sed. Itu akan memecahkan masalah "terlalu banyak args untuk sed". The -Ppilihan harus memungkinkan beberapa proses yang harus bercabang pada waktu yang sama. Jika 0 tidak berfungsi (seharusnya menjalankan sebanyak mungkin), coba angka lainnya (10? 100? Jumlah inti yang Anda miliki?) Untuk membatasi jumlahnya.

Dijeda sampai pemberitahuan lebih lanjut.
sumber
3
Mungkin, perlu untuk find . -name \*.txt -print0menghindari shell memperluas glob dan mencoba untuk mengalokasikan ruang untuk 10 juta argumen untuk ditemukan .
Chris Johnsen
@ ChrisJohnsen: Ya, itu benar. Saya bergegas memposting jawaban saya dan ketinggalan termasuk bagian-bagian penting itu. Saya telah mengedit jawaban saya dengan koreksi itu. Terima kasih.
Dijeda sampai pemberitahuan lebih lanjut.
Mencoba sekarang ... menyilangkan jari
Sandro
7

Saya telah menguji metode ini (dan yang lainnya) pada 10 juta file (kosong), bernama "halo 00000001" menjadi "halo 10000000" (14 byte per nama).

UPDATE: Saya sekarang sudah memasukkan menjalankan quad-core pada 'find |xargs'metode (masih tanpa 'sed'; just 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  

Berikut ringkasan bagaimana jawaban yang diberikan bernasib ketika dijalankan terhadap data uji yang disebutkan di atas. Hasil ini hanya melibatkan biaya dasar; yaitu 'sed' tidak dipanggil. Proses sed hampir pasti akan menjadi yang paling memakan waktu, tetapi saya pikir akan menarik untuk melihat bagaimana metode telanjang dibandingkan.

'find |xargs'Metode Dennis , menggunakan single core, membutuhkan waktu * 4 jam 21 menit ** lebih lama daripada bash arraymetode yang no seddijalankan ... Namun, keunggulan multi-core yang ditawarkan oleh 'find' harus lebih besar daripada perbedaan waktu yang ditunjukkan ketika sed dipanggil untuk memproses file ...

           | 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
-----------+---------+---------+----------------------------------------------------- 
Peter.O
sumber
2

Peluang lain untuk penemuan yang sepenuhnya aman :

while IFS= read -rd $'\0' path
do
    file_path="$(readlink -fn -- "$path"; echo x)"
    file_path="${file_path%x}"
    sed -i -e 's/blah/blee/g' -- "$file_path"
done < <( find "$absolute_dir_path" -type f -print0 )
l0b0
sumber
1

Ini sebagian besar di luar topik, tetapi Anda dapat menggunakannya

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

Manfaat utama di sini (lebih ... xargs ... -I {} ... sed ...) adalah kecepatan: Anda menghindari meminta sed10 juta kali. Akan lebih cepat lagi jika Anda bisa menghindari menggunakan Python (karena python agak lambat, relatif), jadi perl mungkin merupakan pilihan yang lebih baik untuk tugas ini. Saya tidak yakin bagaimana melakukan yang setara dengan nyaman dengan perl.

Cara ini bekerja adalah yang xargsakan memanggil Python dengan argumen sebanyak yang dapat ditampung pada satu baris perintah, dan terus melakukan itu sampai kehabisan argumen (yang dipasok oleh ls -f *.txt). Jumlah argumen untuk setiap doa akan tergantung pada panjang nama file dan, um, beberapa hal lainnya. Itufileinput.input Fungsi menghasilkan garis berurutan dari file bernama dalam argumen setiap permintaan ini, dan inplacepilihan mengatakan itu untuk ajaib "menangkap" output dan menggunakannya untuk mengganti setiap baris.

Perhatikan bahwa replacemetode string Python tidak menggunakan regexps; jika Anda membutuhkannya, Anda harus import redan menggunakannya print re.sub(line, "blah", "blee"). Mereka adalah Perl-Compatible RegExps, yang merupakan semacam versi yang sangat dijaga dari yang Anda dapatkansed -r .

sunting

Seperti akira menyebutkan dalam komentar, versi asli menggunakan glob ( ls -f *.txt) sebagai ganti findperintah tidak akan berfungsi karena gumpalan diproses oleh shell ( bash) itu sendiri. Ini berarti bahwa bahkan sebelum perintah dijalankan, 10 juta nama file akan diganti ke dalam baris perintah. Ini cukup dijamin untuk melebihi ukuran maksimum dari daftar argumen perintah. Kamu bisa memakaixargs --show-limits info khusus sistem untuk ini.

Ukuran maksimum dari daftar argumen juga diperhitungkan oleh xargs, yang membatasi jumlah argumen yang diteruskan ke setiap pemanggilan python sesuai dengan batas itu. Karena xargsmasih harus memanggil python beberapa kali, saran akira untuk menggunakan os.path.walkuntuk mendapatkan daftar file mungkin akan menghemat waktu Anda.

intuisi
sumber
1
apa gunanya menggunakan operator glob (yang akan gagal untuk banyak file pula) ... dan kemudian memberi makan file ke python yang dimiliki os.path.walk()?
akira
@akira: operator glob adalah untuk menghindari mencoba mengganti konten .dan ... Tentu saja ada cara lain untuk melakukan itu (yaitu find) tetapi saya mencoba untuk tetap sedekat mungkin dengan apa yang dipahami OP. Ini juga alasan untuk tidak menggunakan os.path.walk.
intuited
@ Akira: Saran yang bagus, mungkin itu akan jauh lebih cepat.
intuited
Saya pikir OP akan mengerti os.path.walkdengan mudah.
akira
0

Mencoba:

ls | while read file; do (something to $file); done
Reuben L.
sumber
2
ls -fakan lebih baik; apakah Anda benar-benar ingin menunggu stat()dan mengurutkan banyak file?
geekosaur
sekarang saya sedang mencoba: untuk f di * .txt; lakukan bla; selesai Saya akan berikan itu pukulan jika gagal. Terima kasih!
Sandro