Come eseguire sed su oltre 10 milioni di file in una directory?


16

Ho una directory che contiene 10144911 file. Finora ho provato quanto segue:

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

Ho rotto il mio guscio, lsè in una tilda ma non riesco a capire come farne uno.

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

Troppi argomenti per sed

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

Non è più possibile biforcarsi più memoria

Altre idee su come creare questo gentile comando? I file non devono comunicare tra loro. ls | wc -lsembra funzionare (molto lentamente) quindi deve essere possibile.


1
Sarebbe più veloce se puoi evitare di invocare sedper ogni file. Non sono sicuro che esista un modo per aprire, modificare, salvare e chiudere una serie di file sed; se la velocità è essenziale, potresti voler usare un programma diverso, forse perl o python.
intuito il

@intuited: sarebbe ancora più veloce non fare nulla ai file ... sul serio? se vuoi cambiare un pattern in un set di file devi guardare in ogni file per vedere se c'è il pattern. se sai in anticipo che puoi saltare 'alcuni' file, allora è ovvio più velocemente non toccare nemmeno i file. e il tempo di avvio per sedè probabilmente più veloce del lancio pythono perlanche, tranne se si fa tutto in quell'interprete.
Akira,

@akira: Stai dicendo che avviare perl o python una volta per tutti i file che si adatteranno su una riga di comando è più costoso che lanciare sed una volta per ognuno di quei file? Sarei davvero sorpreso se fosse così. —————— Suppongo che tu non abbia capito che il mio suggerimento è di invocare (avviare) il programma di modifica una volta (o almeno meno volte - vedi la mia risposta), e di averlo aperto, modificare e salvare nuovamente tutti i file a sua volta, anziché invocare il programma di modifica separatamente per ciascuno di quei file.
intuito il

il tuo primo commento non riflette ciò che volevi davvero dire: "sostituisci sed con python / perl" .. semplicemente facendo ciò e guardando @ la riga di comando OP ha dato, un lettore innocente potrebbe supporre che "trova. -exec python" è più veloce di "find. -exec sed" .. che ovviamente non è il caso. nella tua risposta chiami python molto più spesso di quanto sia effettivamente necessario.
Akira,

Penso che Akira abbia frainteso il tuo suggerimento (intuito). Credo che stavi suggerendo di raggruppare i file insieme. Ci ho provato con il mio tentativo di xargs, è ora di riprovare :)
Sandro,

Risposte:


19

Prova questo:

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

Alimenterà solo un nome file per ogni invocazione di sed. Ciò risolverà il problema "troppi argomenti per sed". L' -Popzione dovrebbe consentire a più processi di essere biforcati contemporaneamente. Se 0 non funziona (dovrebbe essere eseguito il maggior numero possibile), prova altri numeri (10? 100? Il numero di core che hai?) Per limitare il numero.


3
Probabilmente, dovrà essere find . -name \*.txt -print0per evitare che la shell espanda il glob e cerchi di allocare spazio per 10 milioni di argomenti da trovare .
Chris Johnsen,

@ChrisJohnsen: Sì, è corretto. Mi sono precipitato a pubblicare la mia risposta e mi sono perso anche quelle parti essenziali. Ho modificato la mia risposta con quelle correzioni. Grazie.
In pausa fino a nuovo avviso.

Provandolo ora ... incrocia le dita
Sandro

7

Ho testato questo metodo (e tutti gli altri) su 10 milioni di file (vuoti), denominati "ciao 00000001" a "ciao 10000000" (14 byte per nome).

AGGIORNAMENTO: Ora ho incluso una corsa quad-core sul 'find |xargs'metodo (ancora senza 'sed'; solo 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  

Ecco un riepilogo di come si sono comportate le risposte fornite quando eseguite rispetto ai dati di test sopra menzionati. Questi risultati riguardano solo le spese generali di base; cioè 'sed' non è stato chiamato. Il processo sed sarà quasi sicuramente il più dispendioso in termini di tempo, ma ho pensato che sarebbe stato interessante vedere come si confrontano i metodi nudi.

Il 'find |xargs'metodo di Dennis , usando un singolo core, ha richiesto * 4 ore e 21 minuti ** in più rispetto al bash arraymetodo in una no sedcorsa ... Tuttavia, il vantaggio multi-core offerto da 'find' dovrebbe superare le differenze di tempo mostrate quando viene richiesto sed elaborazione dei 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
-----------+---------+---------+----------------------------------------------------- 

2

Un'altra opportunità per la scoperta completamente sicura :

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 )

1

Questo è per lo più fuori tema, ma potresti usarlo

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

Il vantaggio principale qui (oltre ... xargs ... -I {} ... sed ...) è la velocità: eviti di invocare sed10 milioni di volte. Sarebbe ancora più veloce se tu potessi evitare di usare Python (dato che python è un po 'lento, relativamente), quindi perl potrebbe essere una scelta migliore per questo compito. Non sono sicuro di come fare l'equivalente convenientemente con il perl.

Il modo in cui funziona è che xargsinvocherà Python con tutti gli argomenti che può stare su una singola riga di comando e continuerà a farlo fino a quando non si esauriscono gli argomenti (che vengono forniti da ls -f *.txt). Il numero di argomenti per ogni invocazione dipenderà dalla lunghezza dei nomi dei file e, um, da alcune altre cose. La fileinput.inputfunzione produce righe successive dai file indicati negli argomenti di ogni invocazione e l' inplaceopzione dice di "catturare" magicamente l'output e usarlo per sostituire ogni riga.

Nota che il replacemetodo stringa di Python non usa regexps; se ne hai bisogno, devi import ree usalo print re.sub(line, "blah", "blee"). Sono RegExps compatibili con Perl, che sono una specie di versioni fortemente fortificate di quelle che ottieni sed -r.

modificare

Come Akira menziona nei commenti, la versione originale che utilizza un glob ( ls -f *.txt) al posto del findcomando non funzionerebbe perché i globs vengono elaborati dalla shell ( bash) stessa. Ciò significa che prima ancora che il comando venga eseguito, nella riga di comando verranno sostituiti 10 milioni di nomi di file. Questo è praticamente garantito per superare la dimensione massima dell'elenco di argomenti di un comando. È possibile utilizzare xargs --show-limitsper informazioni specifiche del sistema su questo.

Viene inoltre presa in considerazione la dimensione massima dell'elenco degli argomenti xargs, che limita il numero di argomenti che passa a ciascuna invocazione di Python in base a tale limite. Dato che xargsdovrà ancora invocare Python parecchie volte, il suggerimento di Akira da usare os.path.walkper ottenere l'elenco dei file probabilmente ti farà risparmiare un po 'di tempo.


1
qual è il punto di usare l'operatore glob (che fallirà comunque per quel numero di file) ... e quindi inviare i file a Python che ha os.path.walk()?
Akira,

@akira: l'operatore glob deve evitare di provare a sostituire il contenuto di .e ... Certamente ci sono altri modi per farlo (cioè find) ma sto cercando di attenermi il più vicino possibile a ciò che l'OP capisce. Questo è anche il motivo del mancato utilizzo os.path.walk.
intuito il

@akira: un buon suggerimento, tuttavia, sarebbe probabilmente molto più veloce.
intuito il

penso che OP capirà os.path.walkabbastanza facilmente.
Akira,

0

Provare:

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

2
ls -fsarebbe meglio; vuoi davvero aspettare stat()e ordinare quel numero di file?
Geekosaur,

ora sto provando: per f in * .txt; fare blah; fatto. Ti darò una botta se fallisce. Grazie!
Sandro
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.