Wie läuft sed auf über 10 Millionen Dateien in einem Verzeichnis?

16

Ich habe ein Verzeichnis mit 10144911 Dateien. Bisher habe ich Folgendes versucht:

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

Ich habe meine Muschel lszerschmettert , die ist in einer Tilda, aber ich kann nicht herausfinden, wie ich eine machen soll.

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

Zu viele Argumente für sed

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

Konnte kein Gedächtnis mehr haben

Haben Sie noch weitere Ideen, wie Sie einen solchen Befehl erstellen können? Die Dateien müssen nicht miteinander kommunizieren. ls | wc -lscheint zu funktionieren (sehr langsam), also muss es möglich sein.

Sandro
quelle
1
Es wäre schneller, wenn Sie das Aufrufen sedfür jede Datei vermeiden könnten . Ich bin nicht sicher, ob es eine Möglichkeit gibt, eine Reihe von Dateien zu öffnen, zu bearbeiten, zu speichern und zu schließen sed. Wenn es auf Geschwindigkeit ankommt, können Sie ein anderes Programm verwenden, z. B. Perl oder Python.
Intuited
@intuited: wäre es noch schneller überhaupt nichts an den dateien zu machen ... ernst? Wenn Sie ein Muster in einer Reihe von Dateien ändern möchten, müssen Sie in jeder Datei nachsehen, ob das Muster vorhanden ist. Wenn Sie im Voraus wissen, dass Sie "einige" Dateien überspringen können, ist es offensichtlich schneller, die Dateien nicht einmal zu berühren. und die Startzeit für sedist wahrscheinlich schneller als der Start pythonoder perlauch, es sei denn, Sie tun alles in diesem Interpreter.
Akira
@akira: Wollen Sie damit sagen, dass das einmalige Starten von Perl oder Python für so viele Dateien, wie auf eine Befehlszeile passen, teurer ist als das einmalige Starten von sed für jede dieser Dateien? Ich wäre wirklich überrascht, wenn das der Fall wäre. —————— Ich glaube, Sie haben nicht verstanden, dass mein Vorschlag darin besteht, das Bearbeitungsprogramm einmal aufzurufen (zu starten) (oder zumindest weniger - siehe meine Antwort) und es jede der Dateien öffnen, ändern und erneut speichern zu lassen statt das Bearbeitungsprogramm für jede dieser Dateien einzeln aufzurufen.
Intuited
Ihr erster Kommentar spiegelt nicht das wider, was Sie eigentlich sagen wollten: "Ersetzen Sie sed durch Python / Perl". Wenn Sie dies tun und auf die Kommandozeile schauen, die das OP gegeben hat, könnte ein unschuldiger Leser annehmen, dass "find. -exec python" ist schneller als "find. -exec sed" .. was offensichtlich nicht der Fall ist. In Ihrer eigenen Antwort rufen Sie Python viel häufiger auf, als es tatsächlich benötigt wird.
Akira
Ich denke, dass Akira Ihren (intuitierten) Vorschlag falsch interpretiert hat. Ich glaube, Sie haben vorgeschlagen, Dateien zusammenzufassen. Ich habe das mit meinem xargs-Versuch versucht, Zeit, es noch einmal zu versuchen :)
Sandro

Antworten:

19

Probieren Sie es aus:

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

Pro Aufruf von wird nur ein Dateiname eingegeben sed. Das wird das Problem "zu viele Argumente für sed" lösen. Die -POption sollte das gleichzeitige Verzweigen mehrerer Prozesse ermöglichen. Wenn 0 nicht funktioniert (es sollen so viele wie möglich ausgeführt werden), versuchen Sie es mit anderen Zahlen (10 - 100 - die Anzahl der Kerne, die Sie haben?), Um die Anzahl zu begrenzen.

Bis auf weiteres angehalten.
quelle
3
Wahrscheinlich muss find . -name \*.txt -print0vermieden werden , dass die Shell den Glob erweitert und versucht, Platz für 10 Millionen zu findende Argumente zu reservieren .
Chris Johnsen
@ ChrisJohnsen: Ja, das ist richtig. Ich beeilte mich, meine Antwort zu posten und vermisste es, die wesentlichen Teile mit einzuschließen. Ich habe meine Antwort mit diesen Korrekturen bearbeitet. Vielen Dank.
Bis auf weiteres angehalten.
Versucht es jetzt ... Daumen drücken
Sandro
7

Ich habe diese Methode (und alle anderen) an 10 Millionen (leeren) Dateien mit den Namen "Hallo 00000001" bis "Hallo 10000000" (14 Byte pro Name) getestet.

UPDATE: Ich habe jetzt einen Quad-Core- Lauf in die 'find |xargs'Methode aufgenommen (immer noch ohne 'sed'; nur 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  

Hier ist eine Zusammenfassung der Ergebnisse der angegebenen Antworten, wenn sie mit den oben genannten Testdaten verglichen wurden. Diese Ergebnisse betreffen nur die grundlegenden Gemeinkosten. dh "sed" wurde nicht aufgerufen. Der Sed-Prozess wird mit ziemlicher Sicherheit am zeitaufwändigsten sein, aber ich dachte, es wäre interessant zu sehen, wie die bloßen Methoden verglichen werden.

Dennis ' 'find |xargs'Methode, bei der ein einzelner Kern verwendet wurde, dauerte * 4 Stunden 21 Minuten ** länger als die bash arrayMethode bei einem no sedDurchlauf ... Der Mehrkernvorteil von' find 'sollte jedoch die Zeitunterschiede ausgleichen, die angezeigt werden, wenn sed angefordert wird Verarbeiten der Dateien ...

           | 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
quelle
2

Eine weitere Möglichkeit für den rundum sicheren Fund :

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
quelle
1

Dies ist meistens nicht zum Thema, aber Sie könnten verwenden

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

Der Hauptvorteil hier (gegenüber ... xargs ... -I {} ... sed ...) ist die Geschwindigkeit: Sie vermeiden es, sed10 Millionen Mal aufzurufen . Es wäre noch schneller, wenn Sie die Verwendung von Python vermeiden könnten (da Python relativ langsam ist), daher ist Perl möglicherweise die bessere Wahl für diese Aufgabe. Ich bin nicht sicher, wie ich das Äquivalent bequem mit Perl machen soll.

Dies funktioniert so, dass xargsPython mit so vielen Argumenten aufgerufen wird, wie auf eine einzelne Befehlszeile passen, und dies so lange, bis die Argumente (die von bereitgestellt werden ls -f *.txt) ausgehen. Die Anzahl der Argumente für jeden Aufruf hängt von der Länge der Dateinamen und einigen anderen Dingen ab. Die fileinput.inputFunktion liefert aufeinanderfolgende Zeilen aus den Dateien, die in den Argumenten jedes Aufrufs angegeben sind, und die inplaceOption weist sie an, die Ausgabe magisch zu "fangen" und sie zum Ersetzen jeder Zeile zu verwenden.

Beachten Sie, dass die String- replaceMethode von Python keine regulären Ausdrücke verwendet. Wenn Sie diese brauchen, müssen Sie import reund verwenden print re.sub(line, "blah", "blee"). Es handelt sich um Perl-kompatible RegExps, eine Art stark verstärkter Versionen derjenigen, mit denen Sie arbeiten sed -r.

bearbeiten

Wie Akira in den Kommentaren erwähnt, würde die Originalversion, bei der ein Glob ( ls -f *.txt) anstelle des findBefehls verwendet wird, nicht funktionieren, da Globs von der Shell ( bash) selbst verarbeitet werden. Dies bedeutet, dass vor der Ausführung des Befehls 10 Millionen Dateinamen in die Befehlszeile eingefügt werden. Dies wird so gut wie garantiert die maximale Größe der Argumentliste eines Befehls überschreiten. Sie können xargs --show-limitsfür systemspezifische Informationen dazu verwenden.

Die maximale Größe der Argumentliste wird ebenfalls berücksichtigt xargs, wodurch die Anzahl der Argumente, die an jeden Aufruf von Python übergeben werden, entsprechend dieser Grenze begrenzt wird. Da xargsPython noch einige Male aufgerufen os.path.walkwerden muss, spart Ihnen Akiras Vorschlag , die Dateiliste zu erstellen, wahrscheinlich etwas Zeit.

intuitiert
quelle
1
Was bringt es, den Glob-Operator zu verwenden (der sowieso für so viele Dateien fehlschlägt) ... und dann die Dateien an Python zu senden, das über Folgendes verfügt os.path.walk()?
Akira
@akira: Der Operator glob soll vermeiden, den Inhalt von .und zu ersetzen ... Natürlich gibt es auch andere Möglichkeiten, dies zu tun (zB find), aber ich versuche, mich so genau wie möglich an das zu halten, was das OP versteht. Dies ist auch der Grund für die Nichtverwendung os.path.walk.
Intuited
@akira: Guter Vorschlag, das wäre wohl deutlich schneller.
Intuited
Ich denke, dass OP os.path.walkganz leicht verstehen wird .
Akira
0

Versuchen:

ls | while read file; do (something to $file); done
Reuben L.
quelle
2
ls -fwäre besser; Möchten Sie wirklich darauf warten, stat()dass so viele Dateien sortiert werden?
Geekosaurier
im Moment versuche ich: für f in * .txt; mach bla; erledigt. Ich werde es versuchen, wenn es fehlschlägt. Vielen Dank!
Sandro