Jak uruchomić sed na ponad 10 milionach plików w katalogu?

16

Mam katalog zawierający 10144911 plików. Do tej pory próbowałem:

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

Rozbiłam moją skorupę, lsjest w tyldie, ale nie wiem, jak ją stworzyć.

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

Za dużo argumentów sed

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

Nie można rozwidlić więcej pamięci

Wszelkie inne pomysły na tworzenie tego rodzaju polecenia? Pliki nie muszą się ze sobą komunikować. ls | wc -lwydaje się działać (bardzo powoli), więc musi być możliwe.

Sandro
źródło
1
Byłoby szybciej, gdybyś mógł uniknąć wywoływania seddla każdego pliku. Nie jestem pewien, czy istnieje sposób na otwarcie, edycję, zapisanie i zamknięcie serii plików sed; jeśli prędkość jest niezbędna, możesz użyć innego programu, na przykład Perla lub Pythona.
intuicyjnie
@intuited: byłoby jeszcze szybciej nie robić nic z plikami ... poważnie? jeśli chcesz zmienić wzorzec w zestawie plików, musisz zajrzeć do każdego pliku, aby zobaczyć, czy istnieje wzorzec. jeśli wiesz z góry, że możesz pominąć „niektóre” pliki, to oczywiste jest, że nawet nie można ich dotknąć. a czas uruchamiania sedjest prawdopodobnie szybszy niż uruchomienie pythonlub perlteż, chyba że zrobisz wszystko w tym tłumaczu.
akira
@akira: Czy mówisz, że jednorazowe uruchomienie Perla lub Pythona dla tylu plików, które zmieszczą się w linii poleceń, jest droższe niż jednorazowe uruchomienie sed dla każdego z tych plików? Byłbym naprawdę zaskoczony, gdyby tak było. —————— Wydaje mi się, że nie zrozumiałeś, że moją sugestią jest jednorazowe wywołanie (uruchomienie) programu do edycji (lub co najmniej mniej razy - zobacz moją odpowiedź) i otwarcie, modyfikacja i ponowne zapisanie każdego z plików z kolei zamiast wywoływać program do edycji osobno dla każdego z tych plików.
intuicyjnie
twój pierwszy komentarz nie odzwierciedla tego, co naprawdę chciałeś powiedzieć: „zamień sed przez python / perl” .. po prostu robiąc to i patrząc na @ podany przez OP wiersz poleceń, niewinny czytelnik mógłby założyć, że „find. -exec python” to szybciej niż „find. -exec sed” .. co oczywiście nie jest prawdą. we własnej odpowiedzi wywołujesz python znacznie częściej niż jest to faktycznie potrzebne.
akira
Myślę, że akira źle zinterpretował twoją (intuicyjną) sugestię. Uważam, że sugerowałeś, aby połączyć kilka plików. Próbowałem tego przy mojej próbie xargs, czas spróbować ponownie :)
Sandro

Odpowiedzi:

19

Wypróbuj to:

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

Poda tylko jedną nazwę pliku do każdego wywołania sed. To rozwiąże problem „zbyt wielu argumentów dla sed”. Ta -Popcja powinna umożliwiać rozwidlenie wielu procesów jednocześnie. Jeśli 0 nie działa (powinno działać jak najwięcej), wypróbuj inne liczby (10? 100? Liczba rdzeni, którą masz?), Aby ograniczyć liczbę.

Wstrzymano do odwołania.
źródło
3
Prawdopodobnie trzeba będzie find . -name \*.txt -print0uniknąć sytuacji, w której powłoka rozszerzy glob i spróbuje przydzielić miejsce na 10 milionów argumentów do znalezienia .
Chris Johnsen
@ChrisJohnsen: Tak, zgadza się. Pośpieszyłem, zamieszczając swoją odpowiedź, i pominąłem te istotne części. Zedytowałem odpowiedź z tymi poprawkami. Dzięki.
Wstrzymano do odwołania.
Próbuję teraz ... krzyżuje palce
Sandro
7

Przetestowałem tę metodę (i wszystkie pozostałe) na 10 milionach (pustych) plikach o nazwie „hello 00000001” na „hello 10000000” (14 bajtów na nazwę).

UPDATE: Mam teraz obejmował quad-core bieg na 'find |xargs'metodzie (nadal bez 'sed'; tylko 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  

Oto podsumowanie tego, jak poszły podane odpowiedzi, gdy uruchomiono je z danymi testowymi wymienionymi powyżej. Te wyniki obejmują tylko podstawowe koszty ogólne; tzn. „sed” nie został nazwany. Proces sed prawie na pewno będzie najbardziej czasochłonny, ale pomyślałem, że byłoby interesujące zobaczyć porównanie nagich metod.

'find |xargs'Metoda Dennisa , wykorzystująca pojedynczy rdzeń, zajęła * 4 godziny 21 minut ** dłużej niż bash arraymetoda w no sedbiegu ... Jednak wielordzeniowa przewaga oferowana przez „find” powinna przewyższać różnice czasowe pokazane, gdy wezwany jest sed przetwarzanie plików ...

           | 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
źródło
2

Kolejna szansa na całkowicie bezpieczne znalezienie :

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
źródło
1

Jest to głównie nie na temat, ale możesz użyć

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

Główną korzyścią (ponad ... xargs ... -I {} ... sed ...) tutaj jest szybkość: unikasz wywoływania sed10 milionów razy. Byłoby jeszcze szybciej, gdybyś mógł uniknąć używania Pythona (ponieważ Python jest stosunkowo powolny), więc perl może być lepszym wyborem dla tego zadania. Nie jestem pewien, jak zrobić odpowiednik w perlu.

Działa to w ten sposób, że xargswywołuje Python z tyloma argumentami, ile może zmieścić się w jednym wierszu poleceń i kontynuuje działanie, dopóki nie zabraknie argumentów (które są dostarczane przez ls -f *.txt). Liczba argumentów dla każdego wywołania będzie zależeć od długości nazw plików i, hmm, innych rzeczy. fileinput.inputFunkcja daje kolejne linie z plików nazwanych w każdym wywołaniu argumentów za, a inplaceopcja mówi, że w magiczny sposób „złapać” wyjście i używać go zastąpić każdą linię.

Zauważ, że ciąg Pythona replace metoda nie używa wyrażeń regularnych; jeśli ich potrzebujesz, musisz import rei użyj print re.sub(line, "blah", "blee"). Są to RegExps kompatybilne z Perl, które są swego rodzaju mocno ufortyfikowanymi wersjami tych, które otrzymujesz sed -r.

edytować

Jak wspomina akira w komentarzach, oryginalna wersja używa glob ( ls -f *.txt) zamiastfind polecenia nie działałaby, ponieważ globs są przetwarzane przez bashsamą powłokę ( ). Oznacza to, że zanim polecenie zostanie uruchomione, w wierszu polecenia zostanie wstawionych 10 milionów nazw plików. Jest prawie pewne, że przekracza maksymalny rozmiar listy argumentów polecenia. Możesz użyć xargs --show-limitsdo tego informacji specyficznych dla systemu.

Uwzględniany jest również maksymalny rozmiar listy argumentów xargs, co ogranicza liczbę argumentów przekazywanych do każdego wywołania Pythona zgodnie z tym limitem. Ponieważ xargsnadal będziesz musiał wywoływać Pythona kilka razy, sugestia Akiry, aby użyć os.path.walklisty plików, prawdopodobnie zaoszczędzi ci trochę czasu.

intuicyjny
źródło
1
po co używać operatora glob (który i tak nie powiedzie się dla tak wielu plików) ... a następnie przesłać pliki do Pythona, który ma os.path.walk()?
akira
@akira: operator globu ma unikać próby zastąpienia treści .i ... Z pewnością istnieją inne sposoby, aby to zrobić (tj. find), Ale staram się jak najściślej trzymać się tego, co rozumie OP. Jest to również powód nieużywania os.path.walk.
intuicyjnie
@akira: Dobra sugestia, ale prawdopodobnie byłoby to znacznie szybsze.
intuicyjnie
myślę, że OP zrozumie os.path.walkcałkiem łatwo.
akira
0

Próbować:

ls | while read file; do (something to $file); done
Reuben L.
źródło
2
ls -fbyłoby lepiej; czy naprawdę chcesz na to poczekać stat()i posortować tyle plików?
geekozaur
teraz próbuję: dla f w * .txt; blać; gotowy. Uderzę to, jeśli to się nie powiedzie. Dziękuję Ci!
Sandro